Workflow · last verified 2026-07-13
The Agent SDK delivery workflow: build an agent, ship it to production
This is the practice of delivering a Claude Agent SDK agent end-to-end — from the first query() to a permissioned, evaluated, cost-tracked agent running in production. It is part of the Engineering Workflow Library: one prescribed way of working, with real commands, the artifacts each step produces, and a dated changelog.
This page is the workflow. For what the SDK is — its architecture, message types, and how it relates to Claude Code — see the Claude Agent SDK guide; nothing there is duplicated here.
The job this workflow does
The Agent SDK makes it trivially easy to get an agent working and deceptively easy to ship one that isn't delivered: no pinned model, default permissions, no eval gate, cost as a surprise, and a hosting shape chosen by accident. The gap between a demo and a production agent is not more features — it is the delivery discipline around the same fifty lines of code.
This workflow closes that gap in eight steps, in order. Each step produces an artifact you keep: a pinned dependency, a tool definition, a permission policy, an eval suite, a cost log, a container spec. By the end you have an agent whose behavior is gated by evals, whose blast radius is bounded by policy, and whose spend you can explain — which is the actual definition of "shipped."
It prescribes one good way. The SDK offers many knobs; most of them have a correct production setting, and this page states it rather than surveying the options.
The workflow
1. Start from the delivery question, not the SDK
Before any code: write down what "done" looks like. One job the agent does (not three), the transcript of a successful run, and the criteria you would use to call a run a failure. Those criteria become your eval suite in step 5 — if you can't state them now, you are building a demo, not a product.
Also decide now whether this agent needs to exist as code you host. Anthropic's own documented path is to prototype with the SDK locally, then consider Managed Agents for production — but Managed Agents is beta, not GA, and not ZDR/HIPAA eligible, so for most production workloads today the self-hosted SDK path below is the one you ship.
2. Scaffold the project and make the first query()
TypeScript is the primary path (Python mirrors it — ClaudeSDKClient, @tool, snake_case options). Node 18+ required. The SDK bundles the Claude Code CLI binary pinned to the SDK version, so installing the SDK is installing the runtime — there is no separate CLI to manage.
mkdir invoice-agent && cd invoice-agent
npm init -y && npm pkg set type=module
npm install @anthropic-ai/claude-agent-sdk # 0.3.207 at time of writing
export ANTHROPIC_API_KEY=sk-ant-...
The first call already carries the production defaults. Four options are non-negotiable from day one: pin the model explicitly (the SDK default is whatever the bundled CLI defaults to — undocumented, and it changes under you on SDK updates); set allowedTools to the minimal set (the default [] pre-approves nothing); pass settingSources: [] (the default loads user, project, and local Claude Code settings from the host filesystem — exactly what you don't want on a server); and decide the systemPrompt posture (the default is minimal, not the Claude Code prompt — opt in with { type: 'preset', preset: 'claude_code' } only if you want a coding agent's behavior).
// agent.mjs — the smallest production-shaped query import { query } from '@anthropic-ai/claude-agent-sdk'; for await (const msg of query({ prompt: 'Summarize the failed invoices in ./data', options: { model: 'claude-opus-4-8', // pinned snapshot, never the default fallbackModel: 'claude-sonnet-5', allowedTools: ['Read', 'Glob', 'Grep'], settingSources: [], // never load host settings in prod maxTurns: 12, // there is no top-level session timeout cwd: '/srv/agent/workspace', }, })) { if (msg.type === 'system' && msg.subtype === 'init') saveSessionId(msg.session_id); if (msg.type === 'result') console.log(msg.result); }
Capture session_id from the init message — it is how you resume (options.resume) or fork (forkSession) a conversation later. On model choice: claude-opus-4-8 ($5/$25 per MTok, 1M context) is Anthropic's recommended default for complex agentic work; claude-sonnet-5 ($3/$15, intro $2/$10 through 2026-08-31) for cost-sensitive volume; claude-haiku-4-5 ($1/$5) for classification-grade subtasks.
3. Model the agent's capabilities as tools
Everything the agent can do that isn't a built-in (Read, Write, Edit, Bash, Glob, Grep, WebSearch, WebFetch, and friends) becomes a custom tool. Define them with tool() (name, description, Zod schema, handler) and mount them with createSdkMcpServer() — an in-process MCP server, no subprocess, no network hop. Tool names arrive as mcp__<server>__<tool>, which is what you list in allowedTools.
import { tool, createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
import { z } from 'zod';
const lookupInvoice = tool(
'lookup_invoice',
'Fetch one invoice by id. Returns status, amount, and last error only.',
{ invoiceId: z.string() },
async (args) => ({
content: [{ type: 'text', text: JSON.stringify(await db.invoiceSummary(args.invoiceId)) }],
}),
);
const billing = createSdkMcpServer({ name: 'billing', tools: [lookupInvoice] });
// options: mcpServers: { billing }, allowedTools: ['mcp__billing__lookup_invoice', ...]
Keep tool results compact by design — return the summary the model needs, not the row the database has. Every token a tool returns is context the model re-reads on every subsequent turn; verbose tools are the fastest way to blow both the budget and the quality.
4. Set the permission and sandbox posture
Permissions layer in a fixed order, and you should use the layers in that order: allowedTools (what exists at all) → permissionMode ('default' for prod; 'plan', 'acceptEdits', 'auto', 'dontAsk', and 'bypassPermissions' exist, and the last one never belongs on a server) → a canUseTool callback for per-call decisions → PreToolUse hooks to block or transform specific calls. Bash commands are AST-parsed against your rules, not string-matched — but treat that as a gate, not a guarantee.
// deny-by-default per-call policy + a hook that blocks writes outside the workspace
options: {
canUseTool: async (toolName, input) =>
POLICY.allows(toolName, input)
? { behavior: 'allow', updatedInput: input }
: { behavior: 'deny', message: 'outside policy' },
hooks: {
PreToolUse: [{ hooks: [async (input) =>
input.tool_name === 'Write' && !input.tool_input.file_path.startsWith('/srv/agent/workspace')
? { decision: 'block', reason: 'writes confined to workspace' }
: {},
] }],
},
}
The permission gate is not a sandbox — a permitted Bash call still runs on your host. Pick the sandbox tier by risk: @anthropic-ai/sandbox-runtime (bubblewrap/sandbox-exec plus an egress proxy allowlist) for filesystem and network confinement; a hardened container (--cap-drop ALL --network none) as the baseline for anything internet-facing; gVisor or Firecracker when tenant isolation is the product. And never hand the agent third-party credentials — put a proxy in front of external APIs so the agent holds a scoped token to your proxy, not the vendor key. Multi-tenant deployments add: settingSources: [] (already set), CLAUDE_CODE_DISABLE_AUTO_MEMORY=1, and a per-tenant CLAUDE_CONFIG_DIR, cwd, and egress policy.
5. Build the eval harness before the features
The Agent SDK docs have no dedicated first-party evals page as of this verification date — testing guidance lives in the cookbook and demos. So this step is our prescription, and it is the one we run ourselves: aiArch's own coach agent ships behind an offline deterministic mock plus a scripted eval suite (npm run eval) that gates every deploy. Adopt the same shape before writing feature two.
Concretely: turn the failure criteria from step 1 into scripted transcripts — fixed prompts, fixture data in cwd, assertions on the result message and on which tools were called. Run them against a mock in CI (deterministic, free, fast) and against the live model on a manual cadence and before any prompt or model change. Gate the deploy on an adherence threshold, not on vibes.
# package.json scripts — the gate is a script, not a habit
"eval": "node evals/run.mjs --mock",
"eval:live": "node evals/run.mjs --live --threshold 0.8"
An agent without an eval suite cannot be changed safely — every prompt tweak, SDK bump, and model migration becomes a leap of faith. This is the step teams skip and the step that separates the agents still running in six months from the ones quietly turned off.
6. Wire cost tracking and observability
Every result message carries total_cost_usd. Accumulate it per session and per tenant — but know its two caveats: it is a client-side estimate from a build-time price table, not billing (the Usage & Cost API and Console are authoritative), and parallel tool calls share a message id with identical usage — dedupe by message id before summing or your totals inflate.
const seen = new Set(); let usd = 0;
for await (const msg of query({ prompt, options })) {
if (msg.type === 'result' && !seen.has(msg.uuid)) { seen.add(msg.uuid); usd += msg.total_cost_usd; }
}
if (usd > BUDGET_PER_RUN) alertAndHalt(usd); // budget alarm, per run and per day
For traces, turn on OpenTelemetry: CLAUDE_CODE_ENABLE_TELEMETRY=1 (plus CLAUDE_CODE_ENHANCED_TELEMETRY_BETA=1 for traces) exports to your OTel collector; prompt text is excluded by default, which is the right default. Prompt caching is automatic with a 5-minute TTL (ENABLE_PROMPT_CACHING_1H=1 for an hour) and is why a well-structured agent's marginal turns are cheap. One budgeting note: the Sonnet-5 generation tokenizer produces roughly 30% more tokens for the same text than the previous generation — re-baseline any token budgets you carried over.
7. Choose the hosting shape deliberately
Under the hood, query() spawns a Claude Code CLI subprocess and talks to it over stdio. One session equals one subprocess owning its shell, cwd, and JSONL transcripts — and none of that state survives a container restart. That fact dictates the three shapes: an ephemeral container per one-shot job (simplest, prefer it until you can't); a long-running endpoint holding sessions open via streaming input (ClaudeSDKClient in Python); or the hybrid — ephemeral compute plus a SessionStore adapter (S3/Redis/Postgres reference implementations exist) that mirrors session state so a new container can resume it. The store is a mirror, not a replacement — the subprocess remains the source of truth while it lives.
# sizing per concurrent agent (Anthropic's hosting guidance) # ~1 GiB RAM · ~5 GiB disk · 1 CPU → infra ≈ $0.05/hr; tokens dominate at ≥10× docker run --rm --cap-drop ALL --network none \ -e ANTHROPIC_API_KEY -v $PWD/workspace:/srv/agent/workspace invoice-agent
Anthropic names Modal, Cloudflare Sandboxes, Daytona, E2B, Fly Machines, and Vercel Sandbox as sandbox providers, with Docker/gVisor/Firecracker for self-hosting; deployable examples live in the claude-cookbooks hosting directory. Two footguns here. TypeScript's env option replaces the subprocess environment — spread ...process.env or you silently drop PATH and ANTHROPIC_API_KEY (Python merges instead). And there is no top-level session timeout: bound runs with maxTurns, recycle subprocesses on long-lived endpoints (memory grows over long sessions), and keep subagent fanouts narrow or you will meet your rate limits.
8. Ship it, then keep it current
Pin the exact SDK version in the lockfile and treat minors as reviewable changes, because this SDK earns it: the 0.1.0 rename from claude-code-sdk silently stopped loading the Claude Code system prompt and filesystem settings by default, and 0.2→0.3 made native binaries optional deps, moved MCP server connection to the background, replaced TodoWrite with TaskCreate and friends, and turned @anthropic-ai/sdk and @modelcontextprotocol/sdk into peerDependencies. Read the changelog before every bump; remember the bundled CLI updates with it.
# the recurring maintenance loop — run it on a calendar, not on incident npm outdated @anthropic-ai/claude-agent-sdk # then: changelog review before any bump npm run eval # gate the bump like any deploy
Model ids are pinned snapshots, so a bump never changes your model — but models retire, and pricing windows close (Sonnet-5's intro pricing ends 2026-08-31). Put "re-verify model id, pricing, and eval pass rate" on the same calendar entry as the SDK check. This page carries a last-verified date for exactly the same reason.
Artifacts this workflow produces
- The agent repo — pinned SDK version in the lockfile, pinned model id in code,
settingSources: []. - Tool definitions —
tool()+createSdkMcpServer()modules with compact result contracts. - The permission policy —
allowedToolslist,canUseToolpolicy, PreToolUse hooks, in one reviewable file. - The eval suite — scripted transcripts with a mock mode for CI and a live mode with an adherence threshold.
- The cost log — deduped
total_cost_usdaccumulation per session/tenant, with budget alarms. - The deploy container spec — hardened Dockerfile or provider config, sized ~1 GiB / 5 GiB / 1 CPU per agent.
Pitfalls
- Unpinned model. The SDK default is the bundled CLI's default — undocumented, and it moves on SDK updates. Pin explicitly; use
fallbackModelfor resilience, not for ambiguity. - Default settingSources on a server. The default loads user + project + local Claude Code settings from the host — a config-injection surface and a multi-tenant leak. Always
[]in production. - TS env replacement.
options.envreplaces the subprocess environment; forgetting...process.envdropsPATHand your API key with confusing failures. Python merges — don't carry the Python intuition to TS. - Treating
total_cost_usdas billing. It is a client-side estimate; parallel tool calls duplicate usage under one message id. Dedupe by id, reconcile against the Usage & Cost API. - Permission gate mistaken for a sandbox. An allowed Bash call runs on your host. The gate decides whether; the sandbox bounds what happens when. You need both.
- No session timeout. Nothing bounds a session at the top level. Set
maxTurns, recycle long-lived subprocesses, and watch memory on long sessions. - Assuming state survives restarts. Session state lives in the subprocess and its JSONL transcripts. If resume matters, you need the SessionStore pattern — before the first restart, not after.
- Riding a claude.ai subscription. Third-party products may not use claude.ai subscription login — API key (or Bedrock/Vertex/Foundry) only.
When not to use this workflow
Skip the SDK entirely when a single model call with tool use does the job — a bounded loop you own in fifty lines needs no subprocess runtime, no container sizing, and no session store. The SDK earns its weight when you want the built-in tool suite, subagents, hooks, and session management; it is overhead when you don't.
Skip the self-hosted half when Managed Agents fits: if your workload tolerates a beta service and doesn't need ZDR/HIPAA eligibility, prototyping with the SDK and handing production hosting to Anthropic is the documented path and removes steps 6–7 from your plate. And if what you're automating is your own development workflow rather than a product, you want the Claude Code workflow — same runtime, different practice.
Changelog
- 2026-07-13 — initial version, verified against
@anthropic-ai/claude-agent-sdk0.3.207 /claude-agent-sdk(Python) 0.2.116.
- Package versions: @anthropic-ai/claude-agent-sdk 0.3.207 (npm, Node ≥18) and claude-agent-sdk 0.2.116 (PyPI, 2026-07-11, Python 3.10+), checked 2026-07-13.
- API surface —
query(), options, custom tools, permissions, hosting patterns, sizing, session storage: the Agent SDK docs at code.claude.com/docs/en/agent-sdk/ (TypeScript, Python, hosting, permissions, secure-deployment, cost-tracking, session-storage pages; docs moved from platform.claude.com). - Model ids and pricing: Anthropic models overview — claude-opus-4-8 $5/$25, claude-sonnet-5 $3/$15 (intro $2/$10 through 2026-08-31), claude-haiku-4-5 $1/$5, claude-fable-5 $10/$50.
- Sandboxing tiers and egress proxying: Anthropic engineering on Claude Code sandboxing and the
@anthropic-ai/sandbox-runtimepackage. - Breaking-change history: the claude-agent-sdk-typescript CHANGELOG and migration guide (0.1.0 rename, 0.2→0.3 changes).
- Deployable examples: claude-cookbooks hosting; testing/demos: claude-agent-sdk-demos.
- The eval-first prescription in step 5 is aiArch practice (our coach agent ships behind an offline mock + scripted eval gate), stated as such — the SDK docs carry no dedicated evals page as of 2026-07-13.
SDK minors ship breaking changes; re-verify commands against the changelog before adopting a newer version than the one named above. Corrections: hello@aiarch.dev.
Learn the engineering underneath the workflow.
aiArch teaches agents, evals, permissions, and production architecture by building — on a platform that is itself a production agentic system across Anthropic, AWS, and Cloudflare. The build is the curriculum.
See how aiArch helps senior engineers become AI-native, or compare Professional Membership pricing.
Free sample — no signup · every claim cited · full curriculum is waitlist-only