crouter
SDK

Nodes

Create agent runs, wait for outcomes, and parse structured results.

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

MethodRoutePhaseNotes
nodes.create(params)POST /v1/nodes1Returns immediately with the node; it is already running.
nodes.retrieve(id)GET /v1/nodes/{id}1
nodes.list(query?)GET /v1/nodes1Returns an array, as the daemon does.
nodes.outcome(id, { wait })GET /v1/nodes/{id}/outcome?wait=1One long poll, wait 0–25 s. Returns the wire envelope with `state: 'pending'
nodes.waitForOutcome(id, opts?)repeats the above1Loops until settled. Honours signal; no implicit time bound.
nodes.createAndWait(params, opts?)create + wait1
nodes.parse(params, opts?)create + wait + typed result1See below.
nodes.message(id, body)POST /v1/nodes/{id}/messages1Send a follow-up to a running or dormant node.
nodes.interrupt(id)POST /v1/nodes/{id}/interrupt1Stop the current turn, keep the node.
nodes.cancel(id, body?)POST /v1/nodes/{id}/close1Tears the node and its exclusive subtree down. cancel is the method name; close is the route.
nodes.update(id, patch)PATCH /v1/nodes/{id}/configShipped
nodes.fork(id)POST /v1/nodes/{id}/forkShipped
nodes.revive(id, body?)POST /v1/nodes/{id}/reviveShipped
nodes.promote(id, body?) / nodes.demote(id)matching routesShippedMove a node between base and orchestrator mode.
nodes.recycle(id)POST /v1/nodes/{id}/recycleShipped
nodes.yield(id, body?)POST /v1/nodes/{id}/yieldShipped
nodes.wait(id, body)POST /v1/nodes/{id}/waitShipped
nodes.relaunchRoot(id)matching routeShipped
nodes.reviveAll()POST /v1/nodes/revive-allShipped
nodes.stream(params, options?)create + GET /…/eventsShippedStreaming.
nodes.events(id, options?)GET /v1/nodes/{id}/eventsShippedStreaming.

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.

FieldTypeMeaning
promptstringThe run's entire brief.
kindstringPersona. Omit for the profile's default.
modelstringDurable model override.
profilestringThe profile the run uses: its project purview, environment and profile-owned documents. An application passes its own profile here.
cwdstringWhere the request came from.
pin_cwdstringPin the node to this directory regardless of cwd.
situational_contextstringAmbient context kept out of the visible prompt.
deadlinestring (1h30m)Wall clock from spawn. Expiry cancels the node and records deadline_exceeded.
output_schemastring | JsonSchema | { toJSONSchema(): JsonSchema }Widened from the wire's JSON string; the SDK serializes. A zod v4 object satisfies the third form.
rootbooleanNo 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.
namestringDisplay label.
descriptionstringDisplay description.
parentnode idGraph placement. An external caller leaves this unset.
creatornode idGraph placement. An external caller leaves this unset.
scopesstring[]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. Scopes are prefixed (crtr:llm, crtr:act, crtr:schedule, crtr:memory:…, crtr:files:…); crtr:act requires crtr:llm, and the list must stay inside what the creator's grant holds. Some scopes are recorded rather than enforced, per the CreateNodeRequest contract. See scoped tokens.
worktreestring | booleanCreate a managed git worktree for the run.
fork_fromstringStart from an existing conversation.
no_kickoffbooleanCreate the node without sending the first message.
node_idstringSpawn at an exact id. A collision answers 409 node_id_exists.
prefer_warmbooleanServe 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.

WireNarrow onCarries
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 reasonanything elsedetail: 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

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

PropertyMethodsPhase
nodes.reportslistShipped
nodes.jobslist, cancelShipped
nodes.worktreeclose, abandonShipped
nodes.resultsubmitShipped — 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.

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.

On this page