@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 importstackboneand callstackbone.database,stackbone.ai,stackbone.storagedirectly from any agent tool or durable workflow 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 devgenerates. For the per-surface API (methods, error codes, the result envelope), see the individual pages under@stackbone/sdk.
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:
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:
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:
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<typeof inputSchema>) {
'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 aResultenvelope, so check.errorbefore using.data.stackbone.databaseis native Drizzle (awaiting a query returns rows and throws on error). See the SDK 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, plus defineHook / sleep / FatalError |
workflow |
@stackbone/sdk/connect |
Stackbone Connect auth: connect(), withConnect(), connectHeaders() |
none |
@stackbone/sdk/db |
Drizzle schema + query helpers | none |
@stackbone/sdk/db/testing |
A Postgres test harness for your migrations | @testcontainers/postgresql |
The Workflow SDK 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 | shipped |
stackbone.storage |
S3-compatible object storage (MinIO in dev) | shipped |
stackbone.ai |
An OpenAI-compatible client pointed at OpenRouter | 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 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 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.memoryis part of the public type surface today, but every method returnsnot_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 and
start it by name with stackbone.workflows.start(...). See
Background jobs & workflow triggers.
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:
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.
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 for the full pattern.
Calling a connector
stackbone.connection(id) calls a Stackbone Connect
connector. The operator installs the connector's credentials once in
Studio; your code never holds a token:
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:
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 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 anindex.tsentry file. The folder name is both the agent's identity and themodela client selects to talk to it. - Workflows: every
workflows/<name>.workflow.ts. The workflow name is the file basename without the.workflow.tssuffix, and the exported function is<camelCase(name)>Workflow(soqualify-lead.workflow.tsexportsqualifyLeadWorkflow).
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).
It default-exports defineWorkspace:
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.
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.
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 documents its
own error taxonomy.
Stackbone Connect calls are signed with the per-install
HMAC_SECRET(anx-stackbone-timestampplus anx-stackbone-signatureover 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 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:
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 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:
export MODEL_PROVIDER_API_KEY=sk-or-...
stackbone devAny 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.
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:
// 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/sdkoverview: the per-surface API reference with code samples for every accessor.- Agents: what an agent is and how it runs.
- Workflows: authoring durable workflows,
'use workflow'/'use step', and HITL withrequestApproval(). - Stackbone Connect: connectors,
stackbone.connection(id), and the operator-installs-credentials model. - Local development: how
stackbone devinjects env vars and what to bring yourself.