--- title: 'Local development' description: 'How stackbone dev boots your agents and durable workflows locally, injects env vars and tunnels to cloud Studio.' position: 7 --- # Local development > `stackbone dev` runs your agents and durable > [workflows](/docs/sdk/workflows/overview) against a local Postgres, Redis and > object store. It serves the Studio API on `http://127.0.0.1:4242` with 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. 1. **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 named `stackbone-dev-` so multiple projects can run in parallel. 2. **Platform migrations** _(one-shot)_: applies the `stackbone_platform.*` schemas the emulator needs so its tables match cloud, then exits. 3. **Your migrations** _(one-shot)_: applies your own schema migrations against the local Postgres, then exits. 4. **Studio API**: the local control-plane emulator on `127.0.0.1:4242` (or `--port`), speaking the same protocol as `api.stackbone.ai`. 5. **`frpc` tunnel** _(mandatory)_: exposes `127.0.0.1:4242` over HTTPS through Stackbone's relay at `*.tun.stackbone.ai` so 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. 6. **The boot gate**: the check that decides whether your code runs at all. See [The first run](#the-first-run) below. 7. **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 an `index.ts` exporting an agent definition. The folder name is both the agent name and the `model` a 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/.workflow.ts`. The workflow name is the file basename without the `.workflow.ts` suffix, and the exported function is the camel-cased name plus `Workflow` (so `qualify-lead.workflow.ts` exports `qualifyLeadWorkflow`). `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](https://workflow-sdk.dev/docs) 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. ```ts // 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//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](/docs/cli/reference/configuration#keeping-a-package-out-of-the-bundle). See [Agents](/docs/sdk/agents/overview) and [Workflows](/docs/sdk/workflows/overview) 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 init` to scaffold a workspace shell (a `deep-agents/` folder, a `workflows/` folder and supporting files). Add pieces over time with `stackbone add agent `, `stackbone add workflow ` or `stackbone add workflow-agent `. A project someone handed you needs none of that: it arrives unlinked, and `dev` asks which agent it is. See [A project that arrived without a link](#a-project-that-arrived-without-a-link). - **At least one agent or workflow:** `stackbone dev` discovers your workspace by convention: any `deep-agents//index.ts` (or a `workflows/.workflow.ts`) is enough to activate the runtime. A `stackbone.config.ts` is 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. Set `STACKBONE_FRPC_BIN` to 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 init` registers a **new** agent and writes its own `package.json`, `tsconfig.json` and scaffold over the project you were handed. - `stackbone link` registers 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](/docs/cli/guides/troubleshooting#this-directory-is-not-linked-to-an-agent). ## Synopsis ```sh 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`](/docs/cli/reference/agent-yaml#dev-optional); either flag overrides that file for this session. Override the `frpc` binary location with the env var: ```sh export STACKBONE_FRPC_BIN=/opt/homebrew/bin/frpc ``` Pin 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//installations/?stackbone-dev=https://.tun.stackbone.ai │ │ │ Tunnel https://.tun.stackbone.ai │ │ Local http://127.0.0.1:4242 │ │ │ │ Agent │ │ Protocol v · 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//installations//studio/first-run?stackbone-dev=https://.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 1. A model provider resolves, from your shell or from the deployment's own store (see [Model provider](#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. 2. 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//**` | 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`](#synopsis) 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.ai` - `https://chat.stackbone.ai` - `http://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`): ```jsonc { "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): ```sh STACKBONE_CORS_ALLOW_ORIGINS="https://staging.stackbone.ai,http://localhost:*" stackbone dev ``` **Precedence**: 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`](/docs/sdk/data/storage#path-style-or-subdomain). | | `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](/docs/cli/reference/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: 1. Your shell wins. Export `MODEL_PROVIDER_API_KEY` / `MODEL_PROVIDER_BASE_URL` and the CLI seeds them as-is, with no clicks and no call to the control plane. The older `OPENROUTER_API_KEY` / `OPENROUTER_BASE_URL` names still work as a deprecated alias. 2. Otherwise, whatever you configured in Studio. The **Model provider** screen writes the value into the emulator's own Postgres, so it survives `stackbone dev` restarts. 3. Otherwise the CLI seeds nothing, and the session comes up behind the [boot gate](#the-first-run). 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`: narrows `callDeepAgent(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 workflow `name` on `stackbone.workflows.start` / `.startAndWait` / `.schedule` the same way, to the workflows this workspace declares. - `.stackbone/connect.d.ts`: types `stackbone.connection(id)` and each connector's methods from its schema. - `.stackbone/config.d.ts`: types `stackbone.config` from your `config.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/completions` and `POST /anthropic/v1/messages`: chat with any agent in the workspace, selected by the `model` field. See [Agent protocol](/docs/cli/protocol/overview). - `GET /api/discovery`: the workspace manifest, every agent and workflow it found. - `POST /api/workflows/:name/start` and `POST /api/workflows/:name/chat`: trigger a durable workflow run (one returns the `runId`, 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, and `GET /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](/docs/sdk/workflows/overview)). ## 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-` Compose project in place between runs so Postgres data persists across `stackbone dev` restarts. Wipe it with: ```sh docker compose -p stackbone-dev- down -v ``` ## Using 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](https://github.com/fatedier/frp/releases) and point the CLI at it: ```sh # macOS / Linux: download the release archive, extract, drop `frpc` on PATH export STACKBONE_FRPC_BIN=/opt/homebrew/bin/frpc stackbone dev ``` The 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//installations/`) 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](/docs/cli/guides/troubleshooting#stackbone-dev-failures).