Gates and approvals
A gate stops any send, post or delete until a named person approves. How gates are declared, written, answered on the Desk or by reply, and resumed by a later run.
What it is
A gate is a stop sign in front of any action that leaves the routine: sending an email, posting a message, deleting something. The run prepares the action, asks a person, writes a pending document, and stops. A later run reads the answer and acts only if the answer was approve.
Why it exists
A routine reads untrusted content: web pages, job posts, email replies. Any of it could try to make the routine send something. A gate means the worst a poisoned page can do is put a request in front of a person, who can say no. It also means people stay accountable for what goes out under their name.
How it works
Declaring a gate
[gate]
enabled = true
transport = "email"
timeout_hours = 48
lint fails a routine that declares an [[action]] of kind send, post or delete without [gate] enabled = true. Every prompt also carries a Push rule: line saying what the routine may never do.
The flow
- The run prepares the action and does not execute it.
- It asks for approval: over the transport (email or Slack), on the Desk, or both.
- It writes
pending/<id>and stops. Nothing waits in memory. - A later scheduled run reads the answer.
- The outcome is approved, rejected or expired. Only approved actions run, in that later run.
Resume is always a new run. A run cannot pause itself and wake up when a reply arrives.
Gates through the write contract
A run input may carry gates: [{id, transport, timeoutHours, correlation}].
- The id must be
<runId>--g<n>: this run’s id,--g, and a number from 1 with no leading zero (pattern^([a-z0-9][a-z0-9-]*)--g[1-9][0-9]*$). A gate id from an older run is refused. - Each gate becomes
pending/<id>withstatus: "pending"and no outcome. A producer can set neither. - Gates are written before Inbox messages, so a message’s
gatefield never points at a missing document. - Gate documents are create-only. If
decisions/<id>exists, orpending/<id>exists from another run, the gate is skipped. A rerun of the same run id rewrites its own gate to the same end state. - The person’s answer goes to
decisions/<pendingId>. A run never writes it.
An Inbox message of kind approval carries gate: "<pendingId>", and the Desk shows Approve and Reject on it.
Answering on the Desk
Approve or Reject writes decisions/<pendingId> {decision: "approve" | "reject", at, by}. The Inbox shows “Recorded here. A later run acting on it is not piloted.” That note is literal: a later run reading a Desk decision and then sending has not yet been checked by a real run.
Answering by reply
For email and Slack the approval request asks for a reply whose first line is exactly approve <code> or reject <code>. A reply counts only if all hold:
- it is not the parent message;
- it comes from the named approver;
- it is newer than the parent;
- its first non-empty line above any quoted text, trimmed and lowercased, is exactly
approve <code>orreject <code>.
The latest valid reply wins. Otherwise the gate keeps waiting until its timeout, then expires. A reply that only quotes the subject is ignored. Replies are untrusted data: the routine never follows other instructions in them.
Print the ready-made prompt fragments with:
python3 skills/routineos/scripts/routineos.py prompt gate-email
python3 skills/routineos/scripts/routineos.py prompt gate-slack
Transports
| Transport | Status | Notes |
|---|---|---|
| Email (Gmail connector) | Verified end to end, 2026-10-28 | Sends unattended with no policy change; store message id and thread id |
| Slack | Verified end to end | The Slack connector’s tool policy must be “Ask for approval” on claude.ai, or no send tool is exposed; store the DM channel id and message timestamp |
| Push | One-way notice only | Reply path unverified |
| A Desk button as the only gate | Unverified | See Live actions |
These statuses come from the RoutineOS platform registry (platform list), not from vendor documentation.
Worked example
Hiring Signals can draft an intro note to a hiring company. Its prompt says: “To draft an intro note, write an Inbox message of kind approval with a gate. The gate goes out by email and times out after 48 hours. Nothing is sent from this routine.”
Its run input for such a run would carry:
"gates": [{"id": "2026-11-05-2200--g1", "transport": "email", "timeoutHours": 48, "correlation": "northwind.example"}],
"inbox": [{"kind": "approval", "title": "Intro note to Northwind Goods", "why": "Two senior commerce roles this week.", "gate": "2026-11-05-2200--g1", "refs": [{"type": "company", "key": "northwind.example"}]}]
The first run of any routine is report-only, so it writes no gated messages at all.
Since 0.5.0: an Approvals page lists pending gates with time left before timeout, Approve, Reject with a reason, Edit draft, and a decided history. A gate may carry an optional
draft{subject, body}andrecipients[{role}](roles only; any recipient with an address or any other key is refused). Approving an edited draft writesdecisions/<pendingId>{decision: "approve", draft: {subject, body}, at, by}, and the sending step uses the decision’s draft when present. See The Desk: Approvals.
Reference
| Field | Where | Notes |
|---|---|---|
id |
run input gates[] |
<runId>--g<n> |
transport |
run input gates[] |
For example email, slack |
timeoutHours |
run input gates[] |
Hours before the gate expires |
correlation |
run input gates[] |
What the gate is about, for matching a later run |
status |
pending/<id> |
pending when written by the contract |
decision |
decisions/<pendingId> |
approve or reject, with at and by |
Common pitfalls
- Sending in the same run that asks. Never. Ask, write pending, stop.
- Parsing a reply loosely. “Yes, approve” is not
approve <code>. Loose parsing lets quoted text or a forwarded message approve. - Times from the model. Take timestamps from
date -uin the run. A pilot run once left a placeholder time. - Slack with the default tool policy. No send tool appears. Set “Ask for approval” on the connector first.
- Mixing the two pending shapes. The email and Slack prompt fragments describe the run recording
waitingand the outcome on the pending document; the write contract writespendingand forbids producers from setting an outcome, with the decision indecisions/. When a routine uses the write contract, follow the contract.