Human approval
When a person must approve or reject work before an agent continues, read this because the SDK can create an inbox request whose answer wakes the requesting node.
Human approval
Use this recipe when a decision belongs to a person, not a timeout or an agent guess. Run npx tsx examples/guides/human-approval.ts /path/to/repo; it prints an inbox ticket id. Answer the page in the crtr human inbox and the worker wakes with that answer before it reports its result.
import Crouter from '@north-light/crouter-sdk';
import { resolve } from 'node:path';
const client = new Crouter();
const cwd = resolve(process.argv[2] ?? process.cwd());
const worker = await client.nodes.create({
name: 'approval worker',
cwd,
root: true,
root_lifecycle: 'terminal',
no_kickoff: true,
});
const request = await client.human.requests.create({
creator_cwd: cwd,
requester_node_id: worker.node_id,
delivery: { placement: 'panel', inbox: true, reply: true },
page: {
dialect: 'jsx',
source: `export default function Approval() {
return (
<Page title="Approve the release?" subtitle="The worker will continue with your choice.">
<UserQuestion
id="approval"
label="Release action"
body="Approve to continue the release, or reject to stop it."
mode="single"
options={[
{ id: 'approve', label: 'Approve', recommended: true },
{ id: 'reject', label: 'Reject' },
]}
/>
</Page>
);
}`,
},
});
const ticket = await client.human.requests.retrieve(request.request_id);
if (ticket.inbox_ticket_id === null) throw new Error('the approval request was not added to the inbox');
await client.nodes.message(worker.node_id, {
body: `A human approval request is open. Wait for its reply. When it arrives, state whether the release was approved or rejected, then push a final report.`,
});
console.log(`approval ticket: ${ticket.inbox_ticket_id}`);
console.log('Answer it in the crtr human inbox. The worker will wake with the response.');
const outcome = await client.nodes.waitForOutcome(worker.node_id);
console.log(`worker outcome: ${outcome.kind}`);
if (outcome.kind !== 'result') process.exitCode = 1;
The application creates the page through client.human.requests.create() and names the worker as requester_node_id. delivery.reply: true makes the settled answer travel back to that node. The page's UserQuestion supplies a known response shape, so the person sees an explicit approve or reject decision instead of an unstructured prompt.
The worker is terminal because this is a bounded approval run: it waits after setup, wakes when the person answers, then publishes its final result. The pending human request keeps that wait durable. The application does not poll the node or invent a fallback deadline; the human reply is a canvas event. client.human.inbox can list, inspect, and answer inbox tickets when your application provides its own human interface.
Observed against the local daemon after an approve response:
approval ticket: a643479ca7a79758bf86f32d24bd8c9b22e16509cd60996c5e4fe0bc5f097d45
Answer it in the crtr human inbox. The worker will wake with the response.
worker outcome: result
See lifecycle and wakes for the wake model and scopes and trust for why the daemon, rather than the application, owns delivery of the answer.
Fan-out pipeline
When independent parts of one job need separate agent work before one synthesis, read this because an orchestrator can spawn children, wait for reports, and publish a final result while the SDK streams progress.
Application memory
When an application's agents need shared durable knowledge, read this because a profile store gives runs one owned memory location and scopes can allow reading without writing.