Local development

stackbone dev runs your agents and durable workflows 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-<agent-slug> 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 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/<name>.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 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 init to scaffold a workspace shell (a deep-agents/ folder, a workflows/ folder and supporting files). Add pieces over time with stackbone add agent <name>, stackbone add workflow <name> or stackbone add workflow-agent <name>. 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.
  • At least one agent or workflow: stackbone dev discovers your workspace by convention: any deep-agents/<name>/index.ts (or a workflows/<name>.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 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.

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/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/<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

  1. 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.
  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/<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.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):

{
  "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 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.
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:

  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 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.
  • 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).

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 -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 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 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/<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.

BUILT WITH ❤️ FROM CANADA AND SPAIN