Local development
stackbone devruns your agents and durable workflows against a local Postgres, Redis and object store. It serves the Studio API onhttp://127.0.0.1:4242with the identical wire shape the Stackbone control plane serves in cloud.You publish nothing and nothing runs in the cloud. Save a file and the session reloads that agent or workflow in place, against real datastores.
What it does
stackbone dev walks through a short sequence of boot stages and
keeps the long-lived processes alive until you press Ctrl-C, then tears
them down in reverse order:
Stackbone's own control surface comes up first. The CLI does not compile or run your code until the environment can support it, so a workspace that cannot start yet still leaves you a reachable emulator and a working link.
- Docker stack: Postgres (with
pgvector) on:5433, Redis on:6380(the backend that powers durable workflows and the job runtime), and MinIO on:9004/:9005. The Compose project is namedstackbone-dev-<agent-slug>so multiple projects can run in parallel. - Platform migrations (one-shot): applies the
stackbone_platform.*schemas the emulator needs so its tables match cloud, then exits. - Your migrations (one-shot): applies your own schema migrations against the local Postgres, then exits.
- Studio API: the local control-plane emulator on
127.0.0.1:4242(or--port), speaking the same protocol asapi.stackbone.ai. frpctunnel (mandatory): exposes127.0.0.1:4242over HTTPS through Stackbone's relay at*.tun.stackbone.aiso the cloud-hosted Studio (https://app.stackbone.ai/app) can reach the emulator without tripping mixed-content / local-network-access rules in Safari, Brave or Chrome.- The boot gate: the check that decides whether your code runs at all. See The first run below.
- Your workflows and agents: builds the durable Workflow runtime that hosts your discovered workflows, then registers every discovered deep agent in-process.
How the workspace is discovered
The CLI discovers a workspace by convention from the files on disk, so you
maintain no registry. stackbone dev scans your project this way:
- Agents: every folder under
deep-agents/that contains anindex.tsexporting an agent definition. The folder name is both the agent name and themodela client selects to talk to it. There is no per-agent manifest, and no port: every agent runs inside the one emulator process. - Workflows: every
workflows/<name>.workflow.ts. The workflow name is the file basename without the.workflow.tssuffix, and the exported function is the camel-cased name plusWorkflow(soqualify-lead.workflow.tsexportsqualifyLeadWorkflow).
stackbone init scaffolds a workspace shell, a deep-agents/ folder, a
workflows/ folder and supporting files, and (depending on the optional
first piece you ask for) may add your first agent. From there stackbone dev
boots every discovered agent plus the durable
Workflow runtime for your workflows.
stackbone.config.ts is an optional override. Most projects need none. If it
exists it default-exports defineWorkspace(...), and every field it sets wins
over the convention scan:
| Field | Overrides |
|---|---|
workflows |
the workflows/*.workflow.ts scan |
deepAgents |
the deep-agents/* scan. Declare it to hide an agent from Studio with $internal: true |
build.external |
nothing on disk. Names the packages the compiler must not inline |
Omit deepAgents and the scan still decides. Declare it and the CLI drops the
scan, so the list has to name every agent you want to run.
// stackbone.config.ts (optional: overrides the convention scan)
import { defineWorkspace } from '@stackbone/sdk';
export default defineWorkspace({
agents: [], // required field; empty in a deep-agent workspace
workflows: [
{
name: 'onboarding',
module: 'workflows/onboarding.workflow.ts',
export: 'onboardingWorkflow',
},
],
// Optional. Omit it and `deep-agents/` decides. Declare it and this list is
// the whole registry: `$internal` keeps an agent out of Studio.
deepAgents: [{ name: 'lead-qualifier', dir: 'deep-agents/lead-qualifier' }],
// Rarely needed: a package that cannot survive bundling (native binary,
// WASM asset, runtime file lookups) stays a bare import here and in the
// deployed image alike.
build: { external: ['sharp'] },
});stackbone dev compiles each deep-agents/<name>/index.ts into an
index.mjs next to it, using the same compiler and the same settings as
stackbone build. The compiler bundles your own dependencies in, so a package
that works locally works in the container. See
Keeping a package out of the bundle.
See Agents and Workflows for what these are and how to author them.
Prerequisites
- Docker: Docker Desktop on macOS; Docker Engine on Linux. On Windows the CLI must run from WSL2. Docker Desktop with WSL2 integration enabled (or Docker Engine inside the Ubuntu distro) covers it.
- A project: run
stackbone initto scaffold a workspace shell (adeep-agents/folder, aworkflows/folder and supporting files). Add pieces over time withstackbone add agent <name>,stackbone add workflow <name>orstackbone add workflow-agent <name>. A project someone handed you needs none of that: it arrives unlinked, anddevasks which agent it is. See A project that arrived without a link. - At least one agent or workflow:
stackbone devdiscovers your workspace by convention: anydeep-agents/<name>/index.ts(or aworkflows/<name>.workflow.ts) is enough to activate the runtime. Astackbone.config.tsis optional and only needed when you want to override the workflow scan or keep a package out of the agent bundle. frpc(managed for you). The CLI auto-fetches a pinned, per-platform binary into~/.cache/stackbone/bin/on first run if missing. The tunnel is mandatory, so there is no opt-out flag. SetSTACKBONE_FRPC_BINto point at a system install when your environment forbids untrusted binaries.
A project that arrived without a link
A project you cloned from someone else's repository has no link to an agent.
.stackbone/ is gitignored, so the link never travels with the code.
stackbone dev does not stop for that. It lists the agents your organization
owns, lets you pick one or create a new one named after the folder, writes
.stackbone/project.json, adds it to .gitignore and carries on booting.
Neither of the other commands fits this case:
stackbone initregisters a new agent and writes its ownpackage.json,tsconfig.jsonand scaffold over the project you were handed.stackbone linkregisters a box you have already deployed, so it requires that box's URL, HMAC secret and image tag.
When the CLI cannot ask, it fails instead of guessing and prints what to do.
That covers any run without a terminal, a CI environment variable, --yes or
--json. See
This directory is not linked to an agent.
Synopsis
stackbone dev [--port 4242] [--listen] [--no-auto-migrate] [--print-contract] [--verbose]| Flag | Default | Why you'd flip it |
|---|---|---|
--port |
4242 |
Pick another Studio API port. |
--listen |
off | Bind the Studio server to 0.0.0.0 so other machines on the LAN can reach it. The CLI prints a visible warning. |
--auto-migrate |
on | Generate and apply a migration when you edit src/schema.ts mid-session. Use --no-auto-migrate to write migrations by hand. |
--print-contract |
off | Print the JSON contract this CLI advertises (same payload as the emulator's /api/contract) and exit without booting the dev session. |
--verbose |
off | Stream every log line and docker compose output. Default UI uses per-stage spinners; --verbose switches back to the raw firehose. |
--auto-migrate only governs the schema watcher. The CLI applies pending .sql
files under .stackbone/migrations on every boot either way. Pass neither form
and the session reads dev.autoMigrate from
agent.yaml; either flag overrides that
file for this session.
Override the frpc binary location with the env var:
export STACKBONE_FRPC_BIN=/opt/homebrew/bin/frpcPin a specific frpc release with STACKBONE_FRPC_VERSION if the default
pinned version is incompatible with your platform.
The boot banner
Once the stack is up the CLI prints a banner with the session's URLs and identity:
╭─ stackbone studio ─────────────────────────────────────────╮
│ │
│ ▸ Open Studio https://app.stackbone.ai/app/<orgSlug>/installations/<id>?stackbone-dev=https://<subdomain>.tun.stackbone.ai
│ │
│ Tunnel https://<subdomain>.tun.stackbone.ai │
│ Local http://127.0.0.1:4242 │
│ │
│ Agent <agent name> │
│ Protocol v<N> · <count> capabilities │
│ │
╰────────────────────────────────────────────────────────────╯Use the Open Studio deeplink: it opens the production Studio UI pointed at
your local emulator through the tunnel. Fall back to the Local URL for
offline work, or when the relay handshake fails. The Protocol line reports
the contract version and capability count this CLI advertises on
GET /api/contract.
The first run
A deep agent that names its model as a bare id ("openai/gpt-4o-mini")
cannot build without a model provider: an OpenAI-compatible endpoint you
point the deployment at. On a fresh machine there is none, so stackbone dev
would otherwise fail every agent over a missing setting.
The boot gate prevents that. Stages 1 to 5 above still run, so the emulator is listening and the tunnel is up. The CLI then stops at the gate: it does not build your workflows, mount the World or register any deep agent. It prints what is missing plus a link straight into Studio's guided setup:
Your workflows and agents are not running yet.
This deployment has no model provider configured, so agents that name a
model cannot start.
Configure one in Studio:
https://app.stackbone.ai/app/<orgSlug>/installations/<id>/studio/first-run?stackbone-dev=https://<subdomain>.tun.stackbone.ai
Prefer the terminal? Export MODEL_PROVIDER_BASE_URL (and MODEL_PROVIDER_API_KEY,
if your provider needs a key) and run `stackbone dev` again.The session stays alive and serving while it waits. Studio, the model-provider screens and the agent catalogue all work, and the catalogue reports an empty workspace rather than a broken one.
The two ways the gate opens
- A model provider resolves, from your shell or from the deployment's own store (see Model provider). If you already export the variable, you never see the gate, and an unattended boot (CI, a scripted container start) does not wait for a browser.
- Somebody completed the first run on this deployment. Finishing the guided setup in Studio marks it done, including the deliberate "Continue without a provider" choice. From then on the gate opens on every boot.
Either way the decision and its reason go into the boot output, so you can read
from the log alone why the workloads did or did not start. --json reports the
same thing as a gate object with open and reason.
When you finish the setup in the browser, the terminal picks it up in place:
the held stages run, the CLI prints Setup complete, and your agents answer
without a restart.
The marker belongs to the deployment
The deployment's own database stores "first run done", so the marker is tied to the box and not to your home directory or your user account. A teammate opening the same box skips the setup you already completed.
To go through it again (you changed providers, or you are onboarding a colleague), open Studio's Model provider screen and use Run the guided setup again. It clears the marker and reopens the flow. Your saved provider stays in place, and because a provider now resolves, the gate stays open.
Hot reload while the session runs
A save reloads the piece you touched. The session, the emulator, the tunnel and the durable workflow world all stay up, so open chat sessions and in-flight runs survive the reload.
| You save | What reloads |
|---|---|
deep-agents/<name>/** |
Only the agents whose files changed. The CLI re-bundles them and swaps them into the registry in place |
workflows/** |
The whole workflow set. The CLI re-scans the folder, recompiles, and remounts the workflow runtime |
src/schema.ts |
Nothing restarts. See --auto-migrate for what happens to the migration |
config.schema.ts |
.stackbone/config.d.ts, so your editor types stackbone.config from the new shape |
Only source files count (.ts, .tsx, .mts, .cts, .js, .mjs, .cjs).
Spec files and .d.ts files are ignored, and a burst of writes from one editor
save collapses into a single reload.
A reload that fails prints the error and leaves the previous version running. Fix the file and save again.
Adding the first agent to a project that had no
deep-agents/folder when the session started needs a restart: there was no folder to watch. Every later agent you add is picked up live.
CORS allowlist
The emulator enforces an explicit CORS allowlist (no *). Default origins:
https://app.stackbone.aihttps://chat.stackbone.aihttp://localhost:*(any port; covers any local dev server you run)
Add origins per-repo via stackbone.config.json at the project root
(committable, unlike .stackbone/project.json):
{
"schemaVersion": 1,
"studio": {
"corsOrigins": [
"https://app.stackbone.ai",
"https://chat.stackbone.ai",
"https://staging.stackbone.ai",
"http://localhost:*",
],
},
}Or override per run with the env var (CSV):
STACKBONE_CORS_ALLOW_ORIGINS="https://staging.stackbone.ai,http://localhost:*" stackbone devPrecedence: env > file > default. Each entry is either an exact
origin or a wildcard with * matching a single path segment (no dot
crossing): https://*.stackbone.ai works; localhost:* matches ports
but not subdomains.
Environment injected into your agents
The runtime injects these env vars so the ambient stackbone client,
stackbone.database, .storage, .rag, .config, .secrets, .ai,
.approval, plus stackbone.connection(id), resolves the same
datastores locally as in production. You import that client directly
inside a tool's execution or a workflow 'use step'; there is no handler
wiring to configure.
| Variable | Points at |
|---|---|
PORT |
The port the emulator listens on. |
STACKBONE_POSTGRES_URL |
Local Postgres on :5433 with the stackbone_dev database. |
STACKBONE_S3_ENDPOINT |
MinIO on :9004. |
STACKBONE_S3_BUCKET |
stackbone-dev. |
STACKBONE_S3_ACCESS_KEY |
stackbone. |
STACKBONE_S3_SECRET_KEY |
stackbone-secret. |
STACKBONE_S3_REGION |
us-east-1 (MinIO accepts any region). |
STACKBONE_S3_FORCE_PATH_STYLE |
true, the addressing style MinIO speaks. See stackbone.storage. |
STACKBONE_AGENT_ID |
The slug of the workspace (the multi-tenant key prefix for storage and observability). |
STACKBONE_SECRET_KEY |
The per-agent key stackbone.secrets encrypts and decrypts with. Derived locally; never set it yourself. |
WORKFLOW_REDIS_URL |
Local Redis on :6380, the durable backend the workflow runtime replays from. |
MODEL_PROVIDER_API_KEY |
The model-provider key, when one resolves (see below). Absent for a keyless local gateway. |
MODEL_PROVIDER_BASE_URL |
The OpenAI-compatible base URL, when one resolves. Absent means the OpenRouter default. |
See Configuration → Environment variables for the full list.
Model provider
client.ai and the deep-agent string models talk to an OpenAI-compatible
endpoint: OpenRouter, or any gateway you point them at (Ollama, LM Studio,
LiteLLM, vLLM). stackbone dev resolves that credential in this order:
- Your shell wins. Export
MODEL_PROVIDER_API_KEY/MODEL_PROVIDER_BASE_URLand the CLI seeds them as-is, with no clicks and no call to the control plane. The olderOPENROUTER_API_KEY/OPENROUTER_BASE_URLnames still work as a deprecated alias. - Otherwise, whatever you configured in Studio. The Model provider
screen writes the value into the emulator's own Postgres, so it survives
stackbone devrestarts. - Otherwise the CLI seeds nothing, and the session comes up behind the boot gate. The emulator serves Studio, the model-provider screens and an empty agent catalogue. Your workflows and agents wait until a provider is configured or you choose to continue without one.
A base URL alone is enough: a keyless local gateway needs nothing but its URL. The boot log names which of the three branches you landed on.
Once you continue without a provider, agents that name a model still register. They come up degraded, visible in Studio with the reason they cannot answer.
Generated files
While a dev session runs, the CLI writes editor-type artifacts under
.stackbone/ so your code gets typed autocompletion off your workspace:
.stackbone/agents.d.ts: narrowscallDeepAgent(name, input)(the workflow-to-agent helper) to your declared agent names, so a typo is a compile error..stackbone/workflows.d.ts: narrows the workflownameonstackbone.workflows.start/.startAndWait/.schedulethe same way, to the workflows this workspace declares..stackbone/connect.d.ts: typesstackbone.connection(id)and each connector's methods from its schema..stackbone/config.d.ts: typesstackbone.configfrom yourconfig.schema.ts.
When your workspace declares workflows, the durable Workflow build output
also lands under .well-known/workflow/v1/. All of these regenerate on
every stackbone dev boot, so add .stackbone/ (and .well-known/) to
your .gitignore.
What the emulator serves locally
The local emulator on 127.0.0.1:4242 exposes the same surfaces the cloud
control plane serves, which is how Studio and the inspection commands talk
to your running session:
GET /api/contract: the protocol-version handshake (below).POST /openai/v1/chat/completionsandPOST /anthropic/v1/messages: chat with any agent in the workspace, selected by themodelfield. See Agent protocol.GET /api/discovery: the workspace manifest, every agent and workflow it found.POST /api/workflows/:name/startandPOST /api/workflows/:name/chat: trigger a durable workflow run (one returns therunId, the other streams the run as it executes).GET /api/workflows/:name/schema: the input/output JSON Schema that workflow declares.GET /api/runs: the recent runs, andGET /api/events: an SSE stream of agent logs and lifecycle frames.GET /api/health: a lightweight readiness check for the dev session.
These are the surfaces the rest of the CLI targets against a running dev
session: stackbone runs (list, get, retry, cancel runs),
stackbone workflows (list, schema, start) and stackbone hitl
(approve/reject pending approvals).
Tearing down
Ctrl-C stops your agents, the HTTP server, the tunnel and the Compose
stack in reverse order. The teardown leaves the stackbone-dev-<slug> Compose
project in place between runs so Postgres data persists across stackbone dev
restarts. Wipe it with:
docker compose -p stackbone-dev-<slug> down -vUsing a system frpc binary
When the auto-fetcher cannot run (corporate proxy, unsupported platform,
or you prefer a system-managed binary), install frpc from
fatedier/frp releases and
point the CLI at it:
# macOS / Linux: download the release archive, extract, drop `frpc` on PATH
export STACKBONE_FRPC_BIN=/opt/homebrew/bin/frpc
stackbone devThe CLI uses STACKBONE_FRPC_BIN verbatim, so any path works. The tunnel
is mandatory and there is no --no-tunnel opt-out, because the dashboard
shares the session through the local_tunnel_url field on the installation
row.
Studio handshake
The cloud-hosted Studio (under app.stackbone.ai/app/<orgSlug>/installations/<id>)
performs a handshake against the emulator's GET /api/contract to detect
the protocol version. If your CLI is older than minSupported the UI shows a
version drift banner. Upgrade with pnpm add -g @stackbone/cli@latest
(or update the project-local install) and rerun stackbone dev.
Troubleshooting
The failures you are most likely to hit here (Docker not running, port 4242
already taken, the relay refusing a tunnel grant, a workspace held behind the
boot gate, an agent that will not build) each have their own entry, keyed on the
error.code the CLI emits, on
Troubleshooting.