Nodes
Create agent runs, wait for outcomes, and parse structured results.
client.nodes
Shipped, except where a row says otherwise.
A node is one agent run on the crouter canvas. It is asynchronous by construction: create returns as soon as the daemon has spawned it, and the run keeps working after your call returns.
There is one name for it — client.nodes. There is no client.responses alias. A node has a canvas identity, a kind, a working directory, a profile, a deadline, reports, subscriptions, children, and a lifecycle; naming it responses would promise previous_response_id, output_text, tools, and store semantics that do not exist here.
Methods
| Method | Route | Phase | Notes |
|---|---|---|---|
| nodes.create(params) | POST /v1/nodes | 1 | Returns immediately with the node; it is already running. |
| nodes.retrieve(id) | GET /v1/nodes/{id} | 1 | |
| nodes.list(query?) | GET /v1/nodes | 1 | Returns an array, as the daemon does. |
| nodes.outcome(id, { wait }) | GET /v1/nodes/{id}/outcome?wait= | 1 | One long poll, wait 0–25 s. Returns the wire envelope with state: 'pending' | 'settled'. |
| nodes.waitForOutcome(id, opts?) | repeats the above | 1 | Loops until settled. Honours signal; no implicit time bound. |
| nodes.createAndWait(params, opts?) | create + wait | 1 | |
| nodes.parse(params, opts?) | create + wait + typed result | 1 | See below. |
| nodes.message(id, body) | POST /v1/nodes/{id}/messages | 1 | Send a follow-up to a running or dormant node. |
| nodes.interrupt(id) | POST /v1/nodes/{id}/interrupt | 1 | Stop the current turn, keep the node. |
| nodes.cancel(id, body?) | POST /v1/nodes/{id}/close | 1 | Tears the node and its exclusive subtree down. cancel is the method name; close is the route. |
| nodes.update(id, patch) | PATCH /v1/nodes/{id}/config | Shipped | |
| nodes.fork(id) | POST /v1/nodes/{id}/fork | Shipped | |
| nodes.revive(id, body?) | POST /v1/nodes/{id}/revive | Shipped | |
| nodes.promote(id, body?) / nodes.demote(id) | matching routes | Shipped | Move a node between base and orchestrator mode. |
| nodes.recycle(id) | POST /v1/nodes/{id}/recycle | Shipped | |
| nodes.yield(id, body?) | POST /v1/nodes/{id}/yield | Shipped | |
| nodes.wait(id, body) | POST /v1/nodes/{id}/wait | Shipped | |
| nodes.relaunchRoot(id) | matching route | Shipped | |
| nodes.reviveAll() | POST /v1/nodes/revive-all | Shipped | |
| nodes.stream(params, options?) | create + GET /…/events | Shipped | Streaming. |
| nodes.events(id, options?) | GET /v1/nodes/{id}/events | Shipped | Streaming. |
Action methods keep the product's literal name (fork, revive, promote, yield) rather than being renamed into a generic verb.
Every method in the table accepts RequestOptions as its final argument: { headers?, signal?, timeout?, maxRetries? }. nodes.stream(params, options?) uses those options for create and stream opening, except that its stream has no wall-clock timeout; nodes.events(id, options?) takes NodeEventsOptions, which adds after and excludes timeout.
Create parameters
Wire fields are snake_case. Every NodeCreateParams property is optional; parse() additionally requires output_schema. NodeCreateParams is the daemon's CreateNodeRequest, except output_schema also accepts an object. When neither parent nor root is supplied, the SDK sends root: true; otherwise fields pass through without camelizing.
| Field | Type | Meaning |
|---|---|---|
| prompt | string | The run's entire brief. |
| kind | string | Persona. Omit for the profile's default. |
| model | string | Durable model override. |
| profile | string | The store, memory, and purview the run uses. An application passes its own profile here. |
| cwd | string | Where the request came from. |
| pin_cwd | string | Pin the node to this directory regardless of cwd. |
| situational_context | string | Ambient context kept out of the visible prompt. |
| deadline | string (1h30m) | Wall clock from spawn. Expiry cancels the node and records deadline_exceeded. |
| output_schema | string \| JsonSchema \| { toJSONSchema(): JsonSchema } | Widened from the wire's JSON string; the SDK serializes. A zod v4 object satisfies the third form. |
| root | boolean | No parent, no subscription. An application's run is a root. |
| root_lifecycle | 'terminal' \| 'resident' | terminal for a bounded run; resident for one a person will open and keep. |
| mode | 'base' \| 'orchestrator' | Whether the node works hands-on or fans out to children. |
| name | string | Display label. |
| description | string | Display description. |
| parent | node id | Graph placement. An external caller leaves this unset. |
| creator | node id | Graph placement. An external caller leaves this unset. |
| scopes | string[] | Per-run allow-list. Omit it to inherit every scope, or under a scoped token to receive that token's ceiling; a list outside the ceiling answers 403 scope_denied with the offending scopes in details.scopes. In beta, ask, act, schedule, memory:read, and memory:write are enforced; llm, files:<dir>, net, provider groups, and peers are recorded because their performers are not available. See scoped tokens. |
| worktree | string \| boolean | Create a managed git worktree for the run. |
| fork_from | string | Start from an existing conversation. |
| no_kickoff | boolean | Create the node without sending the first message. |
| node_id | string | Spawn at an exact id. A collision answers 409 node_id_exists. |
| prefer_warm | boolean | Serve from the warm pool when the launch tuple matches. |
| outcome_delivery | { action, payload? } | Arm outcome delivery at birth. |
node_id is how you make a create safely retryable. Retry with the same id and a duplicate fails loudly with 409 node_id_exists instead of quietly spawning a second agent — which is why the client never retries a POST for you. See Errors.
Outcomes
waitForOutcome, createAndWait, and parse return the settled NodeOutcome — the same union the API defines, not a translation of it.
| Wire | Narrow on | Carries |
|---|---|---|
| kind: 'result' | outcome.kind === 'result' | structured_result, final_report_path, and on parse a typed output_parsed |
| kind: 'failure', reason: 'declined' | outcome.reason === 'declined' | declined: { reason, code, retryable } \| null — the agent honestly refused the schema |
| kind: 'failure', any other reason | anything else | detail: NodeOutcomeDetailV1 \| null — deadline_exceeded, a provider fault, a wedge |
Agent-side outcomes are returned, never thrown. A decline carries a typed reason, a code the agent chose, and a retryable flag; routing that through an exception would discard the payload and make the happy path lie about what happened. Only transport faults, daemon errors, and your own abort throw.
const outcome = await client.nodes.waitForOutcome(node.node_id, { signal });
switch (true) {
case outcome.kind === 'result':
console.log(outcome.structured_result, outcome.final_report_path);
break;
case outcome.reason === 'declined':
console.warn(outcome.declined?.reason, outcome.declined?.retryable);
break;
default:
console.error(outcome.reason, outcome.detail);
}
outcome() versus waitForOutcome()
nodes.outcome(id, { wait }) is one long poll: the daemon holds the request open for up to wait seconds (0–25) and answers { node_id, state: 'pending' | 'settled', outcome, node_status, deadline_at }. outcome is null while state is 'pending'. Use it when your own loop owns the timing — a job runner that wants to do other work between polls, or a UI that shows a heartbeat.
nodes.waitForOutcome(id, opts?) repeats that poll until the node settles. It applies no implicit time bound: an agent that runs for an hour is polled for an hour. Bound it from either side — pass a deadline to create so the daemon cancels the run, or pass a signal so your client stops waiting.
for (;;) {
const poll = await client.nodes.outcome(node.node_id, { wait: 25 });
if (poll.state === 'settled' && poll.outcome !== null) {
console.log(poll.outcome);
break;
}
// update your UI or job heartbeat here
}
parse() and structured output
import { z } from 'zod';
const run = await client.nodes.parse({
prompt: 'Summarize the failing tests in this repo.',
cwd: '/path/to/repo',
output_schema: z.object({ failures: z.array(z.string()), root_cause: z.string() }),
});
if (run.kind === 'result') console.log(run.output_parsed.root_cause);
else if (run.reason === 'declined') console.warn(run.declined?.reason);
ParsedOutcome<T> is NodeOutcome & { output_parsed: T | null }, discriminated so output_parsed is non-null exactly when kind === 'result'.
output_parsed is structured_result typed to the schema you passed. A Zod schema uses its inferred output type; a Standard Schema uses ~standard.types.output. A JSON-Schema literal and any other object with toJSONSchema() are accepted and serialized, but their output_parsed type is unknown. The SDK does not re-validate the result — the daemon already enforces the schema when the agent submits it, so a second validation pass would only produce a second set of error messages for the same rejection. There is no helper equivalent to OpenAI's zodTextFormat.
A candidate from Basis's real structured SDK result:
{
"tmp": "k1",
"type": "claim",
"slug": "config-memories-in-project",
"text": "All applet configuration memories belong in project memories.",
"quote": "All configuration memories should be in the project memories. We know that.",
"speaker": "Me",
"confidence": "green"
}
Sending a follow-up
nodes.message(id, body) appends to a node's inbox. A dormant node wakes to read it; a running node picks it up at its next turn boundary.
await client.nodes.message(node.node_id, { body: 'Also check the integration lane.' });
Stopping a run
| Call | Effect |
|---|---|
| nodes.interrupt(id) | Stops the current turn. The node stays on the canvas and can be messaged or revived. |
| nodes.cancel(id, body?) | Tears the node down along with the subtree it exclusively owns. Terminal. |
Aborting a signal you passed to waitForOutcome stops your client waiting. It does not stop the node. Call cancel for that.
Nested resources
Nested routes become nested properties.
| Property | Methods | Phase |
|---|---|---|
| nodes.reports | list | Shipped |
| nodes.jobs | list, cancel | Shipped |
| nodes.worktree | close, abandon | Shipped |
| nodes.result | submit | Shipped — only an agent inside a run calls this |
nodes.reports.list(id) returns the agent's pushed progress reports, newest first. Use it when reports are the progress view your application needs; use streaming for live output and tool-call events.
const reports = await client.nodes.reports.list(node.node_id, { limit: 10 });
for (const report of reports) console.log(report.tier, report.body);
Pagination
List routes return what the daemon returns. nodes.list, nodes.reports.list, and nodes.jobs.list return plain arrays — there is no page object, no hasNextPage(), and no after cursor on them, because the daemon has no paging substrate behind those routes and an envelope there would promise a continuation that can never happen.
The one place the page envelope exists is the phase-3 memory routes. See Memory.
Identifier validation
Methods that take a node id validate it before the request: core node methods, lifecycle methods, nodes.reports, nodes.jobs, nodes.worktree, and nodes.result. An invalid id throws TypeError locally and sends no request. nodes.outcome(id, { wait }) also throws RangeError locally when wait is not an integer from 0 through 25.