DEV Community

Qasim
Qasim

Posted on

Tune spam detection for your agent mailbox

The default spam settings on an agent mailbox are a guess. They might be too loose — phishing and junk land in your support agent's inbox and your model dutifully drafts a reply to a Nigerian prince. Or they're too aggressive — a customer's reply from a slightly-misconfigured small-business mail server gets flagged, and your "always reply within 5 minutes" SLA quietly breaks because the message never reached the agent.

Most people building on top of an inbox treat spam as something the provider handles invisibly, and never touch it. That's fine for a human's inbox — a human sees the spam folder, eyeballs false positives, and corrects course. An autonomous agent doesn't. It acts on what arrives, and never notices what got filtered. So the spam threshold stops being a background convenience and becomes a parameter you actually have to set per agent.

On Nylas Agent Accounts, you set it on a policy. The policy carries a spam_detection block with three knobs — a DNSBL toggle, a header-anomaly toggle, and a spam_sensitivity dial — and you attach that policy to a workspace so every agent in the workspace inherits the same spam posture. Different class of agent, different policy, different threshold. That's the whole idea, and it's what this post walks through.

I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for when I'm wiring this up. Every step shows both the raw API call and the CLI equivalent, because half the time I'm in a provisioning script and the other half I'm poking at a single workspace from a shell.

The grant is still the grant

Quick grounding, because it's easy to overthink this. An Agent Account is just a grant with a grant_id. Everything on the data plane — listing messages, reading bodies, sending — is the same grant-scoped API you'd use against any connected mailbox. Spam detection doesn't change any of that. It changes what arrives before your agent ever sees it.

The control plane is three application-scoped resources: policies (limits and spam settings), rules (inbound/outbound match-and-act), and lists (typed value collections rules reference). Spam tuning lives entirely on the policy. Rules are a separate lever — a block rule rejects a known-bad sender at SMTP, a mark_as_spam rule routes a match to junk — but those are exact-match decisions you make about specific senders. The spam_detection block is the fuzzy, score-based filter that runs on everything. This post is about that fuzzy dial, not the rules.

One more thing worth saying plainly: you don't attach a policy to a grant. You attach it to a workspace, and every Agent Account in that workspace inherits it. That indirection is the feature. It's how "tune spam per class of agent" becomes a real operation instead of a thousand individual settings.

What the three knobs actually do

The spam_detection object on a policy has exactly three fields. No more, no fewer — I checked the spec so you don't have to invent any.

Field Type Range What it does
use_list_dnsbl boolean true / false Enables DNS-based block list (DNSBL) checking on inbound mail. The sender's IP gets looked up against block lists of known spam sources.
use_header_anomaly_detection boolean true / false Enables header-anomaly detection — catches malformed or forged headers that legitimate mail servers don't produce.
spam_sensitivity number (float) 0.15.0 The threshold dial. Higher is more aggressive (more mail flagged as spam); lower is more permissive (more mail reaches the inbox).

A few honest notes on each, because the field names tell you what but not when to reach for them.

use_list_dnsbl is cheap insurance against bulk spam from compromised hosts. The tradeoff is that DNSBLs occasionally list shared IPs or freshly-provisioned cloud ranges, so an agent that legitimately expects mail from senders on consumer ISPs or new infrastructure can see false positives. For a support agent receiving mail from real companies, leave it on. For an agent that ingests from a long tail of unknown small senders, watch your false-positive rate before you commit.

use_header_anomaly_detection is almost always safe to enable. Well-behaved mail servers produce well-formed headers; forged headers are a strong spam signal. The only place I'd think twice is if you're receiving from a known-janky internal system that mangles headers — but that's rare enough that "on" is a sensible default.

spam_sensitivity is the one you'll actually tune over time. The range is 0.1 to 5.0, and the docs recommend starting at 1.0 and adjusting from there: go up if spam is slipping through to the agent, go down if legitimate mail is getting flagged. Treat it like a PID setpoint you nudge based on observed behavior, not a value you set once and forget.

Mapping sensitivity to agent archetypes

Here's how I think about the dial in practice. The right value depends entirely on the cost of each kind of mistake for that agent.

  • Support-triage agent (spam_sensitivity ~1.5, both toggles on). A missed legitimate customer email is expensive — it's a broken SLA. A little spam slipping through is cheap, because your triage logic should classify and ignore junk anyway. Bias toward permissive: you'd rather the agent see one spam message than miss one real one.

  • Outreach / cold-send agent (spam_sensitivity ~2.5, both toggles on). This agent emails strangers and the replies it cares about come from real prospects, but the inbox attracts auto-responders, bounce-backs, and opportunistic spam keyed off the From address. You can afford to be aggressive, because a dropped reply from a genuine prospect is rare and the noise volume is high.

  • High-trust internal agent (spam_sensitivity ~0.5, DNSBL maybe off). If an agent only ever receives mail from your own domains or a known set of partners, crank sensitivity down and consider turning DNSBL off entirely — you don't want a partner's misconfigured relay flagged, and the threat surface is tiny. Pair this with an inbound allow-list rule if you want belt-and-suspenders.

None of these numbers are magic. They're starting points you move based on what you see in the agent's actual mail. The point is that one global default can't be right for all three at once — which is exactly why this is a per-policy setting.

Before you begin

You need:

  • An application API key. Every call below authenticates with Authorization: Bearer <NYLAS_API_KEY>, and the key identifies the application — policies, workspaces, and rules are application-scoped, so there's no grant ID in any of these paths.
  • At least one workspace to attach the policy to. Every application already has a default workspace that holds any Agent Account you haven't explicitly grouped, so you can tune that one immediately. If you want per-archetype tuning, create a workspace per archetype (the workspaces guide covers domain auto-grouping).
  • The CLI, if you want the terminal commands. They're verified against nylas v3.1.27.

New to Agent Accounts? Start with the Agent Accounts overview and come back here to tune spam.

Create a policy with tuned spam settings

This is the part the CLI quietly gets wrong if you're not careful, so I'll flag it up front: nylas agent policy create --name "..." creates an empty policy with just a name. It does not let you set spam settings through flags. To create a policy with a spam_detection block, you pass the full request body with --data (or --data-file for a file). Same JSON shape as the API.

Here's a support-triage policy: both detection toggles on, sensitivity at 1.5.

API — POST /v3/policies:

curl --request POST \
  --url "https://api.us.nylas.com/v3/policies" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Support triage policy",
    "spam_detection": {
      "use_list_dnsbl": true,
      "use_header_anomaly_detection": true,
      "spam_sensitivity": 1.5
    }
  }'
Enter fullscreen mode Exit fullscreen mode

CLI — nylas agent policy create --data:

nylas agent policy create --data '{
  "name": "Support triage policy",
  "spam_detection": {
    "use_list_dnsbl": true,
    "use_header_anomaly_detection": true,
    "spam_sensitivity": 1.5
  }
}'
Enter fullscreen mode Exit fullscreen mode

Both return the created policy with its id. Hold onto that id — you'll need it to attach the policy to a workspace. The response also echoes the spam_detection block back, which is a handy sanity check that the values landed the way you meant.

If you're keeping the JSON in version control (you should — these are infra config), --data-file policy.json reads the same body from a file, which keeps your provisioning scripts clean and diffable.

Update spam settings on an existing policy

Tuning is an ongoing thing. You'll create a policy with a starting sensitivity, watch the agent's mail for a week, and then nudge the dial. Updates go to PUT /v3/policies/{id} and you only need to send the fields you're changing.

Say spam is slipping through to the support agent and you want to bump sensitivity from 1.5 to 2.0:

API — PUT /v3/policies/{id}:

curl --request PUT \
  --url "https://api.us.nylas.com/v3/policies/<POLICY_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "spam_detection": {
      "spam_sensitivity": 2.0
    }
  }'
Enter fullscreen mode Exit fullscreen mode

CLI — nylas agent policy update:

nylas agent policy update <POLICY_ID> --data '{
  "spam_detection": {
    "spam_sensitivity": 2.0
  }
}'
Enter fullscreen mode Exit fullscreen mode

The CLI's --name flag is there for a quick rename, but for nested fields like spam_detection you go through --data, exactly as you do on create. Same rule, same reason: flags don't reach into nested objects.

Partial nested updates are supported, which is the convenient part. You can send only the field you're tuning — the {"spam_detection": {"spam_sensitivity": 2.0}} body above changes just the sensitivity and leaves use_list_dnsbl and use_header_anomaly_detection exactly as they were. You don't have to re-send the whole block to preserve the toggles. So nudging the dial week-to-week is genuinely a one-field update.

To inspect what's currently set before you change it, read the policy back. Both forms:

API — GET /v3/policies (list) and GET /v3/policies/{id} (single):

curl --request GET \
  --url "https://api.us.nylas.com/v3/policies" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"

curl --request GET \
  --url "https://api.us.nylas.com/v3/policies/<POLICY_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode

CLI — nylas agent policy list / get:

nylas agent policy list           # every policy + which workspace it's attached to
nylas agent policy get <POLICY_ID>  # one policy in full
Enter fullscreen mode Exit fullscreen mode

Attach the policy to a workspace

A policy does nothing on its own. It only takes effect when a workspace references it via policy_id, and then it governs every Agent Account in that workspace. This is where "per class of agent" becomes concrete: your support workspace points at the support policy, your outreach workspace points at the aggressive policy, and each set of agents gets the spam posture you tuned for it.

API — PATCH /v3/workspaces/{id}:

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 '{
    "policy_id": "<POLICY_ID>"
  }'
Enter fullscreen mode Exit fullscreen mode

CLI — nylas workspace update --policy-id:

nylas workspace update <WORKSPACE_ID> --policy-id <POLICY_ID>
Enter fullscreen mode Exit fullscreen mode

That's it — every agent in that workspace now runs your tuned spam detection. Even the application's default workspace accepts this: on the default workspace, policy_id and rule_ids are the only fields you can change, but those are exactly the two you care about here, so you can tune the default-workspace agents the same way.

To detach a policy and fall back to your billing plan's maximum (most-permissive) limits, clear policy_id. Both forms:

API — PATCH /v3/workspaces/{workspace_id} with null:

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 '{
    "policy_id": null
  }'
Enter fullscreen mode Exit fullscreen mode

CLI — nylas workspace update with an empty --policy-id:

nylas workspace update <WORKSPACE_ID> --policy-id ""
Enter fullscreen mode Exit fullscreen mode

Detaching is the right move when you've decided an agent class doesn't need extra filtering — don't leave a half-tuned policy attached out of inertia.

Per-archetype, end to end

Putting the pieces together, provisioning a second archetype is three operations: create a policy, attach it, confirm. Here's the aggressive outreach setup both ways.

CLI — the path I actually run:

# 1. Create the policy with a higher sensitivity (grab the returned id)
nylas agent policy create --data '{
  "name": "Outreach policy",
  "spam_detection": {
    "use_list_dnsbl": true,
    "use_header_anomaly_detection": true,
    "spam_sensitivity": 2.5
  }
}'

# 2. Attach it to the outreach workspace
nylas workspace update <OUTREACH_WORKSPACE_ID> --policy-id <POLICY_ID>

# 3. Confirm the attachment
nylas agent policy list
Enter fullscreen mode Exit fullscreen mode

API — the same three calls:

# 1. POST /v3/policies (grab data.id from the response)
curl --request POST \
  --url "https://api.us.nylas.com/v3/policies" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{
    "name": "Outreach policy",
    "spam_detection": {
      "use_list_dnsbl": true,
      "use_header_anomaly_detection": true,
      "spam_sensitivity": 2.5
    }
  }'

# 2. PATCH /v3/workspaces/{id}
curl --request PATCH \
  --url "https://api.us.nylas.com/v3/workspaces/<OUTREACH_WORKSPACE_ID>" \
  --header "Authorization: Bearer <NYLAS_API_KEY>" \
  --header "Content-Type: application/json" \
  --data '{ "policy_id": "<POLICY_ID>" }'

# 3. GET /v3/policies to confirm
curl --request GET \
  --url "https://api.us.nylas.com/v3/policies" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"
Enter fullscreen mode Exit fullscreen mode

Two archetypes, two policies, two sensitivities, and no per-grant configuration anywhere. That's the payoff of attaching at the workspace layer instead of fiddling with each mailbox.

Things to keep in mind

A few gotchas I've hit or watched other people hit:

  • --name makes an empty policy. Worth repeating because it's the easiest mistake to make. If you nylas agent policy create --name "Strict" expecting strict spam settings, you get a named policy with no spam_detection block at all — and it'll run at plan defaults. Always use --data (or --data-file) when you want actual settings.

  • Sensitivity is a float between 0.1 and 5.0. Values outside that range are rejected. Don't reach for 10 thinking "extra aggressive" — 5.0 is the ceiling. And 0.1 isn't "off"; it's just very permissive. There's no spam_sensitivity: 0.

  • Tuning is observe-then-adjust. Set a starting value, let the agent run, and move the dial based on what actually shows up. Spam slipping through → raise it. Legitimate mail getting flagged → lower it. The agent won't tell you it's missing mail, so check the spam folder periodically the way a human would — that's the feedback loop the agent can't run on its own.

  • DNSBL has a false-positive cost. It's effective against bulk spam from compromised hosts, but it can flag legitimate senders on shared or freshly-provisioned IPs. For high-trust internal agents, turning it off and leaning on an allow-list rule is often the cleaner call.

  • One policy, many agents. Because the policy attaches to a workspace, every account in that workspace shares the same spam posture. If one agent in the group needs a different threshold, it needs its own workspace and its own policy — don't try to special-case a single grant.

  • Spam detection and block rules are different layers. The spam_detection dial is fuzzy and score-based; a block rule is an exact match against a specific sender. Use the dial for the general posture and rules for the specific senders you already know about. They compose — tune the dial here, then add rules for the named bad actors over in Policies, Rules, and Lists.

What's next

The data plane never changed — it's the same grant-scoped mailbox the whole time. All you did was decide, per class of agent, how much junk gets to reach it.

Top comments (0)