How many of your email agent's LLM calls are spent classifying newsletters? If your architecture is "webhook fires, model decides," the honest answer is: all of them. Every mailer-daemon notice, every drip campaign, every "we've updated our privacy policy" gets a full inference pass just to conclude ignore this. You're paying reasoning prices for routing work.
There's an older, cheaper tool for routing work: mail rules. Nylas Agent Accounts β API-controlled hosted mailboxes, currently in beta β support server-side rules that sort, tag, and discard mail before your webhook fires, so the agent only reasons over what's left.
Every Agent Account ships with six system folders β inbox
, sent
, drafts
, trash
, junk
, archive
β and you can create custom ones alongside them (system folder names are reserved):
curl --request POST \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/folders" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{ "name": "invoices" }'
For an agent, custom folders aren't organization β they're queues with different processing semantics. invoices
gets the extraction pipeline. newsletters
gets read by nobody. inbox
β whatever rules didn't claim β gets the expensive LLM treatment. Your message-listing code filters by folder (in=invoices
), and suddenly each worker pulls exactly its own workload.
A rule pairs match conditions with actions. Inbound rules match on sender fields β from.address
, from.domain
, from.tld
β with operators is
, is_not
, contains
, or in_list
, and run actions like assign_to_folder
, mark_as_read
, mark_as_starred
, archive
, trash
, mark_as_spam
, or block
.
Here's the newsletter problem solved at the infrastructure layer, straight from the policies and rules docs:
curl --request POST \
--url "https://api.us.nylas.com/v3/rules" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"name": "Newsletters β Reading folder",
"trigger": "inbound",
"match": {
"operator": "any",
"conditions": [
{ "field": "from.address", "operator": "contains", "value": "newsletter@" },
{ "field": "from.domain", "operator": "contains", "value": "substack.com" }
]
},
"actions": [
{ "type": "assign_to_folder", "value": "<READING_FOLDER_ID>" },
{ "type": "mark_as_read" }
]
}'
The operator: "any"
makes the conditions an OR; omit it and you get AND (all
). Pairing assign_to_folder
with mark_as_read
is a nice touch for agent mailboxes β if your agent uses the unread flag as its "needs processing" marker, routed mail arrives pre-cleared.
A rule does nothing until a workspace references it: add its ID to the workspace's rule_ids
array, and it runs for every Agent Account in that workspace. Group accounts by archetype β your support agents in one workspace, outreach agents in another β and each fleet gets its own routing table.
Rules run in priority
order: lower numbers first, range 0β1000, default 10. Order them specific-before-broad β an is
match on a known sender ahead of a contains
catch-all β because the block
action is terminal: first match rejects the message at the SMTP level and nothing else runs. (Blocking is for spam; for sorting, you want the non-terminal actions.)
The structural caps are roomy enough that you'll rarely hit them: 50 conditions per rule, 20 actions per rule, 10 lists per in_list
condition, 500 characters per condition value.
When the matching set changes often β a growing roster of vendor domains that should route to invoices
β put the values in a list instead of inline. Lists are typed collections (domain
, tld
, or address
) that rules reference via the in_list
operator. The vendor-routing setup is three calls β create the list, fill it, point a 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": "Vendor domains", "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": ["vendor-one.com", "vendor-two.net"] }'
Then the rule's condition becomes { "field": "from.domain", "operator": "in_list", "value": ["<LIST_ID>"] }
. You can add up to 1,000 items per request β values are lowercased, trimmed, validated against the list's type, and duplicates are silently ignored β and every rule pointing at the list picks up changes immediately. That means a non-engineer can maintain the routing table by updating list items, with zero rule edits and zero deploys.
Two boundaries to know before you design around rules. First, inbound rules match sender fields only β from.address
, from.domain
, from.tld
. There's no subject or body matching, so "route anything mentioning 'invoice' in the subject" stays in your application code; rules handle the who, your code handles the what. In practice sender-based routing covers more than you'd expect, because automated mail comes from stable addresses.
Second, block
rules fail closed. If a block rule can't be evaluated because of a transient infrastructure error β say, a list lookup fails mid-in_list
match β Nylas blocks the message rather than letting it through, responding 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 from a genuine match.
Server-side sorting has a classic downside β "why is this message in that folder?" becomes an infrastructure mystery. The rule engine logs every evaluation, and you can pull the audit trail per grant:
curl --request GET \
--url "https://api.us.nylas.com/v3/grants/<GRANT_ID>/rule-evaluations?limit=50" \
--header "Authorization: Bearer <NYLAS_API_KEY>"
Each record shows the evaluation stage, the sender data considered, which rules matched, and which actions applied β folder_ids
included. When your invoice pipeline misses a message, this endpoint answers whether routing failed or extraction did.
The mailbox docs put it plainly: inbound filtering is cheaper than reacting to noise. Rules run during the inbound lifecycle, before storage and before message.created
fires β so by the time your application is involved, the junk is in junk
, the newsletters are in reading
, the invoices are in invoices
, and the inbox contains only mail that genuinely needs a model's judgment.
Pull a day of your agent's inbound traffic and count how many messages actually required reasoning. If the number's under half β and it usually is β write the three rules that would've handled the rest, and check the difference in your token bill next week.