--- title: '@stackbone/sdk overview' description: 'Official TypeScript SDK for Stackbone: every published agent and workflow depends on it.' position: 1 --- # @stackbone/sdk overview > The TypeScript SDK behind every Stackbone agent and workflow. One > **ambient client** (`stackbone`) for the agent's data plane, plus two > focused subpaths: `@stackbone/sdk/workflow` for durable orchestration and > human-in-the-loop, and `@stackbone/sdk/connect` for third-party > connectors. Every read returns a uniform `Result` envelope, and a > capability handshake fails fast when the agent and the runtime drift apart. ## Install ```sh pnpm add @stackbone/sdk ``` The package is a thin convenience layer over the clients it carries as its own dependencies: Postgres + [Drizzle](https://orm.drizzle.team/), an S3-compatible client, and an OpenAI-compatible client pointed at [OpenRouter](https://openrouter.ai/docs). You do not add any of those to your `package.json`, and `@stackbone/sdk/db` re-exports Drizzle's query helpers so you import them from one place. You do install the optional peers the authoring subpaths need: see [Peer isolation](#peer-isolation). The product is durable agents and durable workflows. An [agent](/docs/sdk/agents/overview) holds open-ended, multi-turn conversations; a [workflow](/docs/sdk/workflows/overview) runs a fixed, crash-proof pipeline. Both reach the same data plane through the ambient `stackbone` client below. ## The ambient `stackbone` client Inside a deep-agent tool or a workflow step you reach the data plane through the **ambient** handle: `stackbone`, imported from the barrel. It is the same process-scoped client the runtime would build for you, resolved from the environment on first use, so there is no `createClient()` and no credential wiring: ```ts import { tool } from '@langchain/core/tools'; import { defineDeepAgent } from '@stackbone/sdk/deep'; import { stackbone, z } from '@stackbone/sdk'; const readTone = tool( async () => { const tone = await stackbone.config.get('tone'); return tone.error ? 'neutral' : tone.data; }, { name: 'read_tone', description: "Return the agent's current tone setting.", schema: z.object({}), }, ); export default defineDeepAgent({ name: 'support', model: 'anthropic/claude-haiku-4.5', tools: [readTone], }); ``` The same handle works verbatim from a workflow `'use step'`: ```ts import { stackbone } from '@stackbone/sdk'; import { leads } from './schema'; async function countOpenLeads() { 'use step'; // Drizzle handle, S3 client, OpenRouter pool, etc. are built only when // you touch the corresponding module. const rows = await stackbone.database.select().from(leads); return { count: rows.length }; } ``` The SDK builds each member on first access and caches it for the life of the process: one client, one connection pool. Importing `stackbone` builds no surface and reads no env var, so the import stays cheap and free of side effects. ### Reaching each member Every capability reads the same way, `stackbone.`: | Member | What it reaches | | -------------------------- | -------------------------------------------------------------------------------------------- | | `stackbone.config` | Typed reads of dynamic per-agent config set in the dashboard. | | `stackbone.secrets` | Agent-encrypted secrets registered in the dashboard. | | `stackbone.database` | Drizzle handle bound to the agent's Postgres. | | `stackbone.storage` | S3-compatible object storage with per-agent key prefixing. | | `stackbone.ai` | OpenAI-compatible chat, embeddings, image generation, model catalogue. | | `stackbone.rag` | Parse → chunk → embed → store → retrieve on top of `stackbone.database`. | | `stackbone.prompts` | Versioned prompt catalog: `use(key, vars)` returns the text; `get` / `compile` / `list`. | | `stackbone.settings` | [Read-only workspace knobs](/docs/sdk/platform/settings) an operator picks in the dashboard. | | `stackbone.approval` | The HITL inbox + LLM-tool wrapper (the in-agent surface, distinct from workflow gates). | | `stackbone.workflows` | Start another workflow by name, and manage its cron triggers. | | `stackbone.connection(id)` | A [Stackbone Connect connector](/docs/home/features/integrations) by its verbatim id. | `stackbone.connection(id)` is a plain signed fetch and pulls no extra dependency. To call a sibling agent, use `callDeepAgent(name, input)` from `@stackbone/sdk/workflow` instead: it runs one turn of the named agent in-process from inside a workflow step and resolves with `{ text }`. ```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, or a `{ messages: [{ role, content }] }` history when the agent needs more than one turn of context. See [Calling an agent from a workflow](/docs/sdk/reference/cli-integration#calling-an-agent-from-a-workflow) for the full pattern, and [`defineDeepAgent`](/docs/sdk/agents/overview) if you instead want one agent to delegate to a `subagents` entry within its own graph. > **Migrating from an older SDK:** the bare top-level `connection(id)` export on > `@stackbone/sdk/connect` is now a **deprecated** alias for > `stackbone.connection(id)`, and the copy that used to ship from > `@stackbone/sdk/workflow` is gone. The alias still works unchanged, but new > code should use the namespaced form so every capability reads as > `stackbone.`. ### Knowing which run you are in `getInvocationContext()` returns the invocation your code is running inside, or `undefined` when there is none. Use it to correlate your own telemetry with the run the dashboard shows: ```ts import { getInvocationContext } from '@stackbone/sdk'; async function chargeCustomer() { 'use step'; const ctx = getInvocationContext(); console.log('run', ctx?.runId, 'step', ctx?.activeStep?.stepName); } ``` `activeStep` names the workflow step whose body is running: `{ stepId, stepName?, attempt }`. It is `undefined` for a direct invoke and for an agent running outside a workflow. The field resolves on every read, so read it where you need it instead of copying the context object. ### Escape hatch: `createClient(config?)` `createClient(config?)` returns a `StackboneClient` and is the same factory the runtime uses internally. Use it only when you need a client **outside** a tool or step (a one-off script, a test) or when you want to **override** a piece of config the runtime injected. The client builds each member on first access, so `createClient()` itself is cheap and side-effect-free. ```ts import { createClient } from '@stackbone/sdk'; const sb = createClient({ modelProviderKey: process.env['MY_OWN_KEY'] }); const rows = await sb.database.select().from(leads); ``` `config` is optional. Each field falls back to a documented env var the runtime injects when omitted; the full shape lives in `ClientConfig`. The runtime sets `STACKBONE_POSTGRES_URL`, `MODEL_PROVIDER_API_KEY`, `MODEL_PROVIDER_BASE_URL`, `STACKBONE_INSTALLATION_ID`, `STACKBONE_AGENT_ID`, and the storage credentials for you, so in normal agent code you never construct config by hand. ## Authoring workflows: `@stackbone/sdk/workflow` A [durable workflow](/docs/sdk/workflows/overview) is a plain async function marked `'use workflow'` that orchestrates idempotent `'use step'` units. It runs on the upstream [Workflow SDK](https://workflow-sdk.dev/docs) (the same engine behind [Vercel Workflows](https://vercel.com/docs/workflows)), so a run survives crashes and redeploys and can pause for months. You don't register workflows by hand. `stackbone dev` and the deployed runtime **discover the workspace by convention**: they scan your project and pick up every agent folder under `deep-agents/` that has an `index.ts`, and every workflow file at `workflows/.workflow.ts`. A workflow's name is the file basename without the `.workflow.ts` suffix, and its exported function is the camel-cased name plus `Workflow` (so `workflows/refund.workflow.ts` exports `refundWorkflow`). Scaffold one with `stackbone add workflow refund`. For the rare case where you need to override the workflow scan (point at a different file or rename an export), drop an optional `stackbone.config.ts` that default-exports `defineWorkspace(...)`. When present its `workflows` list wins over the convention scan; when absent (the common case) you need no config file at all. Deep agents are still discovered from `deep-agents/` unless you list them explicitly (see `$internal` below): ```ts import { defineWorkspace } from '@stackbone/sdk'; export default defineWorkspace({ agents: [], // required, and empty in a deep-agent workspace workflows: [{ name: 'refund', module: 'workflows/refund.workflow.ts', export: 'refundWorkflow' }], }); ``` `agents` and `workflows` are both required fields, so keep `agents: []` even when you only override workflows. #### Hide workflows and agents from Studio: `$internal` Some workflows and agents are infrastructure that should not appear in the Studio/Playground UI. Mark them with `$internal: true` to exclude them from the discovery manifest: ```ts import { defineWorkspace } from '@stackbone/sdk'; export default defineWorkspace({ agents: [], workflows: [ { name: 'data-sync', module: 'workflows/data-sync.workflow.ts', export: 'dataSyncWorkflow', $internal: true, // ← hidden from Studio/Playground }, { name: 'refund', module: 'workflows/refund.workflow.ts', export: 'refundWorkflow' }, ], // Listing `deepAgents` replaces the `deep-agents/` scan: name every agent // you want the runtime to see, not only the hidden one. deepAgents: [ { name: 'maintenance-bot', dir: 'deep-agents/maintenance-bot', $internal: true, // ← hidden from Studio/Playground }, ], }); ``` The runtime marks system workflows and agents internal for you: `rag-ingest` stays hidden without any configuration. Override this with `$internal: false` if you need it visible. Start another workflow by name, and manage cron schedules, through the ambient `stackbone.workflows` surface: `stackbone.workflows.start(name, input)` / `.startAndWait(name, input)` for triggers, and `.schedule` / `.unschedule` / `.listSchedules` for cron. See [Triggering a workflow](/docs/sdk/workflows/building-workflows#3-triggering-a-run) and [Background jobs & workflow triggers](/docs/sdk/platform/queues). The `@stackbone/sdk/workflow` subpath carries the pause helpers that need the `workflow` peer: `requestApproval()` below, plus the raw `defineHook` / `sleep` escape hatch. > **Migrating from an older SDK:** earlier versions exposed the trigger and > schedule helpers as loose top-level imports from `@stackbone/sdk/workflow` > (`startWorkflow`, `startWorkflowAndWait`, `scheduleWorkflow`, `unschedule`, > `listSchedules`). Those exports are gone. Use the namespaced > `stackbone.workflows.*` form above instead. ### `requestApproval()`: human-in-the-loop `requestApproval()` is the default HITL gate. The workflow body calls it to **pause durably** on a hook; the call records an inbox row and races the human decision against a timeout, applying a `fallback` if nobody decides. Import it from `@stackbone/sdk/workflow`, **not** the main barrel: ```ts import { z } from '@stackbone/sdk'; import { requestApproval } from '@stackbone/sdk/workflow'; export async function refundWorkflow(input: { orderId: string; amount: number }) { 'use workflow'; const decision = await requestApproval({ token: `refund-${input.orderId}`, 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 }; } await performRefund(input.orderId, input.amount); return { refunded: true, decision: decision.status }; } async function performRefund(orderId: string, amount: number) { 'use step'; // runs once, persisted, retried on failure. Keep it idempotent // … } ``` The decision resolves to `{ status: 'approved' | 'rejected', payload?, timedOut }`; gate the side-effect on `status === 'approved'`. **Hard rule:** `requestApproval` (and the raw `defineHook` below) must run in the **workflow body**, never inside a `'use step'`: `.create()` is a workflow primitive that suspends the run, so the I/O lives in steps and the gate lives in the body. For advanced cases (a custom hook schema, several gates, escalation with a timer) the subpath also re-exports `defineHook` and `sleep` verbatim from the upstream `workflow` package as an escape hatch. It re-exports `FatalError` from the same package too: throw it from a `'use step'` to fail the run immediately on a permanent condition (a missing secret, an invalid input) instead of burning the step's retry budget. ### Calling a connector from a step The subpath also exports `callConnector()` so a step can run one connector operation with no agent in the loop. Prefer the namespaced `stackbone.connection(id)` form (below) for the same job. ## Connectors: `@stackbone/sdk/connect` [Stackbone Connect](/docs/home/features/integrations) is the current connector model. The operator installs a connection's credentials once in Studio; a broker mints install-scoped, short-lived tokens, so the agent code never sees a provider secret. Call a connector by id: ```ts async function notify(input: { to: string; subject: string; body: string }) { 'use step'; // Typed from the connector's schema, generated into .stackbone/connect.d.ts // by `stackbone dev`. `.call('operation', args)` is the dynamic escape hatch. 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 }; } ``` The `@stackbone/sdk/connect` subpath carries the lower-level building blocks for wiring broker auth into a connection definition yourself instead of calling through `stackbone.connection(id)`: `connect()`, `withConnect()`, `connectHeaders()`, `callConnector()`, and the `ConnectionAuthorizationRequiredError` / `ConnectionAuthorizationFailedError` classes (match them by `err.name`, never `instanceof`). See [Connections](/docs/sdk/platform/connections) for the full picture. ## Peer isolation The main `@stackbone/sdk` barrel pulls **no** upstream peer at import time. The SDK declares the two families of upstream packages it can use, `deepagents` / `@langchain/*` (agent authoring) and `workflow` (durable execution), as **optional** peerDependencies, and anything that needs them lives behind a subpath: - `@stackbone/sdk/deep` statically imports `deepagents`, and lazily imports `@langchain/openai` / `@langchain/core` only when it needs them. - `@stackbone/sdk/workflow` statically imports `workflow`. A tool-only workflow project that imports only `@stackbone/sdk` therefore never eager-loads, or crash-loops on, a peer it never installed. `@stackbone/sdk/connect` needs no external peer at all: the SDK owns the connection-auth vocabulary itself. ## The `Result` envelope Every method on every ambient member returns a `Result`: ```ts import type { Result, SdkError } from '@stackbone/sdk'; type Result = { data: T; error: null } | { data: null; error: SdkError }; ``` `error.code` is constrained to the typed `SdkErrorCode` union: every literal the SDK can emit lives in a single catalog, so a `switch` over `result.error.code` gets exhaustiveness checking at compile time: ```ts import { type SdkErrorCode, isSdkErrorCode } from '@stackbone/sdk'; function describe(code: SdkErrorCode): string { switch (code) { case 'secrets_not_found': return 'Set the secret in the dashboard.'; case 'config_not_found': return 'Set the config value in the dashboard.'; case 'contract_unreachable': return 'The runtime did not answer the contract handshake.'; default: return 'Unexpected error.'; } } // `isSdkErrorCode` narrows an arbitrary string (e.g. a code that arrived over // the wire) to `SdkErrorCode` for safe re-emission. if (isSdkErrorCode(rawCode)) { describe(rawCode); } ``` Narrowing on `result.error` refines `result.data` to the success payload. The SDK does not throw for expected failure modes: auth, validation, missing config, contract drift and partner errors all surface through `error.code`. The one deliberate exception is `stackbone.database`: its query-builder verbs return Drizzle's native chainable types (typed rows, not envelopes), and a contract-gate failure throws a tagged `Error` instead of swallowing the signature. ## The contract handshake The runtime exposes a contract describing the **protocol version** it speaks and the set of **capabilities** it advertises: ```json { "version": 15, "minSupported": 1, "capabilities": [ "database.postgres_direct", "rag.basic", "queues.jobs", "secrets.read_write", "config.read_write", "approval.fire_and_forget", "storage.s3", "ai.openrouter", "prompts.basic", "browser.provider" ], "build": { "name": "stackbone-cli", "version": "0.3.3" } } ``` That list is what a workspace runtime (`stackbone dev` and the deployed image alike) advertises today. Read it rather than assuming it. A runtime lists only a capability whose route it serves, so the set tells you what you can call. `build` names the runtime that answered and the version it was built from. The SDK fires this handshake **on the first gated member call**, caches it for the process lifetime, and reuses it for every subsequent call. You can inspect the last resolved contract synchronously through `stackbone.contract` (`null` until at least one gated call has resolved; it never fetches and never throws). ### Capability gating Each gated member declares the single capability its surface depends on (`stackbone.database` → `database.postgres_direct`, `stackbone.rag` → `rag.basic`, `stackbone.storage` → `storage.s3`, and so on). Before forwarding a call, the member awaits the handshake and checks that the runtime advertises the capability. If it does not, the call short-circuits with a stable error code: - `contract_version_unsupported`: the negotiated version is below the SDK's hard floor or your declared floor, whichever is higher. - `capability_unavailable`: the version check passes but the runtime does not advertise the capability the member needs. When the handshake itself cannot complete (network error, 404, malformed body) the call surfaces `contract_unreachable` or `contract_malformed` instead: these are **always hard errors** because the SDK cannot tell what it is talking to. ### Escape hatch: `STACKBONE_REQUIRE_CONTRACT=0` For migrations and local debugging, set `STACKBONE_REQUIRE_CONTRACT=0` to suppress the gate. The handshake still runs and still populates `client.contract`, but the SDK downgrades capability/version errors to a one-shot stderr warning and lets the call through. **Reachability errors are never suppressed.** Production agents should leave this flag unset. | Env var | Default | Meaning | | ---------------------------- | --------------- | ----------------------------------------------------------------------- | | `STACKBONE_REQUIRE_CONTRACT` | `1` (gating on) | Set to `0` to suppress capability/version errors (warning instead). | | `STACKBONE_CONTRACT_TTL_MS` | unset (process) | Re-fetch the handshake after this many milliseconds. Default is no TTL. | | `STACKBONE_DEBUG` | unset | Set to `1` to log a one-line handshake-resolved message. | ## Where to go next - **[Agents](/docs/sdk/agents/overview)**: the deep-agent authoring model, and `callDeepAgent()` for reaching a sibling agent from a workflow step. - **[Durable workflows](/docs/sdk/workflows/overview)**: `'use workflow'` / `'use step'`, `requestApproval`, `defineWorkspace`, and triggering runs. - **[Stackbone Connect](/docs/home/features/integrations)**: the broker model and `stackbone.connection(id)`. - **The data-plane surfaces**: [`stackbone.database`](/docs/sdk/data/database), [`stackbone.rag`](/docs/sdk/data/rag), [`stackbone.storage`](/docs/sdk/data/storage), [`stackbone.ai`](/docs/sdk/platform/ai), [`stackbone.config`](/docs/sdk/platform/config), [`stackbone.secrets`](/docs/sdk/platform/secrets), [`stackbone.settings`](/docs/sdk/platform/settings), [`stackbone.prompts`](/docs/sdk/platform/prompts), [`stackbone.approval`](/docs/sdk/humans/approval), [connectors](/docs/sdk/platform/connections). - **[Background jobs & workflow triggers](/docs/sdk/platform/queues)**: model background and recurring work as durable workflow runs started by name. - **[Guardrails](/docs/sdk/humans/guardrails)**: operator-configured rules the runtime enforces at the chat and workflow borders. No SDK surface: you write no code for them, but they can block, mask or hold what reaches your agent. - **[Logging & observability](/docs/sdk/platform/observability)**: the runtime captures the run timeline for you. - **[CLI reference](/docs/cli/reference/commands)**: `stackbone dev`, `stackbone link`, `stackbone workflows`, `stackbone runs`, `stackbone hitl`.