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

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 a Result envelope, so 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 / 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.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 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 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).

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

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/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.
BUILT WITH ❤️ FROM CANADA AND SPAIN