A single inbound email rule can carry up to 50 match conditions and 20 actions β and it runs inside the mail infrastructure itself, before your webhook handler, your queue, or any line of your application code gets involved.
That changes how you build something like invoice routing. The usual approach is: receive message.created
, fetch the message, check the sender, call the folders endpoint to move it. Four network hops and a handler you have to deploy and monitor. With Agent Accounts β programmatic mailboxes currently in beta β there's a declarative alternative: Rules that match sender fields and run assign_to_folder
so invoices are already sitting in the finance folder when your application first sees them.
The Policies, Rules, and Lists docs define a chain:
block
, mark_as_spam
, assign_to_folder
, mark_as_read
, mark_as_starred
, archive
, or trash
.None of them attach to an individual mailbox. Instead, a workspace carries one policy_id
plus an array of rule_ids
, and every agent mailbox in that workspace inherits the lot. Each application gets a default workspace that holds any account you haven't placed elsewhere β attach your rules there once and every unassigned mailbox picks them up. All of this is optional, too: with no workspace policy attached, an account simply runs at your billing plan's maximum limits and delivers every inbound message straight to the inbox.
Say invoices arrive from a handful of vendors. One rule, OR'd conditions, two actions:
curl --request POST \
--url "https://api.us.nylas.com/v3/rules" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"name": "Invoices β Finance folder",
"trigger": "inbound",
"match": {
"operator": "any",
"conditions": [
{ "field": "from.domain", "operator": "is", "value": "billing.vendor-a.com" },
{ "field": "from.address", "operator": "contains", "value": "invoice@" }
]
},
"actions": [
{ "type": "assign_to_folder", "value": "<FINANCE_FOLDER_ID>" },
{ "type": "mark_as_read" }
]
}'
A rule does nothing until a workspace references it β add its ID to the workspace's rule_ids
array and it's live for every account there. Inbound rules can match from.address
, from.domain
, and from.tld
with the operators is
, is_not
, contains
, and in_list
. Matching is case-insensitive.
Hardcoding domains in rule conditions works until accounting onboards a new vendor and files a ticket. A domain
-typed List fixes that. Create it, load it, and point the rule at it:
curl --request POST \
--url "https://api.us.nylas.com/v3/lists" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{ "name": "Invoice vendors", "type": "domain" }'
curl --request POST \
--url "https://api.us.nylas.com/v3/lists/<LIST_ID>/items" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{ "items": ["billing.vendor-a.com", "invoices.vendor-b.io"] }'
Then the rule condition becomes:
{ "field": "from.domain", "operator": "in_list", "value": ["<LIST_ID>"] }
Now whoever maintains the vendor roster updates the List without touching the rule or redeploying anything β every rule that references the list picks up new values immediately. Values get lowercased, trimmed, and validated against the list type on write (a domain
list rejects full email addresses), and duplicates are silently ignored. Lists come in three types β domain
, tld
, and address
β and the type decides which rule fields they can match. One caution: deleting a list cascades to its items, and any rule matching it through in_list
silently stops matching those values.
Rules have fixed limits, and requests that exceed them are rejected with a validation error:
| Cap | Value |
|---|---|
| Conditions per rule | 50 |
| Actions per rule | 20 |
Lists per in_list condition |
|
| 10 | |
Characters per condition value |
|
| 500 |
Fifty conditions sounds like a lot until you start inlining vendor addresses one condition at a time β which is exactly why the List pattern above exists. Ten lists per in_list
condition, each holding thousands of entries, scales much further than inline values ever will.
Inbound rules match exactly three fields: from.address
, from.domain
, and from.tld
. There's no subject matching, no body matching, no "has attachment" condition. So a rule can route mail from billing.vendor-a.com to the finance folder, but it can't catch "an invoice from an unknown sender." That split is actually a decent architecture: deterministic sender-based routing lives in the rule layer, and content-based classification β "is this PDF actually an invoice?" β stays in your application or LLM, working over a pre-sorted folder instead of a raw inbox.
Two adjacent facts from the policy layer are relevant here too. Attachment limits (size, count, and allowed MIME types) on the workspace policy apply to inbound mail only β over-limit attachments are dropped from the stored message β so a policy with a 26214400-byte (25 MB) attachment cap protects your invoice parser from absurd payloads before any code runs. And the same rule_ids
array also carries outbound
rules, evaluated when the account sends; Nylas filters by trigger at evaluation time, so the two directions never cross-fire.
Rules evaluate in priority
order β lower runs first, the range is 0β1000, and the default is 10. The block
action is terminal: for inbound mail it rejects at the SMTP stage, so a spam-block rule at priority 1 means junk never even reaches your invoice rule, let alone your LLM or your handler.
The behavior worth knowing before an incident: rule evaluation fails closed. If a block
rule can't be evaluated because of a transient infrastructure error β say a list lookup fails mid-in_list
match β the message gets blocked rather than waved through. Inbound SMTP responds with a 451
tempfail so the sending server retries instead of bouncing. The audit record carries blocked_by_evaluation_error: true
so you can tell an infrastructure hiccup apart from a genuine match.
Every evaluation writes an audit entry. GET /v3/grants/{grant_id}/rule-evaluations
lists them newest-first, with the evaluation stage, the normalized sender data that was considered, the matched rule IDs, and the applied actions. When finance asks why a vendor's invoice vanished, that's a one-call answer instead of a log-diving session.
Create one domain
List with your top three vendors, one assign_to_folder
rule referencing it, attach both to your default workspace, and send yourself a test invoice. Then check the rule-evaluations endpoint to watch the match happen. Total setup is three API calls. What's the most annoying piece of inbox sorting you're currently doing in application code?