# How to migrate from transactional email to bidirectional agent email

Source: https://developer.nylas.com/docs/cookbook/agent-accounts/migrate-from-transactional-email/

If your agent sends 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, that reply either bounces, lands at a no-reply nobody reads, or goes to a human inbox the agent can't access programmatically. The agent is sending into a void.

Nylas Agent Accounts give the agent a full mailbox — send *and* receive, with threading, webhooks, and folder management built in. This recipe is the migration path.

## What changes and what doesn't

| Concern | Transactional provider | Agent Account |
| --- | --- | --- |
| **Outbound** | API call to send | Same -- API call to send (`POST /messages/send`) |
| **Inbound** | No receive path (or manual polling of a shared inbox) | Built-in mailbox. Replies land automatically, fire `message.created` webhook |
| **Threading** | You manage `Message-ID` tracking yourself | Nylas preserves headers and groups messages into threads automatically |
| **Reply detection** | Parse forwarded email or poll a separate inbox | Webhook fires within seconds of a reply arriving |
| **Domain / DNS** | SPF, DKIM, DMARC records for the transactional provider | MX, SPF, DKIM, DMARC records for Nylas (or use a `*.nylas.email` trial domain) |
| **Deliverability** | Provider handles warm-up, reputation | Nylas handles outbound pipeline; you manage your domain's reputation |
| **State** | External -- you build everything | Threads 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.

## Step 1: Provision the Agent Account

From the [Nylas CLI](https://cli.nylas.com/):

```bash
nylas agent account create outreach@agents.yourcompany.com
```

Or through the API:

```bash
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": "outreach@agents.yourcompany.com"
    }
  }'
```

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 [Setup domains](/docs/v3/agent-accounts/dns-provider-setup/) for the DNS setup. For prototyping, a `*.nylas.email` trial subdomain works out of the box.

## Step 2: Replace the send call

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

**Before (transactional provider):**

```js
// SendGrid / Resend / Postmark -- outbound only
await sendgrid.send({
  to: "prospect@example.com",
  from: "outreach@yourcompany.com",
  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):**

```js
const sent = await nylas.messages.send({
  identifier: AGENT_GRANT_ID,
  requestBody: {
    to: [{ email: "prospect@example.com", 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: "prospect@example.com",
  step: "awaiting_reply",
});
```

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

## Step 3: Subscribe to inbound

From the CLI:

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

Or through the API:

```bash
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.

## Step 4: Handle replies

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

```js
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 === "outreach@agents.yourcompany.com") 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.

## Step 5: Reply in-thread

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

```js
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.

## DNS considerations

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 `outreach@agents.yourcompany.com` and receives replies there.

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

## Things to know

- **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](/docs/v3/agent-accounts/domain-warming/).
- **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.
- **Don't ship the receive path without dedup.** Webhooks are at-least-once. The same `message.created` notification can arrive twice — see [Prevent duplicate agent replies](/docs/cookbook/agent-accounts/prevent-duplicate-replies/) before going live.

## Next steps

- [Handle email replies in an agent loop](/docs/cookbook/agent-accounts/handle-replies/) — the detailed reply detection and routing recipe
- [Build a multi-turn email conversation](/docs/cookbook/agent-accounts/multi-turn-conversations/) — the full send-receive-respond loop with state management
- [Prevent duplicate agent replies](/docs/cookbook/agent-accounts/prevent-duplicate-replies/) — dedup, locking, and rate limiting for the new receive path
- [Email threading for agents](/docs/v3/agent-accounts/email-threading/) — how threading headers work and how Nylas preserves them
- [Setup domains](/docs/v3/agent-accounts/dns-provider-setup/) — register a custom domain and publish its DNS records