Skip to the documentation
API

Create a rule

Conditions on one side, actions on the other. Enabled unless you say otherwise.

POST/rules

Runs the real call on your workspace.

POST /rules

Conditions on one side, actions on the other. Enabled unless you say otherwise.

Example

Needs rules:write. Returns 201. position is not accepted. A new rule appends to the end of the list, and moving it is POST /rules/reorder.

curl
curl -X POST "$OE/rules" -H "$AUTH" -H "Content-Type: application/json" \  -d '{    "name": "Receipts to their own label",    "match": "all",    "conditions": [      { "field": "from_domain", "op": "matches", "value": "*.stripe.com" },      { "field": "subject", "op": "contains", "value": "receipt" }    ],    "actions": [      { "type": "label", "value": "USER_RECEIPTS" },      { "type": "archive" }    ],    "stopProcessing": true  }'
Response
{  "object": "rule",  "id": "rul_7f3a1c94e05d3862c1f0a44b",  "name": "Receipts to their own label",  "description": null,  "enabled": true,  "position": 3,  "match": "all",  "conditions": [    { "field": "from_domain", "op": "matches", "value": "*.stripe.com", "negate": false },    { "field": "subject", "op": "contains", "value": "receipt", "negate": false }  ],  "actions": [    { "type": "label", "value": "USER_RECEIPTS" },    { "type": "archive" }  ],  "stopProcessing": true,  "lastMatchedAt": null,  "matchCount": 0,  "createdAt": "2026-08-30T10:41:02.000Z",  "updatedAt": "2026-08-30T10:41:02.000Z"}

A rule created here is ON, and starts acting on the next message. That is the right default for a call somebody made deliberately, and it is the opposite of the MCP createRule tool, which writes the same rule DISABLED because a model that decides to archive mail should not have it archiving before a person has read the rule back.

A duplicate name on the same connection is rule_name_taken, a 409. Names are how a rule is recognised in a run log and in the settings screen, so two rules called "Newsletters" is a report nobody can read.

The 101st rule is rule_limit_reached, a 422. The cap is a guard against a script in a loop rather than an accounting boundary, and it is not locked. Two creates racing at 99 can both succeed.

A key limited to particular addresses or domains creates only a rule that acts on nothing but mail delivered to them, so the rule needs a delivered_to condition that no other address in the workspace matches: equals naming one of its addresses, or matches naming a whole domain it holds. Anything else is a 422 capability_unsupported on conditions.

An OAuth access token needs a verification code for this call. Until the app has verified one in the last 60 minutes, the call answers 403 step_up_required and changes nothing. An API key is never asked. The Authentication page shows how to ask for a code and verify it.

What a condition can ask

A condition is { field, op, value }, with an optional header naming which header to read and an optional negate. value is ALWAYS a string on the wire. Numeric fields are compared as numbers after Number(value), and the two boolean fields take the literal strings "true" and "false", because one field with one type is a schema an OpenAPI generator can describe and a union of three is not.

FieldReadsOperators
fromThe From: header, normalised the way the blocklist normalises it.text
from_domainThe domain of From: and its PARENTS, down to two labels: a message from mail.corp.example.com matches corp.example.com and example.com too, and matches nothing for com.text
envelope_fromThe SMTP MAIL FROM. Different from from on every mailing list, and the only identity a reject may be written against.text
to, cc, bccAny one address in that header.text
recipientAny address in to, cc or bcc: the shorthand for all three.text
reply_toThe Reply-To header.text
delivered_toThe canonical address this copy was delivered to, plus-tag stripped and lower-cased, which is how a catch-all alias is matched.text
subjectThe subject line as it arrived.text
bodyThe text part, or the HTML reduced to text. Capped, so a 20 MB body is not scanned in full.text
headerAny header, named in the condition’s own header field. Required there and lower-cased before comparison.text
list_idThe List-Id header: the handle a mailing list identifies itself by.text
attachment_nameAny attachment’s filename.text
attachment_typeAny attachment’s MIME type, e.g. application/pdf.text
has_attachmentWhether there is one at all.equals "true" / "false"
spamThe spam verdict the delivery path reached, before your rules ran.equals "true" / "false"
attachment_sizeAn attachment’s size in bytes. A comparison matches when any one attachment satisfies it.gt, lt, equals
message_sizeThe whole message on the wire, in bytes.gt, lt, equals
hourHour of arrival, 0–23, UTC.gt, lt, equals
weekdayDay of arrival, 0–6, Sunday is 0, UTC.gt, lt, equals
bouncedWhether the message is a delivery failure notice. Every workspace starts with a rule on it that adds the BOUNCED label.equals "true" / "false"
OperatorWhat it does
matchesA glob, and only a glob: * for any run of characters, ? for one. No regular expressions. A pattern from an API client runs on the delivery path, and a catastrophically backtracking one there is a mailbox that stops receiving.
containsSubstring, case-insensitive.
equalsThe whole value, case-insensitive. On a numeric field, numeric equality.
starts_withPrefix, case-insensitive.
ends_withSuffix, case-insensitive.
gt, ltNumeric, on the four numeric fields only. A text field with gt never matches.

A matches pattern has to carry at least two alphanumeric characters of its own, the same bar the blocklist applies. A bare * is refused at write time rather than accepted and then quietly matching every message that will ever arrive, which is an outage rather than a rule.

A condition the engine cannot answer (an unknown field from a newer client, a pattern that will not compile, contains "") is treated as a question that was never asked rather than as false, and negate does not flip it. That distinction is load-bearing: a negated broken condition treated as false would fire its rule on every message in the mailbox. equals "" is honoured, because "the subject line is empty" is a real question.

What a rule can do

Action`value`What happens
labela label idAdds the label. USER_… ids come from GET /labels.
remove_labela label idRemoves it. Naming the same label in both is resolved before the message is filed rather than left to whichever ran last.
archivenoneFiles it out of the inbox.
mark_readnoneDrops UNREAD.
starnoneAdds STARRED.
spamnoneFiles it under Spam.
trashnoneFiles it under Trash, clearing the labels a trashed message does not keep.
forwardan addressSends a copy on. Read the note below before you use it.
replya template id or slugAuto-replies with a published template, subject to the loop guard below.
block_sendernoneAdds the sender to the blocklist, so the next message is refused at the door.
rejectnoneRefuses the message at SMTP time with 550 5.7.1 Message refused by the recipient. Envelope only. See below.

reject is refused at write time unless the same rule carries at least one envelope_from condition: reject_needs_envelope, a 422. A 550 answers whoever handed us the message, and on a mailing list that is the LIST, which reads the refusal as a bouncing subscriber and unsubscribes the reader from something they only wanted one person to stop posting to. Even with the condition written, a match that came only from the header identities downgrades to filing under Spam, because the envelope is the only identity a refusal can honestly be aimed at.

A rule-driven forward goes out through the send path, which REBUILDS the message: the original DKIM signature does not survive, and neither do exotic parts, unusual headers or anything past the outbound size ceiling, which a 25 MB message with attachments will exceed. It is a copy of what arrived rather than the message that arrived. The address is checked when the rule is written, so an unverified destination is a 422 on the call rather than a rule that silently drops every tenth message.

reply will not answer a machine. It is suppressed when the message carries Auto-Submitted (other than no), Precedence: bulk|list|junk, List-Id, List-Unsubscribe, X-Autoreply or X-Autorespond, when the envelope sender is empty (the shape every bounce takes) and when the headers could not be read at all. On top of that, one sender gets at most one auto-reply per 24 hours from a given mailbox. Two mailboxes with reply rules and no guard mail each other until somebody notices.

Reference