Skip to content

People in workflows

A human step parks the run and asks people a question. The run costs nothing while it waits: the Job goes idle between the question and the answer, so a workflow can wait days for a decision. When the answer closes the step, the run continues from there with the answer on the carried item.

Use a human step for approvals, reviews, corrections, and anything a model should not decide alone. Branch on the answer afterwards (see Steps).

The simplest gate

A human step with only a question is an open gate: anyone who can write the vault scope answers once, and the run moves on.

{
id: "approve-refund",
kind: "human",
question: "=Refund {{ $json.amount }} to {{ $json.customer }}?",
label: "Approve refund",
}

The answer adds approved (when the answer carries one), text, output, and answeredBy: "human" to the carried item. A following branch can gate on approved.

question is resolved like any mapped param, so ={{ $json.field }} works in it. Without a question, the step’s label is asked.

Assigned steps

Give the step any of assignees, policy, options, fields, into, mode, or document and it becomes an assigned step: only the people it names may answer, the answers are checked against the options and fields, and a policy decides when the step closes.

{
id: "review-reply",
kind: "human",
question: "Review the drafted reply before it goes to the customer.",
params: {
assignees: "participants-except-initiator",
policy: { atLeast: 2 },
options: [
{ value: "send", label: "Send it" },
{ value: "rewrite", label: "Rewrite", requireNote: true },
],
fields: [{ key: "tone", type: "options", options: ["fine", "too formal", "too casual"] }],
into: "review",
},
}

Parameters

In the Builder, add Human approval from Flow. Its panel has Question, then Who is asked, Closes when, Mode, Answers, Form, and Save answers as, which set the params below.

Param Values Default What it does
assignees "participants", "initiator", "participants-except-initiator", a member key, a list of member keys, or an = mapping anyone who can write the scope Who may answer. Keywords resolve against the run’s participants. A member key is 0x + 64 hex characters.
policy "any", "all", { atLeast: n } "any" When the step closes: the first answer, every assignee, or n distinct answers. "all" needs assignees.
options list of { value, label?, requireNote? } approve, reject The choices an answer picks from. requireNote makes a note mandatory for that choice.
fields list of { key, type, label?, required?, options?, help? } none A form each answer fills in. Types: string, text, number, boolean, url, email, options, list.
into a field name none Puts the step’s output under this field instead of merging it into the item.
mode "single" or "open" "single" open turns the step into a shared document people edit and save until one of them closes it. See below.
document { from, format? } none Open mode only. from is the expression the first version is read from; format is "markdown" (default) or "html".

The run fails at the step, with the reason, when its assignees resolve to nobody or when { atLeast: n } is larger than the number of assignees.

What the step produces

When the policy is met, the step’s output is:

{
"responses": [
{ "member": "0x…", "option": "send", "note": "", "values": { "tone": "fine" }, "eventId": "…", "at": 1760000000000 },
{ "member": "0x…", "option": "send", "note": "", "values": { "tone": "too formal" }, "eventId": "…", "at": 1760000004000 }
],
"counts": { "send": 2 },
"assignees": ["0x…", "0x…", "0x…"],
"decision": "send",
"activation": 1
}

decision is the chosen option when every answer agreed, and null on any disagreement, so a branch gating on decision takes its fallthrough edge unless the answers were unanimous. Responses are in arrival order. With into: "review", branch on review.decision.

Answering

Members answer from the run in ROC: a waiting step shows a card with its question, options, and form. An open gate offers Approve and Reject; an assigned step shows its own answer buttons and who it is still waiting on. An app answers by publishing workflow.feedback into the run’s context:

{ "nodeId": "review-reply", "option": "rewrite", "note": "Drop the second paragraph.", "values": { "tone": "too formal" } }

The vault stamps who sent it, and the runner checks that person against the assignees. A refused answer leaves the run where it was and publishes workflow.response_rejected with a reason:

Reason Meaning
not_assignee The sender is not one of the step’s assignees.
not_a_person The answer was published by an agent or app, not a member.
duplicate This member already answered this step.
unknown_option option is not one the step offers.
note_required The chosen option has requireNote and the note is empty.
invalid_field:<key> A field value is missing (when required) or the wrong type.
stale_activation The answer is for an earlier pass of the step.
not_open Nothing is waiting at that step.
redo_target A send-back named a step it cannot return to.

Each accepted answer publishes workflow.response_recorded.

Limits

What Limit
Note length 20,000 characters
One field value 20,000 characters
Fields per step, values per answer 64
Document body (open mode) 2,000,000 characters
Input the run carries into the step 1 MiB (1,048,576 bytes). A larger item fails the step; shrink it before the step.

Shared documents (open mode)

An open step hands people a document to edit together. Its options default to save and done: save stores a new version and keeps the step open; any other option closes it.

{
id: "edit-reply",
kind: "human",
question: "Edit the reply, then mark it done.",
params: {
mode: "open",
assignees: "participants",
document: { from: "={{ $json.draft }}", format: "markdown" },
},
}

A save sends { "option": "save", "values": { "baseVersion": 3, "body": "…" } }. A save based on an old version is refused with stale_version and the latest version, so two editors never overwrite each other silently. The step’s output adds document: { version, path, body } with the final text.

An open step needs a save option and at least one other option when you declare options yourself.

Send a run back

An answer can carry redoFrom: "<step id>" to send the run back to an earlier agent, human, or connector-action step instead of moving on. The target receives the answer’s text as feedback on its item and runs again from there. The target must be upstream of the waiting step, and inside the same item region if the waiting step is in one.

Loop-back edges

An edge with loop is the only edge allowed to close a cycle. Use it for “revise until approved”. In the Builder, select the edge and turn on Loop back, then set Max rounds, Counter name, and When the rounds run out.

edges: [
{ from: "draft", to: "review-reply" },
{ from: "review-reply", to: "send", when: { field: "decision", op: "eq", value: "send" } },
{
from: "review-reply",
to: "draft",
loop: { max: 3, counter: "rounds", exhausted: "escalate" },
},
],
Field What it does
max Most times the target may be entered, the first pass included. 1 to 100.
counter The item field that counts entries. It reads 1 on the first pass, so a prompt can say Round {{ $json.rounds }}.
exhausted The step the run goes to once the bound is spent. Without it, the run fails with “loop limit reached”.

Re-entering a human step starts a new activation: answers to the earlier pass are refused as stale_activation. A cycle with no loop edge is refused before the run starts.

Participants

A run can name who takes part. Participants are what the assignee keywords resolve against.

Start a run with participants from the Run dialog in ROC, under Who takes part (shown when a Human approval step asks the run’s participants), or by publishing workflow.start with a participants list:

{
"participants": [
{ "member": "0x1f…", "name": "Dana", "roles": ["support-lead"] },
{ "member": "0x9c…", "name": "Ravi" }
],
"ticketId": "T-1042"
}
Rule Limit
Participants per run, initiator included 2 to 64
member 0x + 64 hex characters, no duplicates
name up to 120 characters

The member who starts the run is the initiator. If they are not in the list they are added with the role initiator; if they are, they gain the role. A run started through a webhook takes no participants from its body.

Templates can read the run’s people:

Mapping Value
={{ $run.participants }} The participant list.
={{ $run.members }} The participants’ member keys.
={{ $run.initiator }} The initiator’s member key, or empty.
={{ $run.id }} The run id.

Close a run

The initiator can end a waiting or running run by publishing workflow.close with an optional reason. The run ends cancelled and workflow.run_closed is published. A close from anyone else is refused with workflow.close_refused.