Skip to content

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.

You need a workflow you can run. Write your first workflow makes one. For how gates work, read Human gates and trust levels.

An agent drafts an announcement, a person decides, and the announcement is sent only if they say so.

id: announce
name: Send an announcement
version: 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'"

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.

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.

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.

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.

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_required
run_phase: gate_waiting
Run 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: blocked
Step '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:

Terminal window
realm run list --status gate_waiting

It 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, discard

Answer with the run ID, the gate ID and a choice:

Terminal window
realm run respond 64993bb2-6f65-47d9-801c-d008e9291a00 --gate 39792a3a-f9c3-470a-b94c-c314fb254234 --choice send

It 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:

Terminal window
realm run respond 64993bb2-6f65-47d9-801c-d008e9291a00 --gate 39792a3a-f9c3-470a-b94c-c314fb254234 --choice send --by alice

Realm 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, discard

The driver then carries on. Here it ran send, and the run completed:

Phase: completed ✓
Completed: draft, review, send

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: discard

After 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:

Terminal window
realm run drain --expired --all --force

With a two-second limit for the test, it printed:

✓ 2d252821-75ef-4e16-b3c4-41d7a125f830: gate enacted
Drained 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"]
  • 'gate.on_expiry: settle_default' requires 'gate.default_choice' You asked for a default answer without naming it. Add default_choice, with one of the gate’s choices.
  • At the realm workflow run prompt, the gate shows Preview: but no Question: line. The gate has no message and its step has no prompt, so there is no question to show. Add a message. Up to version 0.46.0 the hand-run prompt showed no question for any gate.