Skip to content
RoutineOS
Esc
↑↓navigate↵open⌘Jpreview
On this page

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

  1. The run prepares the action and does not execute it.
  2. It asks for approval: over the transport (email or Slack), on the Desk, or both.
  3. It writes pending/<id> and stops. Nothing waits in memory.
  4. A later scheduled run reads the answer.
  5. 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> with status: "pending" and no outcome. A producer can set neither.
  • Gates are written before Inbox messages, so a message’s gate field never points at a missing document.
  • Gate documents are create-only. If decisions/<id> exists, or pending/<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> or reject <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} and recipients [{role}] (roles only; any recipient with an address or any other key is refused). Approving an edited draft writes decisions/<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 -u in 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 waiting and the outcome on the pending document; the write contract writes pending and forbids producers from setting an outcome, with the decision in decisions/. When a routine uses the write contract, follow the contract.

Was this page helpful?