DEV Community

Give your AI agent its own inbox so it can sign itself up

Provision the throwaway inbox

This is the only step that's specific to Agent Accounts. From the CLI it's one line:

nylas agent account create signup-agent@agents.yourcompany.com

The command prints the new grant's id, status, and connector details. Save that id as AGENT_GRANT_ID - it's the handle for everything that follows.

The create command also takes --name for a display name on outbound mail, and --app-password if you want IMAP/SMTP access so a human can peek at the mailbox from a normal mail client while debugging:

nylas agent account create signup-agent@agents.yourcompany.com \
  --name "Onboarding Agent" \
  --app-password "MySecureP4ssword!2024"

There's deliberately no --workspace flag on create - the API auto-creates a default workspace and policy for the account, and if you want to attach a stricter custom policy you do that afterward with nylas workspace update <workspace-id> --policy-id <policy-id>.

The CLI is a thin wrapper over POST /v3/connect/custom. Here's the same call your agent makes directly. The display name is a top-level name; the app password, if you want one, goes inside settings (18-40 printable ASCII characters with at least one uppercase letter, one lowercase letter, and one digit - the example below satisfies that):

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": "Onboarding Agent",
    "settings": {
      "email": "signup-agent@agents.yourcompany.com",
      "app_password": "MySecureP4ssword!2024"
    }
  }'

The app password is optional - drop it and you get an API-only mailbox, which is all an autonomous agent actually needs. Include it only if a human is going to connect a mail client to watch what the agent receives.

The "provider": "nylas" is what tells the API this is an Agent Account rather than an OAuth grant - that's why there's no refresh token in the body.

The response carries the grant:

{
  "request_id": "5967ca40-a2d8-4ee0-a0e0-6f18ace39a90",
  "data": {
    "id": "b1c2d3e4-5678-4abc-9def-0123456789ab",
    "provider": "nylas",
    "grant_status": "valid",
    "email": "signup-agent@agents.yourcompany.com",
    "name": "Onboarding Agent",
    "scope": [],
    "created_at": 1742932766
  }
}

data.id is your grant_id. For a per-run agent, this whole step happens at the start of the task: provision a fresh mailbox, do the work, delete it at the end. One inbox per run.

Trigger the signup

This step lives entirely in your own code, so I'll keep it conceptual. Your agent fills in the target service's signup form using the Agent Account's address as the email. Whatever shape that takes - a direct API call if the service exposes one, a headless browser step with Playwright for a real form, or a plain fetch POST - the only thing that matters here is that the email field is signup-agent@agents.yourcompany.com.

await fetch("https://saas-you-care-about.example.com/signup", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    email: "signup-agent@agents.yourcompany.com",
    name: "Onboarding Agent",
  }),
});

The service does what it always does: it sends a verification email to that address. Now the address is one your agent can actually read.

Receive the verification email

There are two ways to pick up the inbound message, and which one you use depends on how your agent is shaped.

Poll the messages endpoint

If your agent is a straight-line script - submit the form, wait, read the inbox - polling is the simplest thing that works. List the mailbox and filter by sender and subject so you're not parsing a "Welcome" email by mistake.

The grant-scoped Messages endpoint is GET /v3/grants/{grant_id}/messages, and it takes standard query filters like from and subject (URL-encode the values):

curl --request GET \
  --url "https://api.us.nylas.com/v3/grants/<AGENT_GRANT_ID>/messages?limit=5&from=no-reply@saas-you-care-about.example.com&subject=verify" \
  --header "Authorization: Bearer <NYLAS_API_KEY>"

One thing to know: provider-specific native search syntax (search_query_native) doesn't apply to Agent Accounts, and full-text body search isn't available on them yet. Stick to the structured filter params - from, subject, and friends - and do the fine-grained matching (the exact code or link) yourself once you've fetched the body.

From the terminal, nylas email list and nylas email search do the same thing with friendlier flags. To watch the agent's inbox while you debug:

# Everything in the inbox, newest first
nylas email list signup-agent@agents.yourcompany.com --limit 5

# Just mail from the service you signed up with
nylas email list signup-agent@agents.yourcompany.com \
  --from no-reply@saas-you-care-about.example.com

nylas email search is the better fit when you want to match on subject as well, which is exactly the discriminator you want for verification mail:

nylas email search "verify" signup-agent@agents.yourcompany.com \
  --from no-reply@saas-you-care-about.example.com \
  --subject "verify your email"

Both list and search take the grant ID (or the account email) as a positional argument, so the same command works against any Agent Account you've provisioned. Add --json if you're piping the output into something.

Subscribe to the webhook

Polling is fine for a script, but if your agent is a long-running service, you don't want it spinning on the Messages endpoint waiting for mail that arrives whenever the SaaS gets around to sending it. Inbound mail to an Agent Account fires the standard message.created webhook, same as any other grant - so subscribe to it and let Nylas push.

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

The equivalent API call:

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/signup",
    "description": "Agent signup verification"
  }'

Nylas fires message.created within a second or two of mail landing. The payload carries the message's summary fields - id, grant_id, from, subject, snippet, date - but not the full body. So your handler filters on the summary, then fetches the body for the message it cares about.

(Agent Accounts also emit message.delivered, message.bounced, and message.complaint deliverability webhooks, which are handy when you're debugging outbound mail, but for signup you only need message.created.)

app.post("/webhooks/signup", async (req, res) => {
  // Verify the X-Nylas-Signature header here before trusting anything.
  res.status(200).end();

  const event = req.body;
  if (event.type !== "message.created") return;

  const { grant_id, id: messageId, from, subject } = event.data.object;
  if (grant_id !== AGENT_GRANT_ID) return;

  const sender = from?.[0]?.email ?? "";
  if (!sender.endsWith("@saas-you-care-about.example.com")) return;
  if (!/verif|confirm|code/i.test(subject ?? "")) return;

  await handleVerification(messageId);
});

Ack the webhook immediately with a 200, then do the work. Webhook handlers run on short-lived processes - don't block the response on a body fetch.

Extract the OTP or verification link

Now read the actual message. The webhook only gave you summary fields, so pull the full body from GET /v3/grants/{grant_id}/messages/{message_id}:

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

From the terminal, nylas email read prints the full message - and I lean on this constantly while building the regex, because seeing the actual body of the verification email is the only reliable way to know what you're parsing. The --raw flag shows the body without HTML processing, which is what you want when your parser runs on the raw content:

nylas email read <MESSAGE_ID> signup-agent@agents.yourcompany.com --raw

Whether it's a link or a code, the extraction is regex on the body. For a confirmation link:

async function handleVerification(messageId) {
  const resp = await fetch(
    `https://api.us.nylas.com/v3/grants/${AGENT_GRANT_ID}/messages/${messageId}`,
    { headers: { Authorization: `Bearer ${NYLAS_API_KEY}` } }
  );
  const { data: message } = await resp.json();

  // Pattern 1: a confirmation link.
  const link = /https:\/\/saas-you-care-about\.example\.com\/confirm\?token=[^"\s<]+/.exec(
    message.body
  );
  if (link) return completeSignup(link[0]);

  // Pattern 2: a one-time code. Strip HTML first so the regex sees plain text.
  const text = stripHtml(message.body);
  const patterns = [
    /(?:code|passcode|one[\s-]?time)[^\d]{0,20}(\d{4,8})/i, // "Your code is: 123456"
    /\b(\d{6})\b/, // bare 6-digit
    /\b(\d{4,8})\b/, // 4-8 digit, last resort
  ];
  for (const p of patterns) {
    const m = p.exec(text);
    if (m) return submitOtp(m[1]);
  }
}

Regex-first is the right default. A handful of patterns cover the overwhelming majority of verification emails, they're cheap, and they don't hallucinate.

The CLI does ship two AI email features, but neither one extracts codes: nylas email ai analyze summarizes and triages a batch of recent mail, and nylas email smart-compose drafts new mail from a prompt. One reads your inbox at a high level, the other writes outbound messages - neither pulls a verification code out of a body, so regex stays the right tool here. If you hit a service whose template is genuinely too messy for regex, that's the moment to pass the stripped body to a s

Comments

No comments yet. Start the discussion.