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
There is no factory to call and no client to thread through your code.
The SDK exports a single process-scoped stackbone handle. It builds one
underlying client lazily 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({
model: 'anthropic/claude-haiku-4.5',
systemPrompt: 'You help customers with their account settings.',
tools: [readConfig],
});The same handle works from a durable workflow step in
workflows/onboarding.workflow.ts:
import { 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, 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 |
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 | none |
The Workflow SDK is an upstream package.
It is declared as an optional peer dependency: you only install it when
you author durable workflows. The @stackbone/sdk/workflow subpath imports
that peer, which is exactly why it lives off the main barrel, same for
@stackbone/sdk/deep and its deepagents / @langchain/* peers, only a
project that authors agents installs them.
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 / R2 / MinIO object storage | ✅ 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 |
Organization-encrypted secrets | ✅ shipped |
stackbone.config |
Typed dynamic configuration | ✅ 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.queues |
Background-job publisher (superseded by workflow triggers) | 🚧 pending — returns not_implemented |
stackbone.memory |
Long-term memory | 🚧 pending — returns not_implemented |
The pending surfaces are part of the public type surface today, but every method returns
not_implemented, calling them is safe, the response just signals the capability is not wired yet. There is no separate queue system: model background and recurring work as a durable workflow and start it by name.
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', {
message: `A customer joined the "${plan}" plan. Give up to 3 onboarding tips.`,
});
return text;
}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. Reach for
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
stackbone publish 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).
So a workspace with deep-agents/support/index.ts,
deep-agents/billing/index.ts, and workflows/onboarding.workflow.ts is
already fully registered, 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 are always
discovered from deep-agents/; the config format does not declare them).
It default-exports defineWorkspace:
import { defineWorkspace } from '@stackbone/sdk';
export default defineWorkspace({
workflows: [
{
name: 'onboarding',
module: 'workflows/onboarding.workflow.ts',
export: 'onboardingWorkflow',
},
],
});When present, this file is what stackbone dev, stackbone publish,
stackbone workflows list, and the typed autocompletion below read
instead of scanning the filesystem for workflows.
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 | ✅ (local tunnel) |
STACKBONE_INSTALLATION_ID |
Identifies the install | ✅ |
STACKBONE_AGENT_ID |
Workspace identity + storage key prefix | ✅ |
HMAC_SECRET |
Signs Stackbone Connect broker calls | ✅ |
STACKBONE_POSTGRES_URL |
stackbone.database + stackbone.rag |
✅ (local Postgres) |
STACKBONE_SECRET_KEY |
Decrypts stackbone.secrets |
✅ |
WORKFLOW_REDIS_URL |
Durable workflow execution backend | ✅ (local Redis) |
STACKBONE_S3_ENDPOINT |
stackbone.storage |
✅ (local MinIO) |
STACKBONE_S3_BUCKET |
stackbone.storage |
✅ (stackbone-dev) |
STACKBONE_S3_ACCESS_KEY |
stackbone.storage |
✅ |
STACKBONE_S3_SECRET_KEY |
stackbone.storage |
✅ |
STACKBONE_S3_REGION |
stackbone.storage |
✅ |
OPENROUTER_API_KEY |
stackbone.ai |
❗ bring your own |
OPENROUTER_BASE_URL |
stackbone.ai (override) |
optional |
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,
the error taxonomy is documented on each surface's page under
@stackbone/sdk.
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
actual workspace, a typo becomes a compile error, not a runtime
surprise:
| Generated file | Makes typed |
|---|---|
.stackbone/agents.d.ts |
callDeepAgent(name, ...), only declared agent names autocomplete |
.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, it regenerates on every
stackbone dev. You can regenerate the config types on demand without the
full dev loop with stackbone config types.
Production: zero config
Inside a Stackbone-hosted container the platform pre-populates the same
env contract. 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 fully configured from the injected env vars.
return await stackbone.config.getAll();
}The difference from local dev is who supplies the env: the hosted
runtime fills in OPENROUTER_API_KEY from the org's managed key and
points WORKFLOW_REDIS_URL / STACKBONE_POSTGRES_URL at the per-install
Redis and Postgres.
The durable workflow runtime (
'use step'workflows on/api/workflows/*) is exercised end-to-end bystackbone devtoday; the hosted runtime is still rolling out, so a cloud installation may answer404on the workflow/runs routes until that port lands. Develop againststackbone devin the meantime.
Local development: bring your OpenRouter key
stackbone dev doesn't manage LLM credits, that's your account, not the
platform's. Export the key in your shell (or use a .env.local your
workspace loads) and it feeds stackbone.ai and every agent's model calls:
export OPENROUTER_API_KEY=sk-or-...
stackbone devEscape hatch
The SDK is a façade, not a moat. 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, reach for defineHook and sleep
from @stackbone/sdk/workflow to build a custom gate. Stackbone wrappers
coexist with the upstream SDKs, reach for the upstream client when
the wrapper doesn't fit, come back when it does.
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.