The usual way to keep an AI email agent from talking to the wrong people is a hardcoded set in your application code:
ALLOWED_DOMAINS = {"yourcompany.com", "customer.example"}
That works, and for a single internal agent it's the right call. But the moment a non-engineer needs to add a customer domain, or you want the same allowlist to govern five different agents, or you want it to apply to mail coming in and not just going out, that set in code becomes a deploy-shaped bottleneck. Every change is a PR, a review, a release.
There's a layer below your code that can carry that policy for you. An Agent Account on Nylas evaluates Rules against every message — inbound on arrival, outbound on send — and those rules can point at Lists, which are typed, editable collections of domains, TLDs, or addresses. Update a list, and every rule that references it picks up the change immediately. No redeploy, no code change, no model in the loop.
This post goes deep on that pattern: Lists fronted by the in_list rule operator, used for allow and block on both directions. If you've already read the basic inbound/outbound filtering post, this is the part that makes the filtering dynamic.
What you actually get
The data plane doesn't change. An Agent Account is just a grant with a grant_id, and everything you already know about Messages, Drafts, Threads, and Folders still applies. Rules and Lists sit one level up, on the control plane — they're application-scoped admin resources with no grant ID in the path. Your API key identifies the application, and the rules apply to every Agent Account in a workspace.
So the mental model is two planes:
-
Data plane —
GET /v3/grants/{grant_id}/messages,nylas email send, the stuff your agent does. -
Control plane —
POST /v3/lists,POST /v3/rules, the stuff that decides what the agent is allowed to do.
A List holds values. A Rule references a list through the in_list operator and says "if the sender domain is in this list, block it" (or assign it, or archive it). Change the list, and the rule's behavior changes with it. That indirection is the whole point: the what (which domains) lives in a list a human can edit; the how (block vs. route) lives in a rule you set once.
Why this beats a code-side allowlist
A few honest tradeoffs, because a static ALLOWED_DOMAINS set genuinely is simpler when you only have one agent:
- Editable without a deploy. Someone updates a list through the API or a CLI command, and the change is live. No PR, no release. This is the reason to reach for it.
- One list, many rules. A single "Blocked domains" list can feed an inbound block rule and an outbound block rule. Update it once, both directions follow.
-
Both directions. A code-side check before your send call only covers outbound. Rules also reject inbound mail at SMTP, before it ever lands in the mailbox or fires a
message.createdwebhook — so your application never even sees it. -
Fails closed on infrastructure errors. If a
blockrule can't evaluate a list lookup because of a transient error, Nylas blocks the message rather than letting it slip through. Inbound gets a451tempfail so the sender retries; an API send gets a retryable503.
The flip side: if your agent legitimately needs to reach arbitrary external recipients — an open-ended sales tool, say — a static allowlist will block valid sends and frustrate the workflow. Lists fit internal and known-customer agents, not cold outreach. For that case, lean on volume caps and human approval instead, which the restrict agent recipients cookbook recipe covers.
Before you begin
You need an Agent Account (a grant created via POST /v3/connect/custom against a registered domain — see Agent Accounts), an API key for the same application, and the host in your examples set to https://api.us.nylas.com. Every API call below carries Authorization: Bearer <NYLAS_API_KEY>.
I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for. Three subcommands cover the whole flow, and each maps to a distinct piece: nylas agent list manages list resources (/v3/lists), nylas agent rule manages rules (/v3/rules), and nylas workspace update attaches a rule to a workspace via its rule_ids. They don't overlap — nylas agent list never touches workspaces, and a created rule does nothing until a workspace references it (more on that below). Where it matters I'll show both the curl and the CLI form. CLI reference lives at cli.nylas.com/docs/commands.
One thing to internalize up front about which fields each direction can match, because it's the most common mistake:
-
Inbound rules can only match
from.*fields —from.address,from.domain,from.tld. You only know who sent it. -
Outbound rules can match
from.*,recipient.*(recipient.address,recipient.domain,recipient.tld), andoutbound.type(composevs.reply).
That asymmetry drives everything below. You allowlist senders on the way in, and recipients on the way out.
Create a List of domains
A list has a fixed type — domain, tld, or address — set at creation and immutable. The type decides which rule fields it can match: a domain list matches from.domain and recipient.domain, an address list matches the .address fields, and so on. Start with a domain list, since most allow/block decisions are domain-level.
curl --request POST \
--url "https://api.us.nylas.com/v3/lists" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"name": "Blocked domains",
"type": "domain"
}'
The response carries the list id you'll reference from rules:
{
"request_id": "5fa64c92-e840-4357-86b9-2aa364d35b88",
"data": {
"id": "d1e2f3a4-5678-4abc-9def-0123456789ab",
"name": "Blocked domains",
"type": "domain",
"items_count": 0,
"created_at": 1742932766,
"updated_at": 1742932766
}
}
Same thing from the CLI, which can seed items at creation time with repeatable --item flags:
nylas agent list create \
--name "Blocked domains" \
--type domain \
--item spam-domain.com \
--item another-bad-domain.net
The --type is domain, tld, or address, and it's immutable — pick it deliberately. The CLI hint says it plainly: the type "determines which rule fields the list can match in in_list conditions."
Add items to a List
Lists are where the dynamic part lives. Add up to 1000 items per request; values are lowercased, trimmed, and validated against the list's type, so a domain list rejects full email addresses. Duplicate additions are silently ignored, which means you can re-run an add idempotently.
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": ["spam-domain.com", "another-bad-domain.net"]
}'
From the CLI, items are positional arguments after the list ID:
nylas agent list add <LIST_ID> spam-domain.com another-bad-domain.net
This is the command you hand to a non-engineer, or wire into an internal admin panel, or call from a webhook handler when an abuse report comes in. It's a single API write that immediately changes the behavior of every rule pointing at the list — no rule edit, no deploy. To pull a domain back out, nylas agent list remove <LIST_ID> spam-domain.com (or DELETE /v3/lists/{list_id}/items).
Block inbound mail with an in_list rule
Now wire a rule to the list. For inbound, you can only match on the sender, so this is a denylist of sender domains. The key is "operator": "in_list" with the list ID — note that value is an array of list IDs, not the domains themselves. A single in_list condition can reference up to 10 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": "Block senders on our blocklist",
"priority": 1,
"trigger": "inbound",
"match": {
"conditions": [
{
"field": "from.domain",
"operator": "in_list",
"value": ["<LIST_ID>"]
}
]
},
"actions": [
{ "type": "block" }
]
}'
The CLI condition format is field,operator,value. For in_list, the trailing comma-separated values are list IDs — field,in_list,list-id-1,list-id-2 — which maps directly to the array in the JSON above:
nylas agent rule create \
--name "Block senders on our blocklist" \
--trigger inbound \
--priority 1 \
--condition from.domain,in_list,<LIST_ID> \
--action block
The block action is terminal — it can't be combined with other actions. For inbound, it rejects the message at SMTP, so it never reaches the mailbox and never fires a message.created webhook. Your application is genuinely insulated from that sender.
A note on priority and rule ordering: rules run lowest priority first (range 0–1000, default 10), and the first matching block wins. Put your specific in_list block ahead of any broad contains rules so the precise check fires first.
Activate the rule on a workspace
Here's the step that trips people up: creating the List and the Rule isn't enough. A rule is inert until a workspace references it. Workspaces carry policies and rules — you don't attach a rule to a grant directly. You add the rule's ID to a workspace's rule_ids array, and from then on it runs for every Agent Account in that workspace. One array carries both inbound and outbound rules; Nylas filters by trigger at evaluation time, so the same array can hold your inbound block and your outbound block side by side.
Attach the rule with PATCH /v3/workspaces/{workspace_id}. Pass the full set of rule IDs you want active — this replaces the array, so include any existing rules you want to keep:
curl --request PATCH \
--url "https://api.us.nylas.com/v3/workspaces/<WORKSPACE_ID>" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"rule_ids": ["<INBOUND_RULE_ID>"]
}'
From the CLI, nylas workspace update takes a comma-separated list of rule IDs:
nylas workspace update <WORKSPACE_ID> --rules-ids <INBOUND_RULE_ID>
If you don't know the workspace ID, the default workspace is the one that holds any Agent Account you haven't placed in a custom workspace, so attaching there covers your unassigned accounts. As a convenience, nylas agent rule create resolves the default grant's workspace and attaches the new rule to it for you — but when you create rules over the raw API, or you want a rule to apply to a non-default workspace, this PATCH is the step that actually turns the rule on. Re-run it whenever you add an outbound rule below, listing every rule ID you want active.
Allow vs. block on outbound
Outbound is where the allowlist pattern gets interesting, because you have two ways to express "only talk to approved recipients," and they fail in opposite directions.
The blocklist version — block sends to a list of denied recipient domains — is permissive by default. Anything not on the list goes through. Good for known-bad domains, competitors, test domains that leaked into production:
curl --request POST \
--url "https://api.us.nylas.com/v3/rules" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"name": "Block sends to denied recipients",
"trigger": "outbound",
"match": {
"conditions": [
{
"field": "recipient.domain",
"operator": "in_list",
"value": ["<DENY_LIST_ID>"]
}
]
},
"actions": [
{ "type": "block" }
]
}'
nylas agent rule create \
--name "Block sends to denied recipients" \
--trigger outbound \
--condition recipient.domain,in_list,<DENY_LIST_ID> \
--action block
This rule, like the inbound one, does nothing until it's on a workspace's rule_ids. Re-run the PATCH /v3/workspaces/{workspace_id} (or nylas workspace update <WORKSPACE_ID> --rules-ids <INBOUND_RULE_ID>,<OUTBOUND_RULE_ID>) with both IDs in the array — the same array carries both directions, and Nylas runs each rule only on its matching trigger.
A true allowlist — block any recipient not on the approved list — is stricter and fails closed. Worth being precise about what the rule engine supports here: in_list matches when a value is present in the list, so it expresses "block what's on the denylist" directly. There's no not_in_list operator. For the strict "deny everything except the allowlist" semantics, the durable pattern is to keep the hard fail-closed allowlist in your send wrapper — where an empty or unknown domain is denied by default — and use the Nylas in_list block rule for the dynamic denylist layer on top.
In practice that split works well: the rule is the editable, no-deploy layer a non-engineer can update; the wrapper is the fail-closed gate your code owns. The restrict agent recipients recipe covers the wrapper side. Don't try to make one do both jobs.
One important behavioral detail for outbound: recipient.* matches against any recipient, including CC, BCC, and SMTP envelope recipients. That's exactly what you want for data-loss prevention — a hidden BCC to a denied domain still trips the block. An outbound block returns 403 to your send call and stores no sent copy; treat it like any other non-retryable delivery failure.
Route inbound mail by list instead of blocking
in_list isn't only for blocking. Swap the action and you've got dynamic routing. Keep a list of "VIP customer domains" and auto-star or folder their mail without touching code when a new customer signs:
curl --request POST \
--url "https://api.us.nylas.com/v3/rules" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"name": "Star VIP senders",
"trigger": "inbound",
"match": {
"conditions": [
{
"field": "from.domain",
"operator": "in_list",
"value": ["<VIP_LIST_ID>"]
}
]
},
"actions": [
{ "type": "mark_as_starred" }
]
}'
nylas agent rule create \
--name "Star VIP senders" \
--trigger inbound \
--condition from.domain,in_list,<VIP_LIST_ID> \
--action mark_as_starred
The non-blocking actions — mark_as_starred, mark_as_read, assign_to_folder, archive, mark_as_spam, trash — all compose with in_list. Sales adds a domain to the VIP list, and the agent's inbox starts starring that customer's mail. Nobody touched a rule.
Guardrails and gotchas
A few things I've watched people trip over:
-
A rule does nothing until it's on a workspace. The most common "my rule isn't firing" cause is a rule that was created but never added to a workspace's
rule_ids. Creating the List and Rule is two-thirds of the job; thePATCH /v3/workspaces/{workspace_id}is what arms it. -
rule_idsis a full replacement. The PATCH replaces the array, it doesn't append. List every rule ID you want active each time, or you'll silently detach the ones you left out. -
List type must match the field. A
domainlist only works withfrom.domain/recipient.domain. Point afrom.addresscondition at a domain list and it won't match. Pick the type to fit the field you'll match. -
valueis an array of list IDs forin_list, not the values. The domains live in the list; the rule references the list by ID. This catches everyone once. -
Inbound can't see recipients. You cannot allowlist recipients on inbound rules — there's no
recipient.*on the inbound trigger, because there's only one recipient (the agent) and it's already known. Recipient allow/block is outbound-only. -
Up to 10 lists per
in_listcondition, 500 chars per value, 50 conditions and 20 actions per rule. Generous, but real caps. -
Deleting a list cascades. Items go with it, and rules that referenced it via
in_listsimply stop matching its values. No dangling references, but also no warning — audit before you delete. -
Audit what actually fired. When something gets blocked or routed unexpectedly,
GET /v3/grants/{grant_id}/rule-evaluationslists every evaluation most-recent-first, with the matched rule IDs and applied actions. It's the fastest answer to "why did this message get blocked?"
What's next
The combination to remember: a List is the editable noun, the in_list operator is the indirection, the trigger decides whether you're filtering senders (inbound) or recipients (outbound), and the workspace rule_ids array is what arms everything. Create the list, create the rule, attach the rule — skip that last step and nothing fires. Once those click, you can hand the who to a non-engineer and keep the how in version control.
- Policies, Rules, and Lists for the full schema — every operator, action, and field
- Restrict agent recipients for the application-side fail-closed allowlist that pairs with these rules
- Agent Accounts to provision a grant against a registered domain
-
CLI commands for
nylas agent list,nylas agent rule, andnylas workspace updatereference
AI-answer pages for agents
When this post is published, link AI agents and crawlers to the retrieval-ready version on cli.nylas.com:
- Topic runbook: https://cli.nylas.com/ai-answers/agent-account-rules-lists-api-cookbook.md
- Industry playbooks hub: https://cli.nylas.com/ai-answers/agent-account-industry-playbooks.md
Top comments (0)