Subject-line A/B testing is the kind of thing you usually rent. You sign up for an ESP, drop your contacts into their list, pick "split test" from a dropdown, and let their black box decide a winner — on their definition of "winner," with their attribution, on their schedule. That's fine if email is your whole product. It's a lot of ceremony if you have an agent that already sends mail and you just want to know which of two subjects gets more humans to reply.
Here's the pivot: the experiment is just two sends and a counter. If your agent owns a mailbox, it can run the split itself — pick a variant per recipient, send from its own address, watch replies land back in its own threads, and tally the result. No ESP, no list import, no separate analytics product. The whole thing lives inside the same mailbox the agent already uses.
I work on the Nylas CLI, so the terminal commands below are the exact ones I reach for, and I'll show the matching HTTP call for each one so you can wire it into whatever your agent is actually built on. The data plane is the same either way.
What you actually get
An Agent Account is a real, sending-and-receiving mailbox that your code owns outright — no human, no OAuth handshake, no token to refresh. Under the hood it's just a Nylas grant with a grant_id, which is the important part: every grant-scoped endpoint you already know (Messages, Threads, Folders, Webhooks) works against it unchanged. There's nothing new to learn on the data plane. You send with POST /v3/grants/{grant_id}/messages/send, you read threads with GET /v3/grants/{grant_id}/threads/{thread_id}, and replies arrive as message.created webhooks. That's the entire surface area this experiment needs.
What Nylas does not do is run the experiment for you. There's no "split test" endpoint, no built-in significance math, no variant tracking. And that's correct — an experiment is your business logic, not the mail provider's. Nylas moves the email and reports the events; the random assignment, the sample-size discipline, and the winner call all live in your app. I'll be honest about that line throughout, because the parts you own are the parts that make the result trustworthy.
Why this beats reaching for an ESP
- No data export. Your recipients, your sends, your replies all stay in your system. You're not syncing a contact list into a third party just to test two strings.
- Reply rate, not open rate. Most ESP split tests optimize opens or clicks. With an agent mailbox you can attribute on replies — a human typed something back — which is a far stronger signal for outreach, support, or sales follow-ups.
-
One code path, every provider. A reply from Gmail, Outlook, Yahoo, or an IMAP mailbox lands as the same
message.createdevent on the same thread shape. You write the attribution once. - The math is yours, so you can trust it. You control the split, the minimum sample, and when you're allowed to declare a winner. No vendor deciding "statistical significance" behind a curtain.
The tradeoff: you're writing the experiment harness yourself. If you only ever test subject lines for newsletter blasts and never look at replies, a marketing ESP is genuinely the better tool. This is for the case where an agent is the sender and replies are the outcome you care about.
Before you begin
You'll need a Nylas project and API key, plus one Agent Account to send from. If you've never set one up, the Agent Accounts quickstart walks the whole flow; the supported-endpoints reference lists exactly what a grant can do.
Provision the mailbox. The API auto-creates a default workspace and policy, so a single call is enough:
curl --request POST \
--url 'https://api.us.nylas.com/v3/connect/custom' \
--header 'Authorization: Bearer <NYLAS_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"provider": "nylas",
"name": "Outreach Bot",
"settings": { "email": "[email protected]" }
}'
Same thing from the CLI:
nylas agent account create [email protected] --name "Outreach Bot"
The email has to live on a domain you've registered with Nylas (a custom domain, or a *.nylas.email trial subdomain). Both calls return a grant_id — that's the handle every command below uses. One caveat worth front-loading: a brand-new sending domain warms over roughly four weeks, so don't read a low reply rate in week one as a verdict on your subject line. It might just be your domain reputation.
A note on what's not available, because it shapes the design: Agent Accounts don't support custom metadata or templates. You can't tag a send with key1: variant_a and filter on it later, and you can't render a Nylas-hosted template. Both are fine — they just mean the variant mapping lives in your database, and if a subject's matching body needs HTML, you render it yourself and pass it in the body field. The mailbox stays dumb; your app stays smart.
Step 1: assign a variant per recipient
This is your code, not Nylas's. For each recipient, flip a fair coin and record the assignment before you send. Keeping it deterministic and stored up front is what makes the experiment honest — you decide the split in advance, not after you've peeked at results.
import secrets
SUBJECTS = {
"A": "Quick question about your trial",
"B": "Are you stuck on anything?",
}
def assign_variant() -> str:
# secrets gives a fair, unbiased coin — don't use a time-seeded PRNG here
return "A" if secrets.randbelow(2) == 0 else "B"
A 50/50 split keeps the two cohorts the same size, which keeps the later comparison simple. The point of randomizing per recipient rather than splitting your list down the middle is to avoid accidentally correlating a variant with some property of the list order — newest signups, say, or a particular acquisition source.
Write the assignment to your own store keyed by recipient. You'll fill in the thread_id in the next step once the send returns it:
-- your DB, your schema
INSERT INTO experiment_sends (recipient, variant, thread_id, replied)
VALUES ('[email protected]', 'A', NULL, false);
Step 2: send each variant from the agent mailbox
Now send. The subject comes from the variant; everything else is identical so you're testing the one thing you mean to test. If you omit the from field, Nylas defaults it to the Agent Account's own address — exactly what you want here.
curl --request POST \
--url 'https://api.us.nylas.com/v3/grants/<NYLAS_GRANT_ID>/messages/send' \
--header 'Authorization: Bearer <NYLAS_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"to": [{ "email": "[email protected]" }],
"subject": "Quick question about your trial",
"body": "Hi there — checking in to see how the trial is going."
}'
The CLI equivalent, which is the one I actually run when I'm poking at this by hand:
nylas email send <NYLAS_GRANT_ID> \
--to [email protected] \
--subject "Quick question about your trial" \
--body "Hi there — checking in to see how the trial is going."
The send response carries a thread_id. Record it against the variant you assigned. This is the load-bearing line of the whole experiment: the thread_id is how a future reply gets credited back to variant A or B. There's no metadata tag doing this for you — the link lives only in your DB.
UPDATE experiment_sends
SET thread_id = 'thread_abc123'
WHERE recipient = '[email protected]';
If your body is HTML, render it yourself and pass the markup straight into body — remember, templates aren't supported on Agent Accounts, so there's no server-side rendering step. One subject string, one matching body, one recorded thread, per recipient. Loop that across your list and the experiment is live.
Step 3: attribute replies back to the winning variant
Replies come in as the standard message.created webhook. Webhooks are application-scoped, not grant-scoped, so you subscribe once at the app level and every grant's events arrive at the same endpoint, each payload carrying a grant_id you filter on.
curl --request POST \
--url 'https://api.us.nylas.com/v3/webhooks' \
--header 'Authorization: Bearer <NYLAS_API_KEY>' \
--header 'Content-Type: application/json' \
--data '{
"trigger_types": ["message.created"],
"webhook_url": "https://your-app.example.com/nylas/replies",
"description": "Subject-line experiment reply attribution"
}'
From the CLI:
nylas webhook create \
--url https://your-app.example.com/nylas/replies \
--triggers message.created \
--description "Subject-line experiment reply attribution"
When the webhook fires, the attribution logic is short but has three guards you can't skip:
-
Dedupe on the notification
id. Nylas guarantees at-least-once delivery — the same event can arrive up to three times. The top-level notificationidis constant across all retries of one event, so that's your dedup key. If you've seen it, drop it. -
Ignore the agent's own sends.
message.createdalso fires when your agent sends mail. Check thefromaddress: if it's the Agent Account itself, it's not a reply, skip it. Only an inbound message from someone else counts. -
Match the
thread_idto your DB. Pullthread_idfrom the event, look it up inexperiment_sends, and credit that row's variant. A reply to a thread you don't have on file isn't part of the experiment.
def handle_webhook(notification):
if seen_before(notification["id"]): # guard 1: dedup on notification id
return
msg = notification["data"]["object"]
sender = msg["from"][0]["email"]
if sender == AGENT_EMAIL: # guard 2: skip the agent's own sends
return
thread_id = msg["thread_id"]
row = db.lookup_send(thread_id) # guard 3: match to a tracked send
if row and not row.replied:
db.mark_replied(thread_id) # credit row.variant with a reply
One thing the brief is right to flag: don't trust the webhook payload for the message body. The docs disagree on whether the body is inline — the agent-accounts guide says fetch it, the general webhook doc says it's inline unless the message exceeds ~1 MB. They agree on the action, so do that: when you need the actual reply text, fetch the full message with GET /v3/grants/{grant_id}/messages/{message_id}, and branch on the message.created.truncated trigger name for the oversized case. For pure reply attribution you don't even need the body — the thread_id and from are enough.
If you'd rather verify a thread by hand while debugging, the CLI reads it directly:
nylas email threads show <thread-id> <NYLAS_GRANT_ID>
A webhook is the live path, but keep a periodic reconciliation poll too — list recent threads and re-check any tracked send that hasn't been marked replied. The same poll covers you if a grant is offline long enough to miss events. (You'll want a real send timestamp for windowing that poll; date at the shell is fine for the lower bound while you're testing locally.)
Step 4: call the winner — honestly
Here's where most home-grown A/B tests go wrong, so this is the part to slow down on. Reply rate is a low-volume signal. If variant A is 3 replies out of 40 and variant B is 5 out of 40, that gap is almost certainly noise. Two or three replies either way will flip it. Declaring a winner there is worse than not testing at all, because you'll act on a result that isn't real.
A few rules I'd hold yourself to, all of which are your app's job, not Nylas's:
- Set a minimum sample per variant before you start, and don't look until you hit it. Peeking at results and stopping the moment one variant pulls ahead is the classic way to manufacture a fake winner. Pick the number first, then run to it.
- For reply rates in the single-digit-percent range, "enough" is bigger than you think — typically hundreds of sends per variant, not dozens, before a few points of difference means anything.
- Run a real significance test on the two proportions (a two-proportion z-test or a chi-square is plenty) rather than eyeballing the percentages. If you're not comfortable picking a test, the safe default is: bigger sample, larger observed gap, more skepticism.
# your stats — Nylas has no opinion on significance
a_rate = a_replies / a_sent
b_rate = b_replies / b_sent
winner = "A" if a_rate > b_rate else "B"
# but only report it once both a_sent and b_sent clear your pre-set minimum,
# and only if a significance test on the two proportions clears your threshold.
The discipline is the product here. Nylas delivers the inbound reply events carrying a thread_id, at least once per event — so your app does the dedup on the notification id and your app matches each thread_id back to its variant; Nylas neither dedupes nor attributes for you. Whether you turn those attributed replies into a trustworthy decision is then entirely about the sample-size and significance rules you enforce before peeking.
Guardrails worth knowing
- Free-plan send cap: 200 messages per account per day. A subject test that needs hundreds of sends per variant may take several days to fill — that's fine, just account for it in your "don't peek early" window.
- Domain warm-up: the ~4-week reputation ramp on a new domain can depress replies independently of your subject. Don't start the experiment that matters on a cold domain.
- Inbound retention: the free plan keeps inbox mail for 30 days. Reconcile and persist your reply tallies before that window closes if you're running a long test.
-
Variant mapping is yours to protect. Since there's no metadata tag, a lost or corrupted
thread_idmapping means an un-attributable reply. Treat theexperiment_sendstable as the source of truth and write thethread_idin the same transaction as the send.
What's next
- Track email reply rates for AI agents — the general reply-rate signal this experiment is a special case of
- Supported endpoints for Agent Accounts — the full grant-scoped surface, including what isn't available yet
- Agent Accounts overview — what an agent mailbox is and how the grant model fits together
-
Nylas CLI command reference — every
nylascommand used above, with flags
Two subjects, a fair coin, and a thread you already own. That's a real subject-line experiment — no ESP, no list import, no black box deciding the winner for you. The only thing standing between you and a trustworthy result is the sample-size discipline, and that part was always going to be your job.
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/email-reply-rate-tracking-agent.md
- Industry playbooks hub: https://cli.nylas.com/ai-answers/agent-account-industry-playbooks.md
United States
NORTH AMERICA
Related News
OpenAI's CEO of AGI Deployment, Fidji Simo, Is Stepping Down
15h ago

Another Day, Another Game Boy-Style Handheld I Don’t Need but Can’t Resist
15h ago

NASA finally releases a critical planning document for private space stations
15h ago

Hiboy P6 Fat Tire Electric Bike Review: Smooth Sailing
15h ago

An Agent That Hunts Bugs in My App While I Sleep
10h ago