cd /news/developer-tools/auto-route-invoices-with-inbound-ema… Β· home β€Ί topics β€Ί developer-tools β€Ί article
[ARTICLE Β· art-26141] src=dev.to pub= topic=developer-tools verified=true sentiment=↑ positive

Auto-Route Invoices With Inbound Email Rules

Nylas introduced inbound email rules for Agent Accounts, enabling automatic invoice routing by matching sender fields and assigning messages to folders before application code runs. The rules support up to 50 conditions and 20 actions, and can reference domain lists for dynamic vendor management without redeployment.

read5 min publishedJun 13, 2026

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?

── more in #developer-tools 4 stories Β· sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain β€” perfect for shipping the agent you just read about.

$git push zahid main
β†’ Live at https://your-agent.zahid.host βœ“
Get free account β†’ Pricing
from €0/mo Β· no card required
LIVE [news/auto-route-invoices-…] indexed:0 read:5min 2026-06-13 Β· β€”