Quick start
Build a chat agent, talk to it, give it a tool, pick the conversation back up, then add a sub-agent. Two terminals, no dependencies.
1. Create the worker
Create a file called server.mjs:
// A complete chat agent served with Node's built-in http server.
import { createServer } from "node:http";
function decide({ trigger, proposed }) {
if (trigger.type === "session.start") {
// The engine will use this agent config to generate proposed actions.
return {
agent: {
model: "claude-haiku-4-5-20251001",
stream: true
}
};
}
// Accept the engine's proposal for every other decision.
return proposed;
}
const server = createServer((req, res) => {
let body = "";
req.on("data", (chunk) => (body += chunk));
req.on("end", () => {
const decision = decide(JSON.parse(body));
res.writeHead(200, { "content-type": "application/json" });
res.end(JSON.stringify(decision));
});
});
server.listen(4444, () =>
console.log("worker listening on http://localhost:4444"));Start it:
node server.mjs2. Send a message
In another terminal, install the CLI:
npm install -g @substructure.ai/cliSend a message:
export ANTHROPIC_API_KEY=sk-ant-...
subs run \
--worker-url http://localhost:4444 \
--agent my-agent \
--provider anthropic \
--output pretty \
--input '{"type":"client.message","message":{"role":"user","content": "hi"}}'The reply streams to your terminal. The engine ran the loop; your worker made every decision.
3. Add a tool
Replace decide in server.mjs:
const tools = [
{
name: "get_current_time",
description: "Get the current UTC date and time.",
exec: () => new Date().toISOString()
}
];
function decide({ trigger, proposed }) {
if (trigger.type === "session.start") {
return {
agent: {
model: "claude-haiku-4-5-20251001",
stream: true,
tools: tools.map(({ name, description }) => ({ name, description }))
}
};
}
// Run our tool when the model calls it.
if (trigger.type === "tool.execute") {
const tool = tools.find((t) => t.name === trigger.name);
return { actions: [{ type: "tool.result", result: tool.exec() }] };
}
// Accept the engine's proposal for every other decision.
return proposed;
}Restart the worker and ask a question that needs the tool:
subs run \
--worker-url http://localhost:4444 \
--agent my-agent \
--provider anthropic \
--output pretty \
--input '{"type":"client.message","message":{"role":"user","content": "what time is it?"}}'The model calls the tool, your worker runs it, and the engine folds the result back into the conversation.
4. Continue the conversation
Each run so far started a new session. After every run, the CLI prints the command that continues it:
continue this session with:
subs run --session <session-id> --worker-url http://localhost:4444 ...Run it with a follow-up question:
subs run \
--session <session-id> \
--worker-url http://localhost:4444 \
--agent my-agent \
--provider anthropic \
--output pretty \
--input '{"type":"client.message","message":{"role":"user","content": "what was my first question?"}}'The agent remembers. The whole session is persisted in data.db: messages,
tool calls, results. Stop everything, come back tomorrow, and it still
resumes.
5. Add a sub-agent
One worker can host several agents, told apart by agent_id. Replace the
agent code in server.mjs (everything above createServer):
const tools = [
{
name: "get_current_time",
description: "Get the current UTC date and time.",
exec: () => new Date().toISOString()
}
];
function assistant({ trigger, proposed }) {
if (trigger.type === "session.start") {
return {
agent: {
model: "claude-haiku-4-5-20251001",
stream: true,
tools: tools.map(({ name, description }) => ({ name, description })),
sub_agents: [{ id: "poet", description: "Writes a haiku on any topic." }]
}
};
}
if (trigger.type === "tool.execute") {
const tool = tools.find((t) => t.name === trigger.name);
return { actions: [{ type: "tool.result", result: tool.exec() }] };
}
return proposed;
}
function poet({ trigger, proposed }) {
if (trigger.type === "session.start") {
return {
agent: {
model: "claude-haiku-4-5-20251001",
stream: true,
system: "You are a poet. Respond with a single haiku."
}
};
}
return proposed;
}
// One worker, two agents told apart by agent_id.
function decide(req) {
return req.agent_id === "poet" ? poet(req) : assistant(req);
}The sub-agent appears to the model as a tool. Restart the worker and start a conversation that uses both:
subs run \
--worker-url http://localhost:4444 \
--agent my-agent \
--provider anthropic \
--output pretty \
--input '{"type":"client.message","message":{"role":"user","content": "what time is it? have the poet write a haiku about it"}}'The assistant calls your tool, delegates to the poet, and folds the haiku into its answer. The poet ran in its own session, and its cost and token usage rolled up to the parent.
Next
- Quick start (cloud): run this worker against the hosted engine.
- Core concepts: sessions, turns, triggers, decisions.
- Tool calls: schemas, validation, and where tools run.
- Sub-agents: delegation, child sessions, cost roll-up.
- Durability: what's saved, and how runs recover.
- More agents in
examples/, in Node, Python, and Go.