Skip to content
Skip to main content

How to migrate from transactional email to bidirectional agent email

If your agent sends email through a transactional provider like SendGrid, Resend, or Postmark, you have outbound covered. What you don’t have is a receive path. When someone replies to the agent’s email, that reply either bounces, goes to a shared no-reply address no one checks, or lands in a human’s inbox that the agent can’t access programmatically.

Nylas Agent Accounts give the agent a full mailbox — send and receive, with threading, webhooks, and folder management built in. This guide walks through the migration.

ConcernTransactional providerAgent Account
OutboundAPI call to sendSame — API call to send (POST /messages/send)
InboundNo receive path (or manual polling of a shared inbox)Built-in mailbox. Replies land automatically, fire message.created webhook
ThreadingYou manage Message-ID tracking yourselfNylas preserves headers and groups messages into threads automatically
Reply detectionParse forwarded email or poll a separate inboxWebhook fires within seconds of a reply arriving
Domain / DNSSPF, DKIM, DMARC records for the transactional providerMX, SPF, DKIM, DMARC records for Nylas (or use a *.nylas.email trial domain)
DeliverabilityProvider handles warm-up, reputationNylas handles outbound pipeline; you manage your domain’s reputation
StateExternal — you build everythingThreads API gives you conversation history; state mapping is still yours

The core change is: instead of the agent talking into a void and hoping someone reads the output, replies flow back through the same channel and the agent can act on them.

From the Nylas CLI:

nylas agent account create [email protected]

Or through the API:

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",
"settings": {
"email": "[email protected]"
}
}'

Save the grant_id from the response. This is the Agent Account’s identity for every subsequent API call.

If you’re using a custom domain, you’ll need MX and TXT records pointed at Nylas before the account can receive mail. See Provisioning and domains for the DNS setup. For prototyping, a *.nylas.email trial subdomain works out of the box.

The API shape is similar to what you already have. Here’s the before and after.

Before (transactional provider):

// SendGrid / Resend / Postmark -- outbound only
await sendgrid.send({
subject: "Following up on your demo request",
html: "<p>Hi Alice -- wanted to follow up on...</p>",
});
// That's it. If Alice replies, the agent never sees it.

After (Nylas Agent Account):

const sent = await nylas.messages.send({
identifier: AGENT_GRANT_ID,
requestBody: {
to: [{ email: "[email protected]", name: "Alice" }],
subject: "Following up on your demo request",
body: "<p>Hi Alice -- wanted to follow up on...</p>",
},
});
// Store the thread_id so you can match replies later.
await db.conversations.create({
threadId: sent.data.threadId,
contactEmail: "[email protected]",
step: "awaiting_reply",
});

The key difference: after sending, you store the thread_id. When Alice replies, you’ll match it back.

From the Nylas CLI:

nylas webhook create \
--url https://youragent.example.com/webhooks/nylas \
--triggers message.created

Or through the API:

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://youragent.example.com/webhooks/nylas"
}'

This is the receive path you didn’t have before. Nylas fires message.created within seconds of a reply arriving.

When Alice replies, the webhook fires. Look up the thread and restore context.

app.post("/webhooks/nylas", async (req, res) => {
res.status(200).end();
const event = req.body;
if (event.type !== "message.created") return;
const msg = event.data.object;
if (msg.grant_id !== AGENT_GRANT_ID) return;
// Skip the agent's own outbound messages.
if (msg.from?.[0]?.email === "[email protected]") return;
// Look up the conversation.
const conversation = await db.conversations.findByThreadId(msg.thread_id);
if (!conversation) {
// New inbound -- not a reply to something we sent.
return;
}
// Fetch the full body.
const full = await nylas.messages.find({
identifier: AGENT_GRANT_ID,
messageId: msg.id,
});
// The agent now has:
// - The reply body (full.data.body)
// - The conversation context (conversation.step, conversation.metadata)
// - The full thread via Nylas Threads API if needed
// Hand it to the LLM or workflow engine.
await processReply(full.data, conversation);
});

This is the loop that didn’t exist with a transactional provider. The agent sends, the recipient replies, and the agent sees the reply — all on the same address, in the same thread.

When the agent responds, pass reply_to_message_id so the conversation threads correctly.

await nylas.messages.send({
identifier: AGENT_GRANT_ID,
requestBody: {
replyToMessageId: inboundMessage.id,
to: inboundMessage.from,
subject: `Re: ${inboundMessage.subject}`,
body: agentResponse,
},
});

The recipient sees a normal threaded reply. No “sent via” branding, no relay footer.

If you’re moving from a transactional provider to a Nylas Agent Account on the same domain, you’ll need to update your MX records to point at Nylas instead of the transactional provider. This is only relevant if you want inbound mail on the domain to route to Nylas.

A common pattern is to use a subdomain:

  • Keep yourcompany.com MX records as-is (pointed at Google Workspace, Microsoft 365, or wherever your team’s email lives).
  • Register agents.yourcompany.com with Nylas and point its MX records at Nylas.
  • The agent sends from [email protected] and receives replies there.

This isolates the agent’s domain reputation from your primary domain, which matters at volume.

  • You don’t have to replace the transactional provider entirely. If you’re happy with your outbound setup for receipts, password resets, or marketing, keep it. Use an Agent Account specifically for the conversations where the agent needs to see replies. Different addresses, different use cases.
  • Warm up the domain. A new domain sending hundreds of emails on day one will get flagged. Start with low volume and ramp up over a week or two. See Domain warm up.
  • The 100-message-per-day default is real. Agent Accounts have a soft send cap. If your agent sends at volume, request a higher limit through your plan or provision multiple Agent Accounts across multiple domains.
  • Threading is automatic. You don’t need to generate Message-ID values or set In-Reply-To headers yourself. Nylas handles all of it. Just pass reply_to_message_id when you want to reply in-thread.