# Folder Automation: Let Rules Sort the Agent's Mail

> Source: <https://dev.to/qasim157/folder-automation-let-rules-sort-the-agents-mail-4o8i>
> Published: 2026-06-16 17:06:02+00:00

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](https://developer.nylas.com/docs/v3/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](https://developer.nylas.com/docs/v3/agent-accounts/policies-rules-lists/):

```
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](https://developer.nylas.com/docs/v3/agent-accounts/mailboxes/) 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.
