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.