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

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 a Result envelope, check .error before using .data. stackbone.database is 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 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/<name>.workflow.ts. The workflow name is the file basename without the .workflow.ts suffix, and the exported function is <camelCase(name)>Workflow (so qualify-lead.workflow.ts exports qualifyLeadWorkflow).

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 (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 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 by stackbone dev today; the hosted runtime is still rolling out, so a cloud installation may answer 404 on the workflow/runs routes until that port lands. Develop against stackbone dev in 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 dev

Escape 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/sdk overview: 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 with requestApproval().
  • Stackbone Connect: connectors, stackbone.connection(id), and the operator-installs-credentials model.
  • Local development: how stackbone dev injects env vars and what to bring yourself.

The platform for agent developers.Build, ship and observe agentsthat survive prod.

© 2026 · STACKBONEBUILT WITH ❤️ FROM CANADA AND SPAIN