Getting started with agents
This page takes you from an empty workspace to a running agent you can chat with, all on your machine. If you have never run the CLI before, set up Node 24 and Docker first with the CLI getting started guide, then come back here.
An agent lives inside a workspace, a folder that can hold many agents and
workflows side by side. The steps below assume you
have a workspace already (stackbone init my-workspace). If you don't, create
one first.
1. Scaffold an agent
From inside your workspace, add an agent by name:
stackbone add agent supportThis writes a deep-agents/support/ folder with a single index.ts file.
The command runs offline: the agent becomes a member of the workspace
stackbone init already linked, so you do not need to sign in. It never edits
your TypeScript. Re-running it is safe as long as nothing collides (pass
--force to overwrite a name that does).
The folder it creates follows the convention the runtime scans for:
deep-agents/support/
index.ts # name, model, and tools: the whole agentA deep agent carries no manifest of its own, so add puts the packages it
needs (deepagents, langchain, @langchain/* and zod) in your workspace root
package.json. It only adds the ones you have not pinned yourself, and it
never changes a version you already set. Install them before you run anything:
pnpm install2. Write the agent
index.ts default-exports defineDeepAgent from @stackbone/sdk/deep. The
scaffold fills in the name and a model id, and adds one example tool to show the
shape. Delete that tool and you are left with the whole agent:
import { defineDeepAgent } from '@stackbone/sdk/deep';
export default defineDeepAgent({
name: 'support',
model: 'openai/gpt-4o-mini',
});name must be the folder name. It is how a chat client selects this agent, and
it is who owns the agent's prompts.
model is a bare model id string. defineDeepAgent resolves it through the
model provider configured on the deployment. The runtime injects it as
MODEL_PROVIDER_API_KEY, so you never wire a provider client by hand. Pass a
built LangChain chat-model instance instead of a string to target a provider
directly.
Where the model comes from
model is optional. What you write there is a default, and an operator can
pick another model for this agent in Studio without touching your code. Four
sources are tried in order:
| # | Source | Set by |
|---|---|---|
| 1 | The model picked for this agent in Studio | An operator, on the agent's Models block. |
| 2 | The model in defineDeepAgent(...) |
You, in index.ts. |
| 3 | The workspace's Default model | An operator, under Settings › Workspace. |
| 4 | Nothing resolves, so the agent does not build | Nobody. This is the no_model_configured case. |
So leave model out when the agent must run in deployments whose providers serve
different ids, and write one when you tuned the agent for a specific model:
export default defineDeepAgent({
name: 'support',
// No `model`: the box decides, from the Studio pick or the workspace default.
});An empty string is not the same as leaving it out. defineDeepAgent refuses a
blank model, because a blank one is usually a template literal or an
environment variable that came back empty.
When none of the three sources names a model, the agent reports
no_model_configured and does not answer. Fix it by picking a model on the
agent's Models block in Studio, by writing a model in index.ts, or by
setting the workspace Default model under Settings › Workspace.
Changing the pick rebuilds the agent, so the next turn runs on the new model. A turn already in flight finishes on the one it started with. A model instance you built yourself is the one shape a pick cannot override: it carries its own provider and credential, and Studio shows that row read-only.
Inside a tool or a workflow step you can resolve a model the same way for a call
of your own, with
stackbone.models.use(key).
You write the agent's instruction in Studio, not in this file. It goes in the prompt catalogue, under the prompt keyed by the agent's name. Until you write it, the agent runs with an empty instruction: it answers, with no persona yet.
3. Add a tool
A tool is a plain LangChain tool built with tool(...) from
@langchain/core/tools. Its handler can reach the ambient stackbone client
like any other code in your workspace. The tool below looks up an order in
the agent's own database.
Note
The ./schema module is yours. Declare the orders table and apply its
migration first, as stackbone.database describes.
import { tool } from '@langchain/core/tools';
import { z } from 'zod';
import { defineDeepAgent } from '@stackbone/sdk/deep';
import { stackbone } from '@stackbone/sdk';
import { eq } from '@stackbone/sdk/db';
import { orders } from './schema';
const lookupOrder = tool(
async ({ orderId }: { orderId: string }) => {
const [order] = await stackbone.database
.select()
.from(orders)
.where(eq(orders.id, orderId))
.limit(1);
return order ? JSON.stringify(order) : 'not_found';
},
{
name: 'lookup_order',
description: 'Look up an order by its id.',
schema: z.object({ orderId: z.string() }),
},
);
export default defineDeepAgent({
name: 'support',
model: 'openai/gpt-4o-mini',
tools: [lookupOrder],
});The model decides when to call the tool from its description and schema.
Keep the description plain and the schema narrow: those two fields are all the
model has to go on.
4. Run the local emulator
stackbone devstackbone dev builds every agent it discovers in-process, brings up a local
Postgres, Redis, and MinIO stack, and starts the emulator at
http://127.0.0.1:4242. It hot-reloads when you edit a file, so you can
leave it running while you work. The boot banner prints a cloud Studio
deeplink (the recommended way in) and a local fallback URL.
5. Talk to the agent
An agent speaks the standard OpenAI Chat Completions and Anthropic Messages
APIs. Any client built for either works against it with a base URL and a key.
Pick the agent with the request's model field, which is the agent's folder
name:
curl http://127.0.0.1:4242/openai/v1/chat/completions \
-H 'content-type: application/json' \
-H "authorization: Bearer $STACKBONE_API_KEY" \
-d '{"model":"support","messages":[{"role":"user","content":"Where is order 123?"}]}'STACKBONE_API_KEY holds a workspace API key with agent:chat, minted in
Studio under Integrations → API keys. The local emulator checks it like a deployed
box does. See API keys. The call is stateless by
default: send the full messages[] array each time, the same as calling
OpenAI or Anthropic directly. To have the server keep the transcript for you
instead, send an x-stackbone-session header with a key you choose. To keep
replaying the history yourself but still group the turns into one session,
send x-stackbone-conversation instead. See the
agent protocol for the full contract, including
streaming and the Anthropic wire.
From Studio: open the Studio deeplink from the boot banner and chat with your agent in the Playground. Studio reaches the local emulator over the tunnel, so you get the same surface you would get in the cloud.
6. Call the agent from a workflow
Inside a workflow step, reach this agent by name
with callDeepAgent from @stackbone/sdk/workflow. It runs one turn of the
agent in the same process (no HTTP hop) and resolves with { text }:
import { callDeepAgent } from '@stackbone/sdk/workflow';
async function askSupport(question: string) {
'use step';
return callDeepAgent('support', question);
}Call callDeepAgent from inside a 'use step' function, so the turn
becomes a durable checkpoint. See
Calling an agent from a workflow
for the full detail.
Run an agent on the minimal harness
By default an agent runs on the full deep-agent harness. Next to your tools
it gets a todo list (write_todos), a filesystem (ls, read_file,
write_file and more), a task tool that hands work to a general-purpose
subagent, and a summary of long conversations. A harness prompt that explains
those tools goes after your instruction on every turn.
An agent that answers in one short step needs none of that, and it pays for it
on every turn in prompt tokens and in time to the first token. Set
harness: 'minimal' to drop it:
export default defineDeepAgent({
name: 'phone-desk',
model: 'openai/gpt-4o-mini',
harness: 'minimal',
tools: [lookupOrder],
});A minimal agent keeps its model, its tools, its interruptOn approvals and its
sessions. Its system prompt is your instruction and nothing else. It also keeps
two pieces of the harness that add nothing to that prompt: the repair of a tool
call left without its result (a caller who talks over the agent mid tool call
would otherwise break every later turn on a strict provider), and Anthropic
prompt caching when you pass a built Anthropic model instance. A model id string
runs through OpenRouter, so prompt caching does not apply to it. The setting
applies to this agent only: every other agent in the workspace keeps the full
harness.
Use it when latency matters more than planning, for example in an agent that
answers phone calls. defineDeepAgent refuses these fields next to it, because a
minimal agent has nothing to reach them with:
| Field | Why it is refused |
|---|---|
subagents |
There is no task tool to delegate to them. |
backend |
There is no filesystem for the backend to store. |
editableState key todos or files |
The agent has no such state to seed. |
It also refuses any value other than 'minimal'. Leave harness out for the
full harness.
Note
The minimal harness builds the agent with the langchain package.
stackbone init and stackbone add deep-agent pin it in your workspace root
at 1.5.2, a version inside the range deepagents asks for. A workspace
created before that gets whichever langchain version deepagents pulls in,
which can be newer than the box runs. Add the same pin so stackbone dev and
the box run one copy at one version: pnpm add langchain@1.5.2.
Where to go next
- Examples: complete agents you can copy: a greeter, a config-driven agent, and one backed by retrieval.
- What is an agent: the authoring and discovery model in full.
- Browser tools: hand the agent a real browser when the answer lives on a web page rather than in an API.
- Workflow + Agents: wire this agent into a durable workflow.