Add a human gate
This guide adds a point in a workflow where a person must decide before anything takes effect. At the end you have a gate with your own choices and message, a step that runs only for one of the choices, and a time limit with a safe default.
Before you start
Section titled “Before you start”You need a workflow you can run. Write your first workflow makes one. For how gates work, read Human gates and trust levels.
The workflow this guide builds
Section titled “The workflow this guide builds”An agent drafts an announcement, a person decides, and the announcement is sent only if they say so.
id: announcename: Send an announcementversion: 1
steps: draft: description: Draft the announcement. execution: agent input_schema: type: object additionalProperties: false required: [subject, body] properties: subject: type: string body: type: string
review: description: A person decides whether the announcement goes out. execution: auto depends_on: [draft] trust: human_confirmed gate: choices: [send, discard] message: | Subject: {{ context.resources.draft.subject | upper }}
{{ context.resources.draft.body }} resolution_messages: send: 'Sent.' discard: 'Discarded. Nothing was sent.'
send: description: Send the announcement. execution: auto depends_on: [review] when: "review.choice == 'send'"1. Put the gate on its own step
Section titled “1. Put the gate on its own step”Add an auto step after the work that needs approval, and set trust: human_confirmed on it. The run stops when it reaches this step.
Keeping the gate on its own step, apart from the agent step, makes the record clear: one entry for what the agent produced, and a separate step for the decision.
2. Name the choices
Section titled “2. Name the choices”List the choices under gate.choices. Use words that say what will happen, such as send and discard. Without gate.choices, the choices are approve and reject.
3. Write what the person sees
Section titled “3. Write what the person sees”Put the text for the person in gate.message. It can pull in earlier results with {{ context.resources.STEP.FIELD }}, and it can use filters such as upper. For the draft above, the person is shown:
Subject: OFFICE CLOSED FRIDAY
The office is closed this Friday.Show the thing being approved itself, not a description of it. The person should not have to look anywhere else to decide.
gate.resolution_messages gives one line per choice. When a gate is answered from Slack, Realm posts that line in the thread to confirm what happened.
4. Act on the answer
Section titled “4. Act on the answer”The answer is available to later steps as review.choice. Give the step that must wait for approval a when condition:
when: "review.choice == 'send'"A step with this condition is skipped for any other answer.
5. Run to the gate
Section titled “5. Run to the gate”Register the workflow and start a run with whichever driver you use. When the draft is accepted, the run stops. The driver is told:
status: confirm_requiredrun_phase: gate_waitingRun is paused at gate '39792a3a-f9c3-470a-b94c-c314fb254234'. Available choices: send, discard.An agent that tries to run send at this point is refused:
status: blockedStep 'send' cannot be called now: it waits on the question on step 'review' (choices: send, discard) — answer it with submit_human_response.Its next_actions holds that answer, submit_human_response. Added after version 0.46.0, which says Step 'send' is not eligible in the current run state. and offers nothing to call.
Find runs that are waiting, from any terminal:
realm run list --status gate_waitingIt prints:
64993bb2-6f65-47d9-801c-d008e9291a00 announce v1 gate_waiting 10/1/2026, 11:13:36 PM 2 step(s) gate: review (0m)realm run inspect <run-id> shows the gate’s ID and choices:
Gate: review (gate 39792a3a-f9c3-470a-b94c-c314fb254234, opened 0m ago) Choices: send, discard6. Answer it
Section titled “6. Answer it”Answer with the run ID, the gate ID and a choice:
realm run respond 64993bb2-6f65-47d9-801c-d008e9291a00 --gate 39792a3a-f9c3-470a-b94c-c314fb254234 --choice sendIt prints:
Responded: 64993bb2-6f65-47d9-801c-d008e9291a00 | choice 'send' | new state 'running'Owed to the engine: 'send' — realm run advance 64993bb2-6f65-47d9-801c-d008e9291a00 runs it, with no project code (nothing to load under /home/me/announce), in the environment of the shell it runs in.If a realm workflow run or realm agent is still waiting on this run, it goes on by itself; the line above is for when none is.The second line names the step the answer leaves to the engine and the command that runs it; the third is printed with it. Both were added after version 0.46.0.
The answer names its answerer only if you say who with --by:
realm run respond 64993bb2-6f65-47d9-801c-d008e9291a00 --gate 39792a3a-f9c3-470a-b94c-c314fb254234 --choice send --by aliceRealm records the name as you give it, without spaces at either end, and does not check it. It takes at most 200 characters, with no control characters, and realm run inspect shows it as answered by alice (as stated, not verified). Without --by it shows (not stated). --by names a person. It is never filled in from the operating system or from REALM_OPERATOR, which name a program.
An AI assistant connected over MCP answers by copying the call in next_actions[0].instruction.call_with and filling in the choice. In the reply that opened the gate, that call carries the gate’s claim_token; the same call in a later reply, such as get_run_state’s, has none. See The claim token.
A choice that is not on the list is refused, and the gate stays open:
Choice 'maybe' is not valid. Expected one of: send, discardThe driver then carries on. Here it ran send, and the run completed:
Phase: completed ✓Completed: draft, review, send7. Set a time limit
Section titled “7. Set a time limit”A gate with no limit waits for ever. To give it one, add three keys:
gate: choices: [send, discard] timeout_seconds: 86400 on_expiry: settle_default default_choice: discardAfter a day with no answer, the gate is answered with discard. Choose the default that is safe to happen unattended. To stop the run instead, use on_expiry: abort and leave out default_choice.
The limit takes effect when something acts on the run. To have expired gates dealt with on a schedule, run this from cron:
realm run drain --expired --all --forceWith a two-second limit for the test, it printed:
✓ 2d252821-75ef-4e16-b3c4-41d7a125f830: gate enactedDrained 1/1 run(s).The record shows that the default was used and that send did not run:
Phase: completed ✓Sealed by: gate_expiry_default (review)Skipped: send send: when_false: review.choice == 'send' [lhs → "discard"]If you see something else
Section titled “If you see something else”'gate.on_expiry: settle_default' requires 'gate.default_choice'You asked for a default answer without naming it. Adddefault_choice, with one of the gate’s choices.- At the
realm workflow runprompt, the gate showsPreview:but noQuestion:line. The gate has nomessageand its step has noprompt, so there is no question to show. Add amessage. Up to version 0.46.0 the hand-run prompt showed no question for any gate.
See also
Section titled “See also”- Gates reference lists every
gatekey, including reminders. - Answer gates from Slack
- Operate runs covers runs that are stuck at a gate.