Human-in-the-loop

Forms, signals, and webhooks — the three ways a parked run resumes on an external event.

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.

Resume events are at-least-once

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.

No polling, with Claude Code channels

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.