Human-in-the-loop
Most of what a workflow does is automatic, but some steps want a person in the loop: an approval before a write, a choice between options, a free-text answer the program needs to continue. humanInput()pauses the run at exactly that point and resumes it with the person's validated response.
humanInput()
Call humanInput() with a prompt and an input form. The run suspends until someone answers, then the call resolves to their response and the program carries on:
import { agent, humanInput, output } from "@boardwalk-labs/workflow";
const draft = await agent("Draft a reply to this customer complaint: ...");
const review = await humanInput({
prompt: `Send this reply?\n\n${draft}`,
input: { kind: "choice", options: ["Send", "Revise", "Discard"] },
});
if (review.value === "Send") await send(draft);
output({ decision: review.value });Because the run is real code that holds across a wait, everything above the gate (the draft, any local state) is still in memory when the answer arrives. There is no checkpoint to write or replay to reason about.
Text, choice, multiselect
The input field is a discriminated union, and the return type follows the kind you pick:
// Free text → { value: string }
const name = await humanInput({
prompt: "What should we title the release?",
input: { kind: "text", placeholder: "e.g. Otter 2.1" },
});
name.value;
// One choice → { value: string; isOther: boolean }
const tier = await humanInput({
prompt: "Which severity?",
input: { kind: "choice", options: ["low", "high"], allowOther: true },
});
tier.value; tier.isOther;
// Several choices → { values: string[]; other?: string }
const channels = await humanInput({
prompt: "Notify which teams?",
input: { kind: "multiselect", options: ["eng", "support", "sales"], min: 1 },
});
channels.values;A choice or multiselect can allow an open-text "Other..." entry (allowOther, on by default), so a responder is never boxed in by the options you listed.
Waiting is free
While a gate is open the run is suspended, not spinning: the runner is released and the run shows awaiting_input on its timeline. A wait does not burn compute, and it does not count against budget.max_duration_seconds (which caps active compute), so a workflow can sit waiting on a person for hours or days without cost or being killed for being slow. A budget.deadline_seconds, if you set one, still bounds the total wall-clock (waits included), so a stalled gate can't keep a run open forever.
Your program resumes exactly where it paused, with all of its state intact — nothing before the gate re-runs when a person answers, so code around a gate is just ordinary code.
Answering a gate
Whoever is assigned answers from the dashboard, or from the terminal. The CLI surfaces every open gate as an inbox and lets you respond by key:
boardwalk inputs # the org-wide inbox of gates awaiting a response
boardwalk inputs <runId> # just one run's pending gates
boardwalk respond <runId> <key> --value "ship it" # a text / single-choice gate
boardwalk respond <runId> <key> --values eng,support # a multi-select gate
boardwalk respond <runId> <key> --other "something else" # the open "Other..." entryEach gate has a key (set it explicitly, or let the engine derive one from its position). A run resumes once every gate in its current batch is answered, so two humanInput() calls fired together are answered together. See the CLI reference.
Who can answer, and for how long
By default any org member with permission to respond can answer. Narrow it with assignees, and bound the wait with timeout plus an onTimeout fallback:
const review = await humanInput({
prompt: "Approve this production deploy?",
input: { kind: "choice", options: ["Approve", "Reject"] },
assignees: ["role:admin"], // only admins may answer
timeout: "24h", // give up after a day
onTimeout: { value: { value: "Reject", isOther: false } }, // default to safe
});onTimeout is either "fail" (the default: the run fails if no one answers in time) or a default value to resume with, so a stalled approval can fall back to the safe choice rather than hanging.
Letting an agent ask
The gates above are placed by your program: deterministic code decides where a person is needed. An agent() leaf can also ask mid-loop when you opt it in with humanInput: true, which hands the model a tool to pause and request a human answer when it is genuinely stuck or needs a decision:
await agent("Reconcile these conflicting records; ask me if a row is ambiguous.", {
humanInput: true,
});It is off by default: an agent never blocks a run on a person unless you say it may.