--- title: '@stackbone/sdk integration' description: 'How the CLI wires @stackbone/sdk into a workspace of agents and workflows: env injection, the ambient client, and typed autocompletion.' position: 10 --- # @stackbone/sdk integration > Your agents and workflows reach every platform primitive through one > ambient client: `import { stackbone } from '@stackbone/sdk'`. The CLI > (`stackbone dev`) and the hosted container inject every credential as > env vars, so there is **nothing to wire**: you import `stackbone` and > call `stackbone.database`, `stackbone.ai`, `stackbone.storage` directly > from any agent tool or durable [workflow](/docs/sdk/workflows/overview) step. > > This page covers the **CLI ↔ SDK seam**: which env vars the CLI > injects, how the ambient client reads them, and the typed > autocompletion `stackbone dev` generates. For the per-surface API > (methods, error codes, the result envelope), see the individual pages > under [`@stackbone/sdk`](/docs/sdk/reference/overview). ## The ambient client The SDK exports a single process-scoped `stackbone` handle: no factory to call, no client to thread through your code. It builds one underlying client on first surface access, reading its credentials from the env vars the runtime injects: ```ts import { stackbone } from '@stackbone/sdk'; // From an agent tool, or any workflow 'use step': const greeting = await stackbone.config.get('greeting'); const rows = await stackbone.database.select().from(leads); ``` A real tool, defined inline in `deep-agents/support/index.ts`: ```ts import { tool } from '@langchain/core/tools'; import { defineDeepAgent } from '@stackbone/sdk/deep'; import { stackbone, z } from '@stackbone/sdk'; const readConfig = tool( async () => { const greeting = await stackbone.config.get('greeting'); return greeting.error ? 'No greeting configured.' : greeting.data; }, { name: 'read_config', description: "Return the agent's current configuration.", schema: z.object({}), }, ); export default defineDeepAgent({ name: 'support', model: 'anthropic/claude-haiku-4.5', tools: [readConfig], }); ``` The same handle works from a durable workflow step in `workflows/onboarding.workflow.ts`: ```ts import { stackbone, z } from '@stackbone/sdk'; export const inputSchema = z.object({ email: z.string().email() }); export const outputSchema = z.object({ welcomed: z.boolean() }); export async function onboardingWorkflow(input: z.infer) { 'use workflow'; await sendWelcome(input.email); return { welcomed: true }; } async function sendWelcome(email: string) { 'use step'; // runs once, persisted, retried on failure. Keep it idempotent const reply = await stackbone.ai.chat.completions.create({ model: 'anthropic/claude-haiku-4.5', messages: [{ role: 'user', content: `Write a one-line welcome for ${email}.` }], }); return { sent: !reply.error }; } ``` > `stackbone.config.get(...)` and the other agent reads return a `Result` > envelope, so check `.error` before using `.data`. `stackbone.database` is > native [Drizzle](https://orm.drizzle.team/) (awaiting a query returns > rows and throws on error). See the [SDK overview](/docs/sdk/reference/overview) > for the full result-envelope contract. The escape-hatch constructor `createClient(config)` still exists for tests or when you need to pass explicit config, but production code imports the ambient `stackbone` and never touches it. ## What the SDK exposes ### Public entrypoints `@stackbone/sdk` ships as a small set of subpaths. The main barrel pulls **no** agent-authoring, workflow, or connection peer dependency, so a project that only needs some of these never crash-loops on a peer it did not install: | Import | What it gives you | Peer dependency | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | `@stackbone/sdk` | The ambient `stackbone` client, `defineWorkspace`, and `z` (re-exported zod) | none | | `@stackbone/sdk/deep` | `defineDeepAgent()` to author an agent | `deepagents`, `@langchain/*` | | `@stackbone/sdk/workflow` | `callDeepAgent()` / `streamDeepAgent()` to run an agent from a step, `requestApproval()` for durable [HITL](/docs/sdk/workflows/overview), plus `defineHook` / `sleep` / `FatalError` | [`workflow`](https://workflow-sdk.dev/docs) | | `@stackbone/sdk/connect` | [Stackbone Connect](/docs/home/features/integrations) auth: `connect()`, `withConnect()`, `connectHeaders()` | none | | `@stackbone/sdk/db` | [Drizzle](https://orm.drizzle.team/) schema + query helpers | none | | `@stackbone/sdk/db/testing` | A Postgres test harness for your migrations | `@testcontainers/postgresql` | The [Workflow SDK](https://workflow-sdk.dev/docs) is an upstream package. The SDK declares it as an **optional** peer dependency: you only install it when you author durable workflows. The `@stackbone/sdk/workflow` subpath imports that peer, which is why it lives off the main barrel. The same goes for `@stackbone/sdk/deep` and its `deepagents` / `@langchain/*` peers: only a project that authors agents installs them. `@stackbone/sdk/db/testing` follows the same rule with `@testcontainers/postgresql`, so a deployed agent never installs a Docker orchestration library it only needs at test time. That subpath loads the peer on the first `createTestDatabase()` call, so importing it in a file that never calls it works without the peer installed. ### Surfaces on the ambient client Every platform primitive is a property on `stackbone`: | Accessor | Wraps | Status | | -------------------------- | ------------------------------------------------------------------------------------------ | ---------------------------------- | | `stackbone.database` | Postgres via [Drizzle](https://orm.drizzle.team/) | shipped | | `stackbone.storage` | S3-compatible object storage (MinIO in dev) | shipped | | `stackbone.ai` | An OpenAI-compatible client pointed at [OpenRouter](https://openrouter.ai/docs) | shipped | | `stackbone.rag` | Parser + chunker + embeddings + `pgvector` retrieval | shipped | | `stackbone.approval` | The HITL approvals inbox (agent-local) | shipped | | `stackbone.secrets` | Secrets encrypted with the agent's own key | shipped | | `stackbone.config` | Typed dynamic configuration | shipped | | `stackbone.settings` | [Read-only workspace knobs](/docs/sdk/platform/settings) an operator sets in the dashboard | shipped | | `stackbone.prompts` | Versioned prompt catalog with Mustache-style compile | shipped | | `stackbone.contract` | Synchronous view of the last resolved protocol handshake | shipped | | `stackbone.connection(id)` | A typed [Stackbone Connect](/docs/home/features/integrations) connector handle | shipped | | `stackbone.workflows` | Start another workflow by name, manage dynamic cron triggers | shipped | | `stackbone.memory` | Long-term memory | pending: returns `not_implemented` | > `stackbone.memory` is part of the public type surface today, but every > method returns `not_implemented`. Calling it is safe: the response signals > that the capability is not wired yet. There is no separate queue surface on the ambient client. Model background and recurring work as a [durable workflow](/docs/sdk/workflows/overview) and start it by name with `stackbone.workflows.start(...)`. See [Background jobs & workflow triggers](/docs/sdk/platform/queues). ### Calling an agent from a workflow `callDeepAgent(name, input)` from `@stackbone/sdk/workflow` runs one turn of another agent in your workspace, in-process, and resolves with its reply. Call it from inside a `'use step'` function: ```ts import { callDeepAgent } from '@stackbone/sdk/workflow'; async function askSupport(plan: string) { 'use step'; const { text } = await callDeepAgent( 'support', `A customer joined the "${plan}" plan. Give up to 3 onboarding tips.`, ); return text; } ``` `input` is a non-empty string (one user message) or a `{ messages: [{ role, content }] }` history when the agent needs more than one turn of context. See [Calling a sibling agent](/docs/sdk/agents/calling-a-sibling-agent#from-a-workflow-step-calldeepagent). `streamDeepAgent(name, input)` is the streaming twin. It runs the same in-process turn and returns the same `{ text }`, but it also forwards the reply live onto the run's chat surface, so the Studio Playground serves the workflow as a token-by-token chat instead of a one-shot run. Use `callDeepAgent` when you only want the final text (for example, a later step parses structured data out of it) and `streamDeepAgent` when a person is reading the reply as it arrives. Both take the same arguments. See [Workflow agents](/docs/sdk/workflow-agents/overview) for the full pattern. ### Calling a connector `stackbone.connection(id)` calls a [Stackbone Connect](/docs/home/features/integrations) connector. The operator installs the connector's credentials once in Studio; your code never holds a token: ```ts async function sendMail(input: { to: string; subject: string; body: string }) { 'use step'; const output = await stackbone.connection('stub-mail').sendMail({ to: input.to, subject: input.subject, body: input.body, }); return { sent: output.accepted === true, id: output.id }; } ``` ### Human-in-the-loop Pause a workflow durably until a human decides, with `requestApproval()` from `@stackbone/sdk/workflow`: ```ts import { requestApproval } from '@stackbone/sdk/workflow'; const decision = await requestApproval({ token: input.approvalToken, topic: 'refund', payload: { orderId: input.orderId, amount: input.amount }, title: 'Approve refund', timeout: '24h', fallback: 'reject', }); if (decision.status !== 'approved') { return { refunded: false, decision: decision.status }; } ``` See [Workflows](/docs/sdk/workflows/overview) for the full HITL contract and the resume path. ## The workspace registry The CLI and runtime discover your workspace **by convention**, from the files on disk, with no registry to hand-maintain. `stackbone dev` and the deployed runtime both read the same convention: - **Agents**: every folder under `deep-agents/` with an `index.ts` entry file. The folder name is both the agent's identity and the `model` a client selects to talk to it. - **Workflows**: every `workflows/.workflow.ts`. The workflow name is the file basename without the `.workflow.ts` suffix, and the exported function is `Workflow` (so `qualify-lead.workflow.ts` exports `qualifyLeadWorkflow`). A workspace with `deep-agents/support/index.ts`, `deep-agents/billing/index.ts`, and `workflows/onboarding.workflow.ts` therefore needs no registration step, and most projects need **no** config file at all. ### Optional override: `stackbone.config.ts` If a `stackbone.config.ts` exists at your project root, its `workflows` **win** over the convention scan. Use it only when you need to declare workflows that don't follow the layout above. Agents keep coming from the `deep-agents/` scan unless the same file lists `deepAgents`, in which case that list replaces the scan (see [Hide workflows and agents from Studio](/docs/sdk/reference/overview#hide-workflows-and-agents-from-studio-internal)). It default-exports `defineWorkspace`: ```ts 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', }, ], }); ``` When present, this file is what `stackbone dev`, `stackbone workflows list`, and the typed autocompletion below read instead of scanning the filesystem for workflows. The same file carries how the workspace is compiled. `build.external` names the packages your deep agents must not inline, and it applies to the local run and to the shipped image alike. See [Keeping a package out of the bundle](/docs/cli/reference/configuration#keeping-a-package-out-of-the-bundle). ## How the CLI connects to the SDK `stackbone dev` injects the env vars the ambient client reads, so a fresh workspace works zero-config locally. Each variable falls back to a typed config field if you pass one to `createClient`, but you rarely need to: the runtime provides them all: | Env var | Feeds | Provided by `stackbone dev` | | ------------------------------- | --------------------------------------- | --------------------------- | | `STACKBONE_API_URL` | Control-plane calls | yes (local tunnel) | | `STACKBONE_INSTALLATION_ID` | Identifies the install | yes | | `STACKBONE_AGENT_ID` | Workspace identity + storage key prefix | yes | | `HMAC_SECRET` | Signs Stackbone Connect broker calls | yes | | `STACKBONE_POSTGRES_URL` | `stackbone.database` + `stackbone.rag` | yes (local Postgres) | | `STACKBONE_SECRET_KEY` | Decrypts `stackbone.secrets` | yes | | `WORKFLOW_REDIS_URL` | Durable workflow execution backend | yes (local Redis) | | `STACKBONE_S3_ENDPOINT` | `stackbone.storage` | yes (local MinIO) | | `STACKBONE_S3_BUCKET` | `stackbone.storage` | yes (`stackbone-dev`) | | `STACKBONE_S3_ACCESS_KEY` | `stackbone.storage` | yes | | `STACKBONE_S3_SECRET_KEY` | `stackbone.storage` | yes | | `STACKBONE_S3_REGION` | `stackbone.storage` | yes | | `STACKBONE_S3_FORCE_PATH_STYLE` | `stackbone.storage` | yes (`true`) | | `MODEL_PROVIDER_API_KEY` | `stackbone.ai` | no, you supply it | | `MODEL_PROVIDER_BASE_URL` | `stackbone.ai` (override) | no, you supply it | The two model-provider values are the only ones you bring yourself. Export them in your shell, or save them once on the Studio **Model provider** screen, where they persist across `stackbone dev` restarts. Until one of the two resolves, the first `stackbone dev` stops at the boot gate and holds your agents back rather than failing them. See [The first run](/docs/cli/guides/local-development#the-first-run). The local dev stack is **Postgres + Redis + MinIO**: `stackbone.database` talks to local Postgres, durable workflows run on local Redis, and `stackbone.storage` lands in local MinIO. When a credential is missing, the relevant surface returns a coded `*_missing` error (for example `openrouter_key_missing`, `database_url_missing`) and `.data` is `null`. Each surface's page under [`@stackbone/sdk`](/docs/sdk/reference/overview) documents its own error taxonomy. > Stackbone Connect calls are signed with the per-install `HMAC_SECRET` > (an `x-stackbone-timestamp` plus an `x-stackbone-signature` over the > request). You never construct these headers by hand: `stackbone.connection(id)` > signs every call for you. ## Typed autocompletion `stackbone dev` watches your workspace and writes a `.stackbone/` folder of TypeScript declarations into your project. These augment the SDK's typing interfaces so the ambient client is **strictly typed** against your workspace, and a typo fails at compile time instead of at runtime: | Generated file | Makes typed | | --------------------------- | -------------------------------------------------------------------- | | `.stackbone/agents.d.ts` | `callDeepAgent(name, ...)`, only declared agent names autocomplete | | `.stackbone/workflows.d.ts` | `stackbone.workflows.start(name, ...)`, only declared workflow names | | `.stackbone/connect.d.ts` | `stackbone.connection(id)` operations from the connector catalog | | `.stackbone/config.d.ts` | `stackbone.config.get()` against your `config.schema.ts` | Add `.stackbone/` to your `.gitignore`: the CLI regenerates it on every `stackbone dev`. You can regenerate the config types on demand without the full dev loop with `stackbone config types`. ## Production: zero config A deployed **box** pre-populates the same env contract. The box is the container you run in your own cloud from the folder [`stackbone package`](/docs/cli/reference/package) writes. There is no factory to call and nothing to thread through your code. Your agent tools and workflow steps import `stackbone` and use it directly: ```ts async function execute() { // `stackbone` is configured from the injected env vars. return await stackbone.config.getAll(); } ``` The difference from local dev is who supplies the env. The box injects the same `STACKBONE_*` contract from the Postgres, Redis and MinIO its compose file bundles, and it fills `MODEL_PROVIDER_API_KEY` from the provider the operator saved on the Studio **Model provider** screen. The box runs the same runtime as `stackbone dev`, so durable workflows, agents and the data-plane surfaces behave identically. See [Connect your box](/docs/home/deployments/connect-your-box) to register the box with your agent. ## Local development: bring your model provider `stackbone dev` doesn't manage LLM credits: they come out of your own account. Export the credential in your shell (or use a `.env.local` your workspace loads) and it feeds `stackbone.ai` and every agent's model calls: ```sh export MODEL_PROVIDER_API_KEY=sk-or-... stackbone dev ``` Any OpenAI-compatible endpoint works: point `MODEL_PROVIDER_BASE_URL` at a local gateway (Ollama, LM Studio, LiteLLM, vLLM) and the key becomes optional. If you'd rather not touch your shell, configure it once in the Studio **Model provider** screen and it persists across `stackbone dev` restarts. Full precedence: [Local development → model provider](/docs/cli/guides/local-development#model-provider). ## Escape hatch The SDK is a façade over the upstream clients. The runtime also injects the **upstream env vars**, so you can drop down to the underlying client whenever the wrapper doesn't expose what you need: ```ts // instead of stackbone.database import { drizzle } from 'drizzle-orm/postgres-js'; import postgres from 'postgres'; const sql = postgres(process.env['STACKBONE_POSTGRES_URL']!); const db = drizzle(sql); const rows = await db.execute('SELECT * FROM leads'); ``` The same applies upward to the durable-execution primitives: when `requestApproval()` is too high-level, use `defineHook` and `sleep` from `@stackbone/sdk/workflow` to build a custom gate. Stackbone wrappers **coexist** with the upstream SDKs, so you can drop to the upstream client for one call and use the wrapper for the next. ## Advanced: contract knobs The SDK negotiates a capability contract with the control plane on the first gated call. Three env vars tune that behaviour; you rarely set them: | Env var | Default | Meaning | | ---------------------------- | --------------- | --------------------------------------------------------------------- | | `STACKBONE_REQUIRE_CONTRACT` | `1` (gating on) | Set to `0` to downgrade capability/version errors to warnings. | | `STACKBONE_CONTRACT_TTL_MS` | unset | Re-fetch the handshake after this many milliseconds. Default: no TTL. | | `STACKBONE_DEBUG` | unset | Set to `1` to log a one-line handshake-resolved message per base URL. | ## Where to go next - **[`@stackbone/sdk` overview](/docs/sdk/reference/overview)**: the per-surface API reference with code samples for every accessor. - **[Agents](/docs/sdk/agents/overview)**: what an agent is and how it runs. - **[Workflows](/docs/sdk/workflows/overview)**: authoring durable workflows, `'use workflow'` / `'use step'`, and HITL with `requestApproval()`. - **[Stackbone Connect](/docs/home/features/integrations)**: connectors, `stackbone.connection(id)`, and the operator-installs-credentials model. - **[Local development](/docs/cli/guides/local-development)**: how `stackbone dev` injects env vars and what to bring yourself.