When a run needs something from the outside world, it parks and waits. Three builders cover
the cases: a form (structured human input), a signal (a named nudge), and a webhook
(an inbound HTTP call). All three pause the run with zero cost and resume it when the event
arrives. Each handler has the shape function(ctx, data), where data is the inbound payload.
Forms
form(name, opts, fn) parks the run (WAITING_FORM) until a submission arrives.
form("approval_form", {
title: "Approval Required",
schema: { // JSON Schema, validated on submit
type: "object",
properties: { approved: { type: "boolean" }, comment: { type: "string" } },
required: ["approved"],
},
next: "decide",
}, function onApproval(ctx, data) {
ctx.vars.approved = data.approved === true;
return {};
});
Drive it from the shell:
flowctl form list <exec>
flowctl form get <exec> approval_form # see the schema
flowctl form submit <exec> approval_form --data '{"approved":true}'
The form name you submit to is the form node’s name. Opts: title, schema, optional fields
(UI binding hints), next, plus label / userVisible / narrative.
Signals
signal(name, opts, fn) parks the run (WAITING_SIGNAL) until a named signal arrives — a
lightweight resume with an optional data payload.
signal("wait_for_continue", { signal: "continue_signal", next: "step_three" },
function onContinue(ctx, data) { return { resumed: true }; });
flowctl signal send <exec> --name continue_signal --data '{"by":"ops"}'
The name you send to (--name) is the signal value in the opts, not the node name.
Webhooks
webhook(name, opts, fn) parks the run (WAITING_WEBHOOK) until an HTTP callback hits its path.
webhook("wait_for_payment", {
path: "/payment-received", // callback path; what `webhook list` reports
signal: "payment_received", // internal signal name the callback raises
methods: ["POST"],
repeatable: true, // accept multiple callbacks (e.g. partial updates)
next: "process_payment",
}, function onPayment(ctx, data) { return { paid: data.amount }; });
flowctl webhook list <exec>
flowctl webhook call <exec> /payment-received --payload '{"amount":250}'
A one-shot webhook (default) resumes once and then rejects further calls; repeatable: true
keeps it active so it can fire repeatedly — useful for status updates that loop back via a
branch until a condition is met.
A form/signal/webhook handler can run more than once if a worker crashes mid-commit. Keep the handler’s effect idempotent — set variables to a final value rather than incrementing blindly, and guard any external action it triggers.
Start a run with —channel <id> and the engine pushes the parked form or
webhook straight into your Claude Code session, where the agent gathers the input and submits
it. See Claude Code channels.
Next: Triggers & scheduling.