Skip to content
Skip to main content

Set policies on an AI agent inbox

Last updated:

An autonomous email agent that reads, decides, and sends has no human watching each action, so the policy you set is the only thing standing between a normal run and a runaway one. The agent might loop on a stuck thread, email a recipient it picked from an attacker’s forwarded message, or burn through your daily send quota in an hour. None of that is the model’s job to prevent, and you can’t trust the model to police itself.

This recipe sets enforceable policy on an Agent Account inbox at two layers: account-level Policies, Rules, and Lists that the platform enforces before mail reaches the provider, and a thin send wrapper in your own code for the checks the platform can’t see. Each agent also runs under its own identity, so its actions stay separate from your users and from every other agent.

How do I set rules or policies on an AI agent email account?

Section titled “How do I set rules or policies on an AI agent email account?”

Set policy in two places. Attach a Policy, Rules, and Lists to the Agent Account’s workspace for limits and recipient blocks the platform enforces at SMTP, and wrap POST /v3/grants/{grant_id}/messages/send in your own code for context the platform can’t judge. Account-level rules fail closed, so a block rule rejects a send before it reaches the provider.

A Policy bundles limits and spam settings, a Rule matches mail and runs actions like block or assign_to_folder, and a List holds the domains or addresses a rule references. You don’t attach any of these to a single grant. Inbound rules apply to Agent Accounts through their workspace’s policy_id and rule_ids, so every account in the workspace inherits them. Outbound rules, including a send block, are evaluated application-wide from your sending application’s enabled rules rather than per-workspace. A single List holds up to 1000 items per add request, with each value capped at 500 characters, which covers most agent allowlists with room to spare.

A runaway loop is the failure that hurts most, so cap send volume before anything else. Set limit_count_daily_email_sent on a Policy and the platform enforces it per Agent Account, refusing sends once the agent crosses the daily count. Without a Policy, an account runs at your billing plan’s maximum, which is usually far higher than any single agent should ever need.

The request below creates a Policy through POST /v3/policies with a daily send cap and tightened retention. Use it to give a prototype agent a low ceiling while a production sales agent runs on a separate, higher one. The limit_spam_retention_period must be shorter than limit_inbox_retention_period, so spam clears out ahead of the inbox.

curl --request POST \
--url "https://api.us.nylas.com/v3/policies" \
--header "Authorization: Bearer <NYLAS_API_KEY>" \
--header "Content-Type: application/json" \
--data '{
"name": "Prototype agent policy",
"limits": {
"limit_count_daily_email_sent": 200,
"limit_inbox_retention_period": 90,
"limit_spam_retention_period": 14
},
"spam_detection": { "spam_sensitivity": 1.0 }
}'

Attach the returned policy ID to a workspace with PATCH /v3/workspaces/{workspace_id}, and every account in that workspace picks up the 200-per-day ceiling. The send wrapper later in this recipe adds a tighter per-hour cap, because a daily limit alone can’t stop a burst that empties your quota in minutes.

A send cap limits volume but not direction, so pair it with an outbound Rule that rejects sends to recipients the agent must never reach. An outbound block rule runs before the message leaves the platform and returns 403 with no sent copy stored. The recipient.* fields match against any recipient, including BCC and SMTP envelope addresses, so a hidden recipient can’t slip past.

The rule below blocks any send to a competitor or test domain through POST /v3/rules. Pair it with a List of domains so a non-engineer can update the blocked set without touching rule definitions: create a domain list, add up to 1000 items per request, then reference it with "operator": "in_list". For the inverse pattern, an application-side allowlist of approved recipients, see restrict AI agent email recipients.

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 outbound to test and competitor domains",
"trigger": "outbound",
"match": {
"conditions": [
{ "field": "recipient.domain", "operator": "is", "value": "example.net" }
]
},
"actions": [ { "type": "block" } ]
}'

Decide the agent’s allowed actions in your own send wrapper, because the platform enforces limits and recipient blocks but can’t judge intent. Funnel every send through one function that runs your per-hour cap, an idempotency key, and a dry-run gate, then forwards an approved payload to POST /v3/grants/{grant_id}/messages/send. The agent gets exactly one way to send, and that path is the only place application policy lives.

The wrapper below threads three checks through a single call. The Idempotency-Key header accepts up to 256 characters, and the API caches each response for 1 hour per grant, so a retried call returns the original result instead of sending twice. A reused key with a different payload returns 409, which is your signal that something changed underneath a retry.

import uuid, time, requests
NYLAS = "https://api.us.nylas.com"
_sends = {} # grant_id -> list of unix timestamps
HOURLY_CAP = 50
def guarded_send(grant_id, message, api_key, dry_run=False):
now = time.time()
recent = [t for t in _sends.get(grant_id, []) if now - t < 3600]
if len(recent) >= HOURLY_CAP:
raise RuntimeError(f"hourly cap reached: {len(recent)} sends")
if dry_run:
return {"status": "dry_run", "would_send_to": message["to"]}
resp = requests.post(
f"{NYLAS}/v3/grants/{grant_id}/messages/send",
headers={
"Authorization": f"Bearer {api_key}",
"Idempotency-Key": str(uuid.uuid4()),
"Content-Type": "application/json",
},
json=message,
)
resp.raise_for_status()
recent.append(now)
_sends[grant_id] = recent
return resp.json()

How can an autonomous agent send and receive email under its own identity?

Section titled “How can an autonomous agent send and receive email under its own identity?”

Give the agent its own Agent Account, a dedicated grant with its own mailbox and address on a domain you control. The agent sends through POST /v3/grants/{grant_id}/messages/send and receives through a message.created webhook, all under that one grant. Its sent and received mail never mixes with a user’s mailbox or with another agent’s, which keeps audit trails clean.

A separate identity matters for accountability. When every send carries the agent’s own from address, you can attribute any message to exactly one agent, and you can revoke that one grant without touching anyone else. Subscribe a message.created webhook through POST /v3/webhooks to drive the receive side, and the agent reacts to replies in its own thread. Each webhook payload caps at 1 MB; bodies over that arrive as message.created.truncated with the body omitted, so fetch the full message before acting. For the full setup, see give your agent its own email.

Audit at both layers, because an autonomous agent acts while nobody watches and you need to reconstruct what happened after the fact. The platform records a rule-evaluation entry every time it judges an inbound or outbound message for an Agent Account. List them through GET /v3/grants/{grant_id}/rule-evaluations, most recent first, to answer “why did this send get blocked?” in under 1 minute instead of digging through provider logs.

Each evaluation record carries evaluation_stage (outbound_send for a send), the evaluation_input of normalized recipient data considered, the matched_rule_ids, and applied_actions with blocked: true when the send was rejected. Records return in reverse chronological order, so the most recent decision sits at the top of the first page. Layer your own append-only log on top: write one record per decision with the inbound message ID, the recipient, whether each application check passed, the idempotency key, and the outcome, including the sends your wrapper declined.

Account policy vs application send wrapper

Section titled “Account policy vs application send wrapper”

Account-level Policies and Rules and your application send wrapper cover different gaps, and a serious agent uses both. The table compares where each control runs. The unified Nylas account policy column shows what the platform enforces before mail reaches the provider, no application code required.

ControlApplication send wrapperUnified Nylas account policy
Daily send quotaManual counter per grant in your storelimit_count_daily_email_sent on a Policy, enforced platform-side
Block recipient domainsAllowlist check before the send callOutbound block rule, rejects with 403 before the provider
Per-hour burst capRolling counter in the wrapperNot available at the account level
Dry-run previewWrapper returns recipients, sends nothingNot available
Audit trailYour append-only log of every decisionGET /v3/grants/{grant_id}/rule-evaluations, per grant
Spam and retentionOut of scopespam_sensitivity and retention fields on a Policy

One honest tradeoff: if your agent legitimately emails arbitrary external recipients, for example an outreach tool that contacts any prospect, a static recipient block will reject valid sends and stall the workflow. In that case drop the outbound block rule, lean on the daily and per-hour caps, and add a human-approval step for first contact with a new domain instead.