<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Qasim Muhammad</title>
    <description>The latest articles on DEV Community by Qasim Muhammad (@qasim157).</description>
    <link>https://dev.to/qasim157</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3837851%2F1a2b79c0-c959-45ef-b215-a68515f17bef.jpg</url>
      <title>DEV Community: Qasim Muhammad</title>
      <link>https://dev.to/qasim157</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/qasim157"/>
    <language>en</language>
    <item>
      <title>One Agent Identity Per Customer: Multi-Tenant Email</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:53:32 +0000</pubDate>
      <link>https://dev.to/qasim157/one-agent-identity-per-customer-multi-tenant-email-2m4m</link>
      <guid>https://dev.to/qasim157/one-agent-identity-per-customer-multi-tenant-email-2m4m</guid>
      <description>&lt;p&gt;Provisioning a tenant-scoped email identity for your SaaS is one POST:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/connect/custom"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "provider": "nylas",
    "workspace_id": "&amp;lt;WORKSPACE_ID&amp;gt;",
    "settings": {
      "email": "scheduling@customer-a.com"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No OAuth dance, no refresh token — just an address on a registered domain. The response comes back already valid:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"request_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"5967ca40-a2d8-4ee0-a0e0-6f18ace39a90"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"data"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"b1c2d3e4-5678-4abc-9def-0123456789ab"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"provider"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"nylas"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"grant_status"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"valid"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"email"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"scheduling@customer-a.com"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"scope"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[],&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"created_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="mi"&gt;1742932766&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;data.id&lt;/code&gt; is a &lt;code&gt;grant_id&lt;/code&gt; that works with every existing Nylas endpoint, and the account is live immediately. That's the primitive behind a multi-tenant pattern worth knowing: one &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/" rel="noopener noreferrer"&gt;Agent Account&lt;/a&gt; per customer, on each customer's own verified domain, all managed from a single application. (Agent Accounts are in beta, so the surface may shift before GA.)&lt;/p&gt;

&lt;h2&gt;
  
  
  The architecture in one paragraph
&lt;/h2&gt;

&lt;p&gt;Your app runs &lt;code&gt;scheduling@customer-a.com&lt;/code&gt;, &lt;code&gt;scheduling@customer-b.com&lt;/code&gt;, and so on — same code path, different identities. Each account has its own policy, its own send quota, and its own sender reputation. A single application can manage accounts across an &lt;em&gt;unlimited&lt;/em&gt; number of registered domains, so tenant count is a billing question, not an architectural one. Customer A's deliverability problems stay Customer A's; nothing they do contaminates Customer B's mail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Domains: register once, mint accounts forever
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/provisioning/" rel="noopener noreferrer"&gt;provisioning docs&lt;/a&gt; lay out two domain strategies you can mix freely in one application:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Strategy&lt;/th&gt;
&lt;th&gt;Address format&lt;/th&gt;
&lt;th&gt;Setup&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Trial domain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alias@&amp;lt;your-application&amp;gt;.nylas.email&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;None — instant&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Your own domain&lt;/td&gt;
&lt;td&gt;&lt;code&gt;alias@yourdomain.com&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;MX + TXT records at the DNS provider&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;For the per-customer pattern, each tenant brings their domain. You register it once per organization (picking the US or EU data center region), the customer publishes the MX record (routes inbound to the platform) and TXT records (ownership proof plus SPF/DKIM for outbound), and verification flips to &lt;code&gt;verified&lt;/code&gt; automatically once DNS propagates. From then on you create as many accounts under it as your plan allows.&lt;/p&gt;

&lt;p&gt;Two field-tested recommendations from the docs: prototype on &lt;code&gt;*.nylas.email&lt;/code&gt; and move to custom domains before launch, and prefer a dedicated subdomain like &lt;code&gt;agents.customer-a.com&lt;/code&gt; so agent sender reputation is isolated from the customer's primary marketing domain. The same mechanism handles environment separation — &lt;code&gt;agents.staging.yourcompany.com&lt;/code&gt; next to &lt;code&gt;agents.yourcompany.com&lt;/code&gt; on one application keeps staging traffic off the production domain. High-volume senders sometimes go further and shard outbound across &lt;code&gt;sales-a.yourcompany.com&lt;/code&gt;, &lt;code&gt;sales-b.yourcompany.com&lt;/code&gt; purely for reputation isolation.&lt;/p&gt;

&lt;h2&gt;
  
  
  Workspaces are the tenant boundary
&lt;/h2&gt;

&lt;p&gt;Notice the &lt;code&gt;workspace_id&lt;/code&gt; in the request up top. Policies and rules — send limits, spam detection, retention, inbound filtering — apply through workspaces, not individual grants. Place each tenant's accounts in their own workspace and the whole tenant inherits its policy in one move.&lt;/p&gt;

&lt;p&gt;The placement rules are worth knowing precisely:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Pass &lt;code&gt;workspace_id&lt;/code&gt; explicitly and the account lands there, picking up that workspace's limits, spam settings, and rules.&lt;/li&gt;
&lt;li&gt;Omit it, and the account is auto-grouped into a workspace whose &lt;code&gt;domain&lt;/code&gt; matches the email address (when &lt;code&gt;auto_group&lt;/code&gt; is enabled), or falls back to your application's default workspace.&lt;/li&gt;
&lt;li&gt;Move an existing account later with &lt;code&gt;PATCH /v3/grants/{grant_id}&lt;/code&gt; and a new &lt;code&gt;workspace_id&lt;/code&gt;.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For multi-tenant SaaS, that auto-group behavior is a nice default: accounts on &lt;code&gt;customer-a.com&lt;/code&gt; cluster together without bookkeeping on your side. But explicit &lt;code&gt;workspace_id&lt;/code&gt; per tenant is the predictable choice once policies differ between customers.&lt;/p&gt;

&lt;h2&gt;
  
  
  Fleet operations without leaving the terminal
&lt;/h2&gt;

&lt;p&gt;The API call above is what your provisioning service runs; for day-to-day operations across tenants, the CLI exposes the same lifecycle:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent account create scheduling@customer-a.com
nylas agent account list &lt;span class="nt"&gt;--json&lt;/span&gt;
nylas agent account get scheduling@customer-a.com
nylas agent status              &lt;span class="c"&gt;# connector readiness&lt;/span&gt;
nylas agent policy list         &lt;span class="c"&gt;# policies attached to accounts&lt;/span&gt;
nylas agent account delete scheduling@customer-a.com &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Agent Accounts also show up in &lt;code&gt;nylas auth list&lt;/code&gt; alongside connected OAuth grants, which is a useful reminder of the design: to the rest of the platform, a tenant's agent is just another grant. There's a Dashboard path too (&lt;strong&gt;Agent Accounts → Accounts → Create account&lt;/strong&gt;), handy for support staff who need to inspect a tenant's inbox without shipping code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quotas and the optional human door
&lt;/h2&gt;

&lt;p&gt;Per-tenant quota math starts from the platform defaults: 200 messages per account per day on the free plan (paid plans have no daily cap by default), and a stricter per-workspace quota can be set through a policy when a tenant's use case warrants it. Storage runs 3 GB per organization on the free plan, with more on paid tiers.&lt;/p&gt;

&lt;p&gt;If a tenant wants their staff to supervise the agent's mailbox from Outlook or Apple Mail, set an &lt;code&gt;app_password&lt;/code&gt; at creation — 18–40 printable ASCII characters with at least one uppercase letter, one lowercase letter, and one digit. It's bcrypt-hashed on write (you can reset it, never read it back), and without it, IMAP/SMTP access simply stays disabled. That's a sensible per-tenant toggle: API-only for most, protocol access for the customers who ask.&lt;/p&gt;

&lt;h2&gt;
  
  
  Verifying a tenant is live
&lt;/h2&gt;

&lt;p&gt;After provisioning, the smoke test is satisfyingly boring: send a test email to the new address from any external client, then list the mailbox —&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; GET &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/messages?limit=5"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you've registered a &lt;code&gt;message.created&lt;/code&gt; webhook, the notification arrives as mail lands, shaped identically to the same event for any connected grant — branch on &lt;code&gt;provider: "nylas"&lt;/code&gt; when one handler serves both kinds of accounts.&lt;/p&gt;

&lt;h2&gt;
  
  
  Two questions that come up in design reviews
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What happens if a tenant's domain verification stalls?&lt;/strong&gt; Nothing breaks — the domain just stays unverified and you can't mint accounts on it yet. Registration is per-organization and verification flips automatically when DNS propagates, so the onboarding flow should poll domain status rather than assume it. Until then, the tenant can run on your trial domain.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Can we change a tenant's policy without touching accounts?&lt;/strong&gt; Yes — that's the point of routing policy through workspaces. Swap or edit the workspace's policy and every Agent Account in it inherits the change; nothing is configured per-grant.&lt;/p&gt;

&lt;p&gt;The end-to-end tenant onboarding flow — register domain, wait for &lt;code&gt;verified&lt;/code&gt;, create workspace, provision account, smoke-test — is automatable from your existing provisioning code, and the trial-domain path lets you build it before any customer DNS exists. Sketch yours as a single idempotent &lt;code&gt;onboardTenant(domain, alias)&lt;/code&gt; function and see how far one afternoon gets you.&lt;/p&gt;

</description>
      <category>saas</category>
      <category>email</category>
      <category>api</category>
      <category>architecture</category>
    </item>
    <item>
      <title>Voice Agents That Follow Up by Email</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:53:25 +0000</pubDate>
      <link>https://dev.to/qasim157/voice-agents-that-follow-up-by-email-5ej6</link>
      <guid>https://dev.to/qasim157/voice-agents-that-follow-up-by-email-5ej6</guid>
      <description>&lt;p&gt;Last sprint, a team I talked to demoed a voice agent that handled support calls impressively — right up until a caller asked "can you email me those instructions?" and the room went quiet. The agent could talk about the docs. It had no address to send them from. The workaround on the whiteboard afterwards was grim: relay through a shared &lt;code&gt;noreply@&lt;/code&gt;, lose the replies, reconcile threads manually in the ticketing system.&lt;/p&gt;

&lt;p&gt;Voice agents hit this wall constantly, because phone calls generate follow-up artifacts — reset instructions, documents, meeting recaps — and email is how callers expect to receive them. The clean fix is the same one that works for text agents: the voice agent gets its own mailbox.&lt;/p&gt;

&lt;h2&gt;
  
  
  The identity half
&lt;/h2&gt;

&lt;p&gt;A Nylas &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/" rel="noopener noreferrer"&gt;Agent Account&lt;/a&gt; is a hosted mailbox you create through the API — Agent Accounts are in beta — and the voice use case from the product docs is exactly the scenario above: a voice agent taking support calls sends documents, reset instructions, or meeting recaps from its own &lt;code&gt;voice-agent@yourcompany.com&lt;/code&gt; address the moment the caller asks. The part that makes it more than a send pipe: when the caller replies, the reply returns through the same account, so the full conversation is one thread in one mailbox. The phone call and its written follow-ups stop living in separate systems.&lt;/p&gt;

&lt;p&gt;Each account is a real grant with a &lt;code&gt;grant_id&lt;/code&gt; that works against the existing Messages, Threads, and Webhooks endpoints, ships with six system folders, and sends up to 200 messages per account per day on the free plan.&lt;/p&gt;

&lt;h2&gt;
  
  
  The plumbing half
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.nylas.com/docs/cookbook/cli/connect-voice-agents/" rel="noopener noreferrer"&gt;voice agents recipe&lt;/a&gt; covers how the runtime actually calls email tools. The flow is the same regardless of vendor:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;speech → STT → LLM (function-calling) → subprocess(nylas …) → JSON → LLM → TTS → speech
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The LLM decides on a tool, the runtime spawns a Nylas CLI subprocess with &lt;code&gt;--json&lt;/code&gt;, the result comes back, and the model composes a spoken response. On LiveKit, a tool is just a decorated function:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;livekit.agents&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;function_tool&lt;/span&gt;
&lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;

&lt;span class="nd"&gt;@function_tool&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;list_recent_emails&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;int&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;str&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="sh"&gt;"""&lt;/span&gt;&lt;span class="s"&gt;List the last few emails. Keep limit small for voice.&lt;/span&gt;&lt;span class="sh"&gt;"""&lt;/span&gt;
    &lt;span class="n"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;subprocess&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;run&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;nylas&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;email&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;list&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--limit&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nf"&gt;str&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;limit&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;--json&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
        &lt;span class="n"&gt;capture_output&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;text&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="bp"&gt;True&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;timeout&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;stdout&lt;/span&gt; &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;out&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;returncode&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="k"&gt;else&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Could not fetch emails.&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Vapi is the same idea over webhooks — Vapi posts JSON to your backend when the LLM calls a tool, your handler executes the CLI, and you return &lt;code&gt;stdout&lt;/code&gt; in Vapi's envelope:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/vapi/tools&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;parameters&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;nylas&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;list&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--limit&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parameters&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;limit&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
                &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;--json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;execAsync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;args&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;timeout&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;30000&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;results&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt;
      &lt;span class="na"&gt;toolCallId&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;toolCall&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;result&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;stdout&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;}],&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Retell, Bland.ai, and OpenAI Realtime all follow the generic define-schema, dispatch-to-subprocess, return-JSON pattern. The recipe is explicit about why this beats running an MCP server next to the voice runtime: voice frameworks expect function-call-style tools that hand back a JSON blob, not a JSON-RPC peer. A side benefit of routing through the CLI: it absorbs every provider difference, so the same tools work whether the grants behind them are Gmail, Microsoft 365, Exchange, Yahoo, iCloud, IMAP — or an Agent Account.&lt;/p&gt;

&lt;h2&gt;
  
  
  Voice surfaces every UX mistake immediately
&lt;/h2&gt;

&lt;p&gt;Four rules from the recipe, none optional:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cap lists at 5.&lt;/strong&gt; Reading a 50-message inbox aloud takes minutes. Default &lt;code&gt;--limit 5&lt;/code&gt; and let the caller say "more."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Summarize, don't read.&lt;/strong&gt; Have the LLM produce "You've got three emails from Ada about the contract and a calendar invite from Rin" rather than narrating subject lines.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confirm before send. Always.&lt;/strong&gt; Speech-to-text mishears recipients and subjects in ways that send the wrong mail to the wrong person. The agent speaks the recipient, subject, and gist; only an explicit "yes" triggers the send tool:
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;   AGENT:  "Send to Ada at acme.test, subject 'pricing', body 'I'm in'?"
   USER:   "Yes."
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Translate errors.&lt;/strong&gt; "Error 401: invalid grant" is not a voice response. Map failures to "I couldn't reach email right now — you may need to re-authenticate."&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And one rule that's really an SLA: every subprocess call needs a timeout, and 30 seconds is the right number. Voice users won't wait a minute; the framework's silence detection kicks in and the conversation falls apart. Aim for a round-trip under 2 seconds on the common tools — &lt;code&gt;nylas email list --limit 5 --json&lt;/code&gt; clears that comfortably — and return a graceful spoken fallback when the timeout fires instead of bubbling the exception.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why the dedicated address changes the product
&lt;/h2&gt;

&lt;p&gt;Run the follow-up sends through the agent's own account rather than a borrowed human grant and three things improve at once:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Continuity.&lt;/strong&gt; The caller replies to the recap, the reply lands in the agent's inbox, and the next interaction — voice or email — has the whole history in one thread.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Auditability.&lt;/strong&gt; Every message the agent ever sent is sitting in its sent folder. The recipe separately recommends logging every send (recipient, subject, run ID, approval source) to your own store; the mailbox gives you the ground truth to reconcile against.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-user routing stays sane.&lt;/strong&gt; Voice platforms serving many users need per-user grant routing anyway — pass &lt;code&gt;--api-key&lt;/code&gt; and &lt;code&gt;--grant-id&lt;/code&gt; per command. The agent's outbound identity stays constant while the caller-side grants vary.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Quick answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Can I use MCP instead of subprocess tools?&lt;/strong&gt; If your runtime genuinely speaks MCP — Claude Code does, for example — yes, and the docs cover that path separately. Voice runtimes mostly don't, which is why the recipe defaults to subprocess + &lt;code&gt;--json&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Where does the calendar fit?&lt;/strong&gt; The same subprocess pattern covers &lt;code&gt;nylas calendar events list&lt;/code&gt;, so "do I have anything tomorrow?" is one more decorated function, not a new integration.&lt;/p&gt;

&lt;p&gt;A reasonable first milestone: wire one tool — &lt;code&gt;send_recap&lt;/code&gt; — into your existing voice stack, pointed at an agent address on a trial domain, with the confirm-before-send exchange in the conversation script. Call it yourself, ask for the recap, and reply to the email it sends you. If the reply shows up threaded in the agent's inbox, you've got the loop. What would your voice agent send first — recaps, docs, or reset links?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>voice</category>
      <category>email</category>
      <category>agents</category>
    </item>
    <item>
      <title>How an AI Agent Can Sign Up for a Service on Its Own</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:53:20 +0000</pubDate>
      <link>https://dev.to/qasim157/how-an-ai-agent-can-sign-up-for-a-service-on-its-own-53eb</link>
      <guid>https://dev.to/qasim157/how-an-ai-agent-can-sign-up-for-a-service-on-its-own-53eb</guid>
      <description>&lt;p&gt;An AI agent that can't receive email can't finish a signup form. That one limitation quietly rules out a huge class of autonomous workflows — the research agent that needs a developer account on a data source, the QA agent that registers for a SaaS on every test run, the purchasing agent that needs a buyer profile on a marketplace. Every one of them dies at "we've sent you a verification email."&lt;/p&gt;

&lt;p&gt;The blocker was never the form. Headless browsers fill forms fine. The blocker is that verification emails traditionally route to a human inbox, which puts a human back in a loop that was supposed to have none.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/sign-up-for-a-service/" rel="noopener noreferrer"&gt;Agent Accounts&lt;/a&gt; remove that dependency. The agent gets its own hosted mailbox (the feature is in beta), signs up with that address, catches the verification email via webhook, and completes onboarding by itself. Here's the whole flow, condensed from the cookbook recipe.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provision, subscribe, sign up
&lt;/h2&gt;

&lt;p&gt;Three setup moves. First, create the mailbox — one CLI command, or &lt;code&gt;POST /v3/connect/custom&lt;/code&gt; with &lt;code&gt;"provider": "nylas"&lt;/code&gt; if you'd rather hit the API:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent account create signup-agent@agents.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The API version is the same Bring Your Own Authentication endpoint other providers use — no OAuth refresh token involved:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/connect/custom"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "provider": "nylas",
    "settings": {
      "email": "signup-agent@agents.yourdomain.com"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the grant ID it prints. Second, subscribe to inbound mail:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas webhook create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; https://youragent.example.com/webhooks/signup &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--triggers&lt;/span&gt; message.created
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;message.created&lt;/code&gt; event fires within a second or two of mail arriving, carrying the message's summary fields. The webhook URL has to be publicly reachable over HTTPS; for local development, the recipe recommends VS Code port forwarding or Hookdeck to expose your dev server.&lt;/p&gt;

&lt;p&gt;Third, submit the target service's signup form with the agent's address — a direct API call if the service exposes one, a Playwright/Puppeteer step if it doesn't, or a plain &lt;code&gt;fetch&lt;/code&gt; POST when the endpoint is simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;https://saas-you-care-about.example.com/signup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="na"&gt;method&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;POST&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Content-Type&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;application/json&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="na"&gt;body&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;email&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;signup-agent@agents.yourdomain.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Automation Agent&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="p"&gt;}),&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Catch the verification email
&lt;/h2&gt;

&lt;p&gt;This is the part that used to require a human. Now it's a webhook handler with three filters and a regex:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/signup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message.created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;grant_id&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;id&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;grant_id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;AGENT_GRANT_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Only react to mail from the service you signed up with.&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@saas-you-care-about.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Pull the full body (the webhook carries summary fields only).&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="s2"&gt;`https://api.us.nylas.com/v3/grants/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;AGENT_GRANT_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;/messages/&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;messageId&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;Authorization&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;`Bearer &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;NYLAS_API_KEY&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;message&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;resp&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;()).&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt;
    &lt;span class="sr"&gt;/https:&lt;/span&gt;&lt;span class="se"&gt;\/\/&lt;/span&gt;&lt;span class="sr"&gt;saas-you-care-about&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;example&lt;/span&gt;&lt;span class="se"&gt;\.&lt;/span&gt;&lt;span class="sr"&gt;com&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;confirm&lt;/span&gt;&lt;span class="se"&gt;\?&lt;/span&gt;&lt;span class="sr"&gt;token=&lt;/span&gt;&lt;span class="se"&gt;[^&lt;/span&gt;&lt;span class="sr"&gt;"&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exec&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
      &lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;fetch&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt; &lt;span class="c1"&gt;// Complete the signup.&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For multi-step confirmations, captchas, or OAuth redirects, swap the final &lt;code&gt;fetch&lt;/code&gt; for a headless browser following the link. And if the service sends a code instead of a link, the parsing shape is identical — the &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/extract-otp-code/" rel="noopener noreferrer"&gt;OTP extraction recipe&lt;/a&gt; covers that variant.&lt;/p&gt;

&lt;h2&gt;
  
  
  The judgment calls the code doesn't show
&lt;/h2&gt;

&lt;p&gt;The recipe's "things to know" section is where the real engineering lives:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Don't trust the first message that arrives.&lt;/strong&gt; Lots of services send a "Welcome" email &lt;em&gt;before&lt;/em&gt; the verification email. The handler above matches the sender &lt;em&gt;and&lt;/em&gt; the expected URL pattern before clicking anything — keep both checks.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Lock the inbox down.&lt;/strong&gt; Pair an allow-list of expected &lt;code&gt;from.domain&lt;/code&gt; values with a &lt;code&gt;block&lt;/code&gt; rule for everything else. If the agent's address ever leaks, the mailbox stays clean instead of becoming a spam magnet feeding garbage into your automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Verify webhook signatures.&lt;/strong&gt; The handler above trusts &lt;code&gt;req.body&lt;/code&gt; for brevity; in production, check the &lt;code&gt;X-Nylas-Signature&lt;/code&gt; header on every POST before acting. A signup agent acts on what webhooks tell it, and an unverified webhook endpoint is an unauthenticated entry point into your automation.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reuse or burn — pick deliberately.&lt;/strong&gt; One account can serve many signup runs, or you can provision fresh per run and delete after. If you go per-run, teardown belongs in the happy path &lt;em&gt;and&lt;/em&gt; the failure path:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent account delete signup-agent@agents.yourdomain.com &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Or, over the API, a plain &lt;code&gt;DELETE /v3/grants/&amp;lt;AGENT_GRANT_ID&amp;gt;&lt;/code&gt; — the grant is the account, so deleting one deletes the other. Inactive grants accumulate, and "we'll clean those up later" is how every team ends up with 400 of them.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Know your quota.&lt;/strong&gt; A free-plan account sends up to 200 messages per account per day, and inbound is generous but not infinite. A large test matrix should spread across multiple grants rather than reusing one.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Respect the terms of service.&lt;/strong&gt; Programmatic signup is fine for your own testing and first-party integrations. Scraping third parties is a different conversation — nothing in the platform enforces this, but your legal team will care.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this pattern is bigger than signups
&lt;/h2&gt;

&lt;p&gt;The verification-email catch is one instance of a general capability: the agent has a durable, addressable identity that other systems can send things to. Receipts, password resets, export-ready notifications, "your report is attached" emails — anything a service communicates over email becomes something an agent can act on, because the agent is reachable the same way a person is.&lt;/p&gt;

&lt;p&gt;Some services keep talking after signup, too — follow-up confirmations, "complete your profile" nudges, billing notices. The same webhook handler grows into a full &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/handle-replies/" rel="noopener noreferrer"&gt;reply loop&lt;/a&gt; where the agent reads each message, decides, and responds from the same address it registered with.&lt;/p&gt;

&lt;p&gt;The end-to-end loop — provision, register, verify, act — runs against a trial &lt;code&gt;*.nylas.email&lt;/code&gt; domain with no DNS setup, so the experiment costs you a webhook endpoint and an afternoon. Pick one service your team signs up for manually today (a staging SaaS, an internal tool, a sandbox API) and automate exactly that signup. Which manual verification step would you kill first?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>email</category>
      <category>agents</category>
    </item>
    <item>
      <title>Extract OTP Codes From Email, Automatically</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:53:15 +0000</pubDate>
      <link>https://dev.to/qasim157/extract-otp-codes-from-email-automatically-17l9</link>
      <guid>https://dev.to/qasim157/extract-otp-codes-from-email-automatically-17l9</guid>
      <description>&lt;p&gt;What does your automation do when the login flow it's driving sends a six-digit code instead of a confirmation link? For most teams the honest answer is "a human goes and checks a shared inbox," which is a strange bottleneck to leave in the middle of an otherwise fully automated pipeline.&lt;/p&gt;

&lt;p&gt;There's a cleaner shape: the agent owns the mailbox the code lands in. With a Nylas &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/extract-otp-code/" rel="noopener noreferrer"&gt;Agent Account&lt;/a&gt; — a hosted mailbox controlled entirely through the API, currently in beta — the OTP email arrives, a webhook fires, your handler extracts the code, and whatever orchestrates the login gets it back. No human, no inbox-checking Slack message, no screen-scraping Gmail.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step one: make sure it's the right email
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;message.created&lt;/code&gt; webhook fires on &lt;em&gt;every&lt;/em&gt; inbound message, so the first job is filtering down to the one that actually carries the code. The recipe uses two signals together — sender domain and a subject heuristic:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/webhooks/otp&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;end&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;message.created&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;grant_id&lt;/span&gt; &lt;span class="o"&gt;!==&lt;/span&gt; &lt;span class="nx"&gt;AGENT_GRANT_ID&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;from&lt;/span&gt;&lt;span class="p"&gt;?.[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]?.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;""&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;senderMatches&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sender&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;endsWith&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;@no-reply.example.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;subjectLooksRight&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="sr"&gt;/code|verif|one.&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;time|passcode/i&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;subject&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;senderMatches&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;subjectLooksRight&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;handleOtp&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Neither check alone is enough. Sender-only matching trips on welcome emails from the same domain; subject-only matching trips on anything that mentions "verification."&lt;/p&gt;

&lt;h2&gt;
  
  
  Regex first, LLM second
&lt;/h2&gt;

&lt;p&gt;Most OTP emails follow one of a few shapes: a standalone 4–8 digit number, or a code after a label like "Your code is:". Three patterns, tried in order from most to least specific, cover the vast majority of services:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;patterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;(?:&lt;/span&gt;&lt;span class="sr"&gt;code|passcode|one&lt;/span&gt;&lt;span class="se"&gt;[\s&lt;/span&gt;&lt;span class="sr"&gt;-&lt;/span&gt;&lt;span class="se"&gt;]?&lt;/span&gt;&lt;span class="sr"&gt;time&lt;/span&gt;&lt;span class="se"&gt;)[^\d]{0,20}(\d{4,8})&lt;/span&gt;&lt;span class="sr"&gt;/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="c1"&gt;// "Your code is: 123456"&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(\d{6})\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;        &lt;span class="c1"&gt;// bare 6-digit&lt;/span&gt;
  &lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b(\d{4,8})\b&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;      &lt;span class="c1"&gt;// bare 4–8 digit (last resort)&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One detail that's easy to miss: strip the HTML before matching. Inline styles and hidden tracking pixels are full of digit sequences that will happily satisfy your last-resort pattern.&lt;/p&gt;

&lt;p&gt;When regex strikes out — usually a code buried in a noisy marketing layout — fall back to a small LLM with a deliberately narrow prompt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;extractWithLlm&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;response&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;openai&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;chat&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;completions&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;create&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;model&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;gpt-4o-mini&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;messages&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;system&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;You extract one-time verification codes from email bodies. &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Respond with JSON only: {&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;code&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: &lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;&amp;lt;the code&amp;gt;&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;} or &lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt;
          &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;{&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;code&lt;/span&gt;&lt;span class="se"&gt;\"&lt;/span&gt;&lt;span class="s2"&gt;: null} if no code is present.&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
      &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;role&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;user&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;content&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;plaintext&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;slice&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;4000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="p"&gt;],&lt;/span&gt;
    &lt;span class="na"&gt;response_format&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;json_object&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;parsed&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;response&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;choices&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;].&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;content&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nf"&gt;returnCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;code&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Only the first 4,000 characters of plaintext go in, and the model is asked for one thing in one shape — JSON with a &lt;code&gt;code&lt;/code&gt; field or &lt;code&gt;null&lt;/code&gt;. Don't ask the model to "understand" the email. Banking and enterprise senders sometimes rotate formats across sessions (6 digits, 8 digits, alphanumeric), and the LLM fallback is what absorbs those shifts without a regex update.&lt;/p&gt;

&lt;h2&gt;
  
  
  Getting the code back to whoever's waiting
&lt;/h2&gt;

&lt;p&gt;The signup or login that triggered all this is blocked, waiting. The simplest bridge is a promise registry keyed by a correlation value — session ID, expected sender, run ID — with a timeout (the recipe defaults to 60 seconds):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;awaitCode&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;correlationKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;60&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;delete&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;correlationKey&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
      &lt;span class="nf"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;OTP timeout&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
    &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;pending&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;correlationKey&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;resolve&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;reject&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;timer&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In production, swap the in-memory &lt;code&gt;Map&lt;/code&gt; for a real queue or pub/sub — webhook handlers run on short-lived processes, and a restart between "code arrived" and "code consumed" loses the code.&lt;/p&gt;

&lt;h2&gt;
  
  
  The failure modes that actually bite
&lt;/h2&gt;

&lt;p&gt;The recipe's warning list is the best part, because each item is a production incident in miniature:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Codes expire fast.&lt;/strong&gt; Most services invalidate OTPs in 5–15 minutes. Check &lt;code&gt;message.date&lt;/code&gt; freshness before returning a code — a slow agent will confidently hand back a dead one.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multiple codes in the inbox.&lt;/strong&gt; A stale code from an earlier attempt plus a fresh one means your regex can grab the wrong match. Sort by message timestamp, newest first, always.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Never log the code.&lt;/strong&gt; OTPs are credentials. Log that one was received and returned; never the value.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Back off on failure.&lt;/strong&gt; A tight retry loop requesting code after code looks like an attack from the service's side and gets the agent's address blocked.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Dedup redelivered webhooks.&lt;/strong&gt; Nylas delivers webhooks at least once. A redelivered &lt;code&gt;message.created&lt;/code&gt; can re-trigger extraction and hand a stale code back to a fresh login attempt — the &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/prevent-duplicate-replies/" rel="noopener noreferrer"&gt;duplicate-reply prevention patterns&lt;/a&gt; apply here too.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;There's also a defense you set up before any of this code runs: lock the inbox down at the mail layer. Agent Account &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/policies-rules-lists/" rel="noopener noreferrer"&gt;policies and rules&lt;/a&gt; can constrain inbound so only expected sender domains ever reach the agent's inbox. An OTP mailbox that accepts mail from anyone is an OTP mailbox someone will eventually try to confuse with look-alike messages; an allowlist makes the "match the right email" step mostly a formality.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What about magic links instead of codes?&lt;/strong&gt; Same architecture, different regex — match a URL pattern instead of digits and follow the link instead of returning a value. The &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/sign-up-for-a-service/" rel="noopener noreferrer"&gt;signup recipe&lt;/a&gt; covers that variant.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why fetch the message body separately?&lt;/strong&gt; The &lt;code&gt;message.created&lt;/code&gt; webhook payload only carries summary fields — sender, subject, snippet. The full body comes from &lt;code&gt;GET /v3/grants/{grant_id}/messages/{message_id}&lt;/code&gt;, which is the first call inside the extraction handler.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why not just use the LLM for everything?&lt;/strong&gt; Cost and latency, but mostly determinism. Regex either matches or it doesn't; you want the probabilistic component to be the fallback, not the front door.&lt;/p&gt;

&lt;h2&gt;
  
  
  Where this slots in
&lt;/h2&gt;

&lt;p&gt;OTP extraction is rarely the whole feature — it's the middle step of something bigger, usually an agent &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/sign-up-for-a-service/" rel="noopener noreferrer"&gt;signing up for a third-party service&lt;/a&gt; end to end: provision the mailbox, submit the form, catch the verification, finish onboarding. The link-based variant of verification is the same architecture with a URL regex instead of a digit regex.&lt;/p&gt;

&lt;p&gt;Try this with a service you control first: point a test signup at the agent's address, watch the webhook land, and check which of the three regex tiers actually matched. If you've built OTP extraction before — what's the weirdest code format you've had to parse? I'm collecting nominations.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>email</category>
      <category>api</category>
      <category>agents</category>
    </item>
    <item>
      <title>Ephemeral Inboxes: Spin Up a Mailbox Per Test Run</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:53:10 +0000</pubDate>
      <link>https://dev.to/qasim157/ephemeral-inboxes-spin-up-a-mailbox-per-test-run-48cc</link>
      <guid>https://dev.to/qasim157/ephemeral-inboxes-spin-up-a-mailbox-per-test-run-48cc</guid>
      <description>&lt;p&gt;Two CI workers kick off at the same moment. Both sign up a test user, both poll the shared QA Gmail account for "the" verification email, and worker #7 grabs the message that belonged to worker #12. The test passes. The wrong test. You spend an afternoon staring at a green build that should've been red.&lt;/p&gt;

&lt;p&gt;Shared inboxes are the single biggest source of flakiness in email-dependent E2E tests, and every workaround — catch-all forwarding rules, label rules scoped per PR, OAuth tokens living on the runner — adds another moving part that breaks on its own schedule. The fix is structural: every test gets its own address, on infrastructure your suite provisions and destroys.&lt;/p&gt;

&lt;h2&gt;
  
  
  One wildcard, infinite addresses
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.nylas.com/docs/cookbook/use-cases/build/e2e-email-testing/" rel="noopener noreferrer"&gt;E2E email testing recipe&lt;/a&gt; sets this up with one CLI command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas inbound create e2e
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You get back an inbox ID and a wildcard pattern shaped like &lt;code&gt;e2e-*@yourapp.nylas.email&lt;/code&gt;. From there, each test mints a unique address under the wildcard — &lt;code&gt;e2e-&amp;lt;uuid&amp;gt;@yourapp.nylas.email&lt;/code&gt; — and there's nothing to provision per address. You don't pay or configure per address either; the wildcard is just a convention, so burn UUIDs freely. Mail flows through MX records hosted on the Nylas side, which means zero DNS work in your own zone (the tradeoff: addresses live under &lt;code&gt;*.nylas.email&lt;/code&gt;).&lt;/p&gt;

&lt;p&gt;The Playwright fixture is two pieces — an address minter and a poller:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;test&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;base&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;extend&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;Fixtures&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;testEmail&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({},&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`e2e-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nf"&gt;randomUUID&lt;/span&gt;&lt;span class="p"&gt;()}&lt;/span&gt;&lt;span class="s2"&gt;@yourapp.nylas.email`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;

  &lt;span class="na"&gt;pollInbox&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;testEmail&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;use&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;poll&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;timeoutMs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;&lt;span class="nx"&gt;_000&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;timeoutMs&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
      &lt;span class="k"&gt;while &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nb"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;now&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="nx"&gt;deadline&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;out&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;execSync&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
          &lt;span class="s2"&gt;`nylas inbound messages &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;INBOX_ID&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt; --json --limit 50`&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
        &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;toString&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
        &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;out&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt;
          &lt;span class="nx"&gt;m&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;to&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;t&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;email&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;testEmail&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
        &lt;span class="p"&gt;);&lt;/span&gt;
        &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;match&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
        &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Promise&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nf"&gt;setTimeout&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;r&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;1500&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
      &lt;span class="p"&gt;}&lt;/span&gt;
      &lt;span class="k"&gt;throw&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`Email never arrived for &lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;testEmail&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="p"&gt;};&lt;/span&gt;
    &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;use&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;poll&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A signup test then reads the way you wish it always had:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;signup completes after email verification&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;testEmail&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;pollInbox&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;/signup&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;testEmail&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByLabel&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Password&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hunter2-correct&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByRole&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Create account&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt; &lt;span class="p"&gt;}).&lt;/span&gt;&lt;span class="nf"&gt;click&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Check your inbox&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;pollInbox&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;linkMatch&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/https:&lt;/span&gt;&lt;span class="se"&gt;\/\/[^\s&lt;/span&gt;&lt;span class="sr"&gt;"&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\/&lt;/span&gt;&lt;span class="sr"&gt;verify&lt;/span&gt;&lt;span class="se"&gt;\?[^\s&lt;/span&gt;&lt;span class="sr"&gt;"&amp;lt;&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;linkMatch&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;not&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toBeNull&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;goto&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;linkMatch&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;expect&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getByText&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;Email verified&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;toBeVisible&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Password reset is the same shape. OTP flows swap the link regex for &lt;code&gt;/\b\d{6}\b/&lt;/code&gt; — though watch out for bodies with multiple 6-digit numbers (phone numbers, transaction IDs); match near the label text or use a stricter extraction helper. And when the verification URL lives in an &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt; instead of plain text, parse the HTML rather than regexing the whole body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;cheerio&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;cheerio&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;$&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;cheerio&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;load&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;html&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;link&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;$&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;a:contains("Verify your email")&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;attr&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;href&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Why this is parallel-safe by construction
&lt;/h2&gt;

&lt;p&gt;Playwright runs tests across workers, and with &lt;code&gt;fullyParallel: true&lt;/code&gt; the inbox ID is shared — but the addresses aren't. Each test polls for messages addressed to &lt;em&gt;its&lt;/em&gt; UUID, so the matching logic never sees another worker's mail. No filtering by subject, no "wait until the right message bubbles to the top." Delivery latency is typically under 5 seconds, so the 1.5-second poll interval catches most messages within two iterations; the 30-second default timeout is generous for almost any flow.&lt;/p&gt;

&lt;p&gt;One performance note from the recipe: &lt;code&gt;execSync&lt;/code&gt; blocks the test until the CLI returns. That's fine for most suites, but chatty ones should swap in &lt;code&gt;execAsync&lt;/code&gt; and await in parallel. And if you want a clean inbox between debugging sessions, mark messages read in an &lt;code&gt;afterEach&lt;/code&gt; — otherwise mail just ages out with the standard retention window.&lt;/p&gt;

&lt;h2&gt;
  
  
  When the test needs a full mailbox, not just an address
&lt;/h2&gt;

&lt;p&gt;A wildcard inbox covers assertion-style tests: did the email arrive, does it contain the right link. Some suites need more — an identity that can &lt;em&gt;send&lt;/em&gt;, sign up for a third-party service, and complete onboarding autonomously. That's the &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/sign-up-for-a-service/" rel="noopener noreferrer"&gt;Agent Account flow&lt;/a&gt;: a fully functional, API-controlled mailbox (Agent Accounts are in beta) that your pipeline provisions per run.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent account create signup-agent@agents.yourdomain.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recipe pairs this with a &lt;code&gt;message.created&lt;/code&gt; webhook, which fires within a second or two of mail arriving — your handler matches the expected sender, fetches the full body, extracts the confirmation link, and follows it. Two of its warnings are worth tattooing onto any test-infra design doc:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Don't trust the first message that arrives.&lt;/strong&gt; Plenty of services send a "Welcome" email before the verification email. Match the sender &lt;em&gt;and&lt;/em&gt; the expected URL pattern before acting on anything.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't ship per-run agents without teardown.&lt;/strong&gt; Inactive grants accumulate. Delete on completion &lt;em&gt;or&lt;/em&gt; failure:
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent account delete signup-agent@agents.yourdomain.com &lt;span class="nt"&gt;--yes&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Also practical: a free-plan Agent Account sends up to 200 messages per account per day, so a large test matrix should provision multiple grants rather than hammering one. If your test address ever leaks, an allow-list policy — a list of allowed &lt;code&gt;from.domain&lt;/code&gt; values paired with a &lt;code&gt;block&lt;/code&gt; rule for everything else — keeps the inbox deterministic. And one non-technical warning the recipe makes explicitly: programmatic signup is fine for your own testing and first-party integrations, but check the target service's terms before automating against third parties.&lt;/p&gt;

&lt;h2&gt;
  
  
  Which one do you need?
&lt;/h2&gt;

&lt;p&gt;Rough decision rule: if the test only ever &lt;em&gt;receives&lt;/em&gt; (verification links, OTPs, notification assertions), the wildcard inbound inbox is lighter and faster to adopt. If the test has to &lt;em&gt;act&lt;/em&gt; — send replies, complete a signup conversation, exercise your product's email round-trip — provision an Agent Account per run and tear it down in &lt;code&gt;afterAll&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A reasonable middle ground is reusing one long-lived Agent Account across signup runs instead of provisioning per run — the signup recipe explicitly supports both. Per-run accounts give you perfect isolation; a reused account gives you faster setup and one less teardown path to get wrong. Pick per-run for parallel CI, reused for local development.&lt;/p&gt;

&lt;p&gt;The proof-of-concept costs about ten minutes: run &lt;code&gt;nylas inbound create e2e&lt;/code&gt;, drop the fixture above into your Playwright project, and convert exactly one flaky signup test. Run it with &lt;code&gt;--repeat-each=10&lt;/code&gt; next to the old shared-inbox version and compare failure counts. That diff is the whole argument.&lt;/p&gt;

</description>
      <category>testing</category>
      <category>email</category>
      <category>api</category>
      <category>devops</category>
    </item>
    <item>
      <title>Give Your Scheduling Bot Its Own Calendar</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:53:06 +0000</pubDate>
      <link>https://dev.to/qasim157/give-your-scheduling-bot-its-own-calendar-45b1</link>
      <guid>https://dev.to/qasim157/give-your-scheduling-bot-its-own-calendar-45b1</guid>
      <description>&lt;p&gt;A scheduling link makes the human do the work; a scheduling agent with its own calendar does the negotiating. Booking pages outsource the back-and-forth to a UI. The agent model keeps it where it already happens — in email — and answers from a real address with a real calendar behind it.&lt;/p&gt;

&lt;p&gt;The setup: meeting requests land at &lt;code&gt;scheduling@agents.yourcompany.com&lt;/code&gt;, an LLM parses intent, the agent checks availability against its &lt;em&gt;own&lt;/em&gt; free/busy, proposes slots, and creates events that show up as normal invitations in Google Calendar, Microsoft 365, and Apple Calendar. No human mailbox in the loop, no delegation permissions, no calendar borrowed from whoever set the bot up.&lt;/p&gt;

&lt;p&gt;This runs on a Nylas &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/calendars/" rel="noopener noreferrer"&gt;Agent Account&lt;/a&gt; — a hosted mailbox-plus-calendar you provision through the API. Agent Accounts are in beta, so expect some movement before GA.&lt;/p&gt;

&lt;h2&gt;
  
  
  Provision the identity
&lt;/h2&gt;

&lt;p&gt;One CLI command or one API call:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent account create scheduling@agents.yourcompany.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The primary calendar is provisioned automatically — no extra call before you can create events on it. The API equivalent is &lt;code&gt;POST /v3/connect/custom&lt;/code&gt; with &lt;code&gt;"provider": "nylas"&lt;/code&gt; and the email address in &lt;code&gt;settings&lt;/code&gt;; no OAuth refresh token involved. Save the grant ID, then subscribe a webhook to four triggers: &lt;code&gt;message.created&lt;/code&gt;, &lt;code&gt;event.created&lt;/code&gt;, &lt;code&gt;event.updated&lt;/code&gt;, and &lt;code&gt;event.deleted&lt;/code&gt;. When Nylas sends the challenge &lt;code&gt;GET&lt;/code&gt; to your endpoint, respond with the &lt;code&gt;challenge&lt;/code&gt; value within 10 seconds to activate it.&lt;/p&gt;

&lt;h2&gt;
  
  
  The negotiation loop
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.nylas.com/docs/cookbook/use-cases/act/scheduling-agent-with-dedicated-identity/" rel="noopener noreferrer"&gt;full tutorial&lt;/a&gt; wires this end to end, but the shape is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Human emails the agent. &lt;code&gt;message.created&lt;/code&gt; fires; the webhook only carries summary fields, so the handler fetches the full body.&lt;/li&gt;
&lt;li&gt;The LLM extracts duration, timezone, and urgency.&lt;/li&gt;
&lt;li&gt;The agent queries &lt;code&gt;/calendars/free-busy&lt;/code&gt; against its own primary calendar and replies with 3 candidate slots.&lt;/li&gt;
&lt;li&gt;The human picks one; another &lt;code&gt;message.created&lt;/code&gt; fires; the agent creates the event with &lt;code&gt;notify_participants=true&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The availability check is the part people overcomplicate. Free/busy returns busy blocks over a window; the agent generates candidate slots and filters out the overlaps:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;freeBusy&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;nylas&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;calendars&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getFreeBusy&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;identifier&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AGENT_GRANT_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;requestBody&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="na"&gt;startTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preferredWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;start&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;endTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;floor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;parsed&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;preferredWindow&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;end&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;getTime&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="mi"&gt;1000&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="na"&gt;emails&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;scheduling@agents.yourcompany.com&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;
  &lt;span class="p"&gt;},&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;openSlots&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;candidates&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nf"&gt;overlapsAnyBusyBlock&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;slot&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;freeBusy&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The proposal reply then goes out through the standard send endpoint with &lt;code&gt;replyToMessageId&lt;/code&gt; set, so it threads under the original request. The recipient sees a normal reply from the agent's address — no relay footer, no sent-via branding.&lt;/p&gt;

&lt;p&gt;That last flag matters. With &lt;code&gt;notify_participants=true&lt;/code&gt;, an ICS &lt;code&gt;REQUEST&lt;/code&gt; goes out from the agent's address, and the recipient's calendar client renders it as a standard invitation:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&amp;lt;GRANT_ID&amp;gt;/events?calendar_id=primary&amp;amp;notify_participants=true"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "title": "Product demo",
    "when": { "start_time": 1744387200, "end_time": 1744390800 },
    "participants": [
      { "email": "alice@example.com" },
      { "email": "bob@example.com" }
    ]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;When Alice clicks &lt;strong&gt;Yes&lt;/strong&gt; in Gmail, Google sends the response back to the agent's mailbox, the event's &lt;code&gt;participants[].status&lt;/code&gt; updates automatically, and &lt;code&gt;event.updated&lt;/code&gt; fires — the agent knows who accepted without parsing a single email. Declines can trigger an LLM-drafted "here are some alternatives" reply; reschedule proposals can be answered with &lt;code&gt;POST /events/{id}/send-rsvp&lt;/code&gt; and a status of &lt;code&gt;yes&lt;/code&gt;, &lt;code&gt;no&lt;/code&gt;, or &lt;code&gt;maybe&lt;/code&gt;, which goes out as a standard ICS &lt;code&gt;REPLY&lt;/code&gt; every participant sees.&lt;/p&gt;

&lt;h2&gt;
  
  
  Updates, cancellations, and the notify switch
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;notify_participants&lt;/code&gt; is a per-call decision, not a one-time setting, and it applies to the whole event lifecycle. A &lt;code&gt;PUT /events/{id}&lt;/code&gt; with changed fields updates the meeting on every participant's calendar; &lt;code&gt;DELETE /events/{id}&lt;/code&gt; removes it everywhere. Pass &lt;code&gt;notify_participants=false&lt;/code&gt; when you want silence — pre-staging events the agent will announce later, or backfilling historical data without emailing anyone.&lt;/p&gt;

&lt;p&gt;The trap is the inverse: deleting an event &lt;em&gt;without&lt;/em&gt; &lt;code&gt;notify_participants=true&lt;/code&gt; leaves the meeting sitting on participants' calendars. For a scheduling agent, cancel with notification unless you have a specific reason not to. One more timezone note from the calendars doc: an Agent Account has no default time zone the way a human's calendar does, so pass &lt;code&gt;timezone&lt;/code&gt; on create or stick to epoch &lt;code&gt;start_time&lt;/code&gt;/&lt;code&gt;end_time&lt;/code&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  The agent as invitee, not just organizer
&lt;/h2&gt;

&lt;p&gt;It works in reverse too. When someone invites the agent to &lt;em&gt;their&lt;/em&gt; meeting, the invitation flows through the mailbox, Nylas parses it, and a matching event appears on the agent's primary calendar with the agent's status set to &lt;code&gt;noreply&lt;/code&gt;. You drive the response logic entirely off the &lt;code&gt;event.created&lt;/code&gt; webhook — the event object already carries the organizer, participants, and times. One gotcha from the &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/calendars/" rel="noopener noreferrer"&gt;calendars doc&lt;/a&gt;: the invite email also fires &lt;code&gt;message.created&lt;/code&gt;, so decide which webhook drives your logic and ignore the other, or you'll process every invitation twice.&lt;/p&gt;

&lt;h2&gt;
  
  
  Field notes worth stealing
&lt;/h2&gt;

&lt;p&gt;A few things the tutorial calls out that are easy to learn the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Threading is testable — test it.&lt;/strong&gt; Replies must preserve &lt;code&gt;Message-ID&lt;/code&gt;, &lt;code&gt;In-Reply-To&lt;/code&gt;, and &lt;code&gt;References&lt;/code&gt; so the conversation threads in Gmail and Outlook. Nylas preserves these on outbound; send yourself a request and verify the reply lands in the original thread before launch.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Watch the send cap.&lt;/strong&gt; Free-plan Agent Accounts send up to 200 messages per account per day, and a busy scheduling agent — proposals, confirmations, reminders — can get there. Paid plans have no daily cap by default; a policy can also set a stricter quota. Sort this before launch, not after the first missed confirmation.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Don't tunnel through ngrok.&lt;/strong&gt; Nylas blocks webhook URLs on ngrok because of throughput limiting; use VS Code port forwarding or Hookdeck for local development.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Humans can supervise over IMAP.&lt;/strong&gt; Set &lt;code&gt;app_password&lt;/code&gt; on the grant and your ops team can open the agent's mailbox in Outlook or Apple Mail to audit replies and intervene — every IMAP action syncs back to the API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Separate calendars for separate concerns.&lt;/strong&gt; Beyond the primary, you can create additional calendars up to your plan's cap — say, &lt;code&gt;sales-calls&lt;/code&gt; and &lt;code&gt;internal&lt;/code&gt; on the same agent.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The agent can't see mail it never receives.&lt;/strong&gt; A request routed to junk won't fire a &lt;code&gt;message.created&lt;/code&gt; on the inbox. If you run spam rules, audit them with the rule evaluations endpoint so important senders aren't silently filtered.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrong parses create real calendar chaos.&lt;/strong&gt; Latency is forgiving here (minutes, not milliseconds), but intent extraction errors are not. Require human confirmation for first-time senders or high-value meetings.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;One agent per role.&lt;/strong&gt; Scheduling, support, and outreach want different quotas and spam sensitivities. Model each as its own account with its own policy.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Counter-proposing a time isn't a first-class endpoint today — the common pattern is RSVP &lt;code&gt;no&lt;/code&gt; or &lt;code&gt;maybe&lt;/code&gt; plus a reply proposing alternatives. For heavily negotiated multi-participant flows, Scheduler is purpose-built and works with Agent Accounts.&lt;/p&gt;

&lt;p&gt;If you want to try this, the fastest start is a trial &lt;code&gt;*.nylas.email&lt;/code&gt; subdomain: provision the account, create one event with &lt;code&gt;notify_participants=true&lt;/code&gt;, and accept it from your personal calendar. Watching &lt;code&gt;event.updated&lt;/code&gt; arrive the moment you click &lt;strong&gt;Yes&lt;/strong&gt; is the point where the architecture clicks. What's your tolerance for letting the LLM book without human confirmation — and where would you draw that line?&lt;/p&gt;

</description>
      <category>ai</category>
      <category>calendar</category>
      <category>agents</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>A Sales Outreach Agent That Owns Its Email Address</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:53:01 +0000</pubDate>
      <link>https://dev.to/qasim157/a-sales-outreach-agent-that-owns-its-email-address-2a8c</link>
      <guid>https://dev.to/qasim157/a-sales-outreach-agent-that-owns-its-email-address-2a8c</guid>
      <description>&lt;p&gt;200 messages per account per day. That's the free-plan send ceiling on a Nylas &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/" rel="noopener noreferrer"&gt;Agent Account&lt;/a&gt;, and it's a surprisingly useful number to design an outreach agent around — it forces the kind of pacing that keeps cold email from becoming spam, and paid plans drop the daily cap by default when you outgrow it.&lt;/p&gt;

&lt;p&gt;The bigger idea: instead of sending campaigns through a rep's mailbox or a send-only API, the agent gets its own address. &lt;code&gt;sales-agent@yourcompany.com&lt;/code&gt; is a real mailbox — it sends, it receives replies, it owns a calendar. Agent Accounts are in beta, but the model is straightforward: each account is just another grant, so the Messages, Threads, Events, and Webhooks endpoints you'd use for a connected Gmail account work unchanged.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the loop looks like
&lt;/h2&gt;

&lt;p&gt;The sales-outreach pattern from the product docs runs in three stages, all on one &lt;code&gt;grant_id&lt;/code&gt;:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Send&lt;/strong&gt; the campaign through the standard send endpoint.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Classify replies&lt;/strong&gt; with an LLM into &lt;code&gt;interested&lt;/code&gt; / &lt;code&gt;not now&lt;/code&gt; / &lt;code&gt;unsubscribe&lt;/code&gt;, threading every exchange through the Messages API.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Book the meeting&lt;/strong&gt; — when a prospect says yes, the same grant creates an event on the agent's own calendar and sends the invite.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;No CRM hand-offs between three tools, no rep mailbox cluttered with sequence noise.&lt;/p&gt;

&lt;h2&gt;
  
  
  Replies arrive as webhooks
&lt;/h2&gt;

&lt;p&gt;Inbound mail fires &lt;code&gt;message.created&lt;/code&gt;, and the payload looks exactly like it does for any other grant. One subscription covers your whole application:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s1"&gt;'https://api.us.nylas.com/v3/webhooks/'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Content-Type: application/json'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s1"&gt;'Authorization: Bearer &amp;lt;NYLAS_API_KEY&amp;gt;'&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data-raw&lt;/span&gt; &lt;span class="s1"&gt;'{
    "trigger_types": ["message.created", "event.created", "event.updated"],
    "description": "Outreach agent",
    "webhook_url": "https://your-app.example.com/webhooks/nylas",
    "notification_email_addresses": ["dev-team@your-company.com"]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Your endpoint gets a &lt;code&gt;GET&lt;/code&gt; with a &lt;code&gt;challenge&lt;/code&gt; query parameter first — echo it back in a &lt;code&gt;200&lt;/code&gt; and deliveries start flowing as &lt;code&gt;POST&lt;/code&gt;s. The payload's &lt;code&gt;data.object&lt;/code&gt; carries sender, recipients, subject, snippet, and the field that matters most here: &lt;code&gt;thread_id&lt;/code&gt;, which groups the whole back-and-forth into one conversation your classifier (and your CRM) can reason about.&lt;/p&gt;

&lt;p&gt;Two handler habits from the pipeline recipe are worth copying verbatim. First, respond with a &lt;code&gt;200&lt;/code&gt; immediately and do the real work after — slow handlers trigger Nylas retries, which means more duplicates to dedup. Second, verify the signature before trusting anything in the body:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;crypto&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;verifyWebhookSignature&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;webhookSecret&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;x-nylas-signature&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;];&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hmac&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;crypto&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;createHmac&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;sha256&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;webhookSecret&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;hmac&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;update&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;)).&lt;/span&gt;&lt;span class="nf"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="s2"&gt;hex&lt;/span&gt;&lt;span class="dl"&gt;"&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;signature&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="nx"&gt;digest&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An outreach agent acts on what webhooks tell it. A forged &lt;code&gt;message.created&lt;/code&gt; that skips verification is a forged instruction to your agent.&lt;/p&gt;

&lt;h2&gt;
  
  
  The unglamorous parts that decide whether this works
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.nylas.com/docs/cookbook/use-cases/automate/automate-sales-pipeline/" rel="noopener noreferrer"&gt;sales pipeline automation recipe&lt;/a&gt; is blunt about where these systems fail, and every lesson transfers directly to an outreach agent:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Webhooks are at-least-once.&lt;/strong&gt; The same &lt;code&gt;message.created&lt;/code&gt; notification can land twice, especially if your handler was slow. Track processed webhook IDs in Redis or a database table with a TTL — an in-memory &lt;code&gt;Set&lt;/code&gt; doesn't survive restarts. An outreach agent that double-processes a reply is an agent that replies twice.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Out-of-office replies fire &lt;code&gt;message.created&lt;/code&gt; like real replies.&lt;/strong&gt; Detect them with subject patterns (&lt;code&gt;out of office&lt;/code&gt;, &lt;code&gt;automatic reply&lt;/code&gt;, &lt;code&gt;auto-reply&lt;/code&gt;) and skip. An OOO classified as "interested" pollutes your pipeline worse than a missed reply.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Use the &lt;code&gt;folders&lt;/code&gt; field for direction.&lt;/strong&gt; A message in &lt;code&gt;SENT&lt;/code&gt; is the agent's own outbound; &lt;code&gt;INBOX&lt;/code&gt; means the prospect wrote back. Don't compare addresses against a roster — check the folder.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Skip internal mail.&lt;/strong&gt; If every address on a message belongs to one of your company's domains, it's colleagues talking — not pipeline signal. Compare sender and recipient domains against your own and bail early, or your activity log fills up with "lunch?" threads.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Filter automated senders.&lt;/strong&gt; &lt;code&gt;no-reply@&lt;/code&gt;, &lt;code&gt;notifications@&lt;/code&gt;, &lt;code&gt;mailer-daemon@&lt;/code&gt;, &lt;code&gt;postmaster@&lt;/code&gt;, &lt;code&gt;bounce@&lt;/code&gt; — none of these are prospects, and a bounce treated as engagement is how an agent emails a dead address forever. The recipe also flags the &lt;code&gt;List-Unsubscribe&lt;/code&gt; header and "unsubscribe" in the subject as bulk-sender tells.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;noReplyPatterns&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;
  &lt;span class="sr"&gt;/^no-&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;reply@/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/^notifications&lt;/span&gt;&lt;span class="se"&gt;?&lt;/span&gt;&lt;span class="sr"&gt;@/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/^mailer-daemon@/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/^postmaster@/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="sr"&gt;/^bounce@/i&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;];&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;noReplyPatterns&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;some&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;p&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;test&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;senderEmail&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Closing the loop on the calendar
&lt;/h2&gt;

&lt;p&gt;This is the piece send-only infrastructure can't replicate. When the classifier returns &lt;code&gt;interested&lt;/code&gt; and the prospect agrees to a time, the agent creates the event on its own primary calendar with &lt;code&gt;notify_participants=true&lt;/code&gt;. The invite goes out from the agent's address as a normal calendar invitation; the &lt;code&gt;event.created&lt;/code&gt; and &lt;code&gt;event.updated&lt;/code&gt; webhooks in the subscription above tell you when the prospect accepts. The same recipe's advice applies here too: key CRM records on the event &lt;code&gt;id&lt;/code&gt;, because meetings get rescheduled constantly and &lt;code&gt;event.updated&lt;/code&gt; will fire for every change.&lt;/p&gt;

&lt;p&gt;For longer deal cycles, replies can arrive days after the last touch. The &lt;a href="https://developer.nylas.com/docs/cookbook/use-cases/industries/sales-engagement/" rel="noopener noreferrer"&gt;multi-turn conversation pattern&lt;/a&gt; — send, receive, restore context from the thread, respond — is built for exactly that, and the thread history doubles as the record of everything the agent ever told a prospect.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just use the rep's inbox?
&lt;/h2&gt;

&lt;p&gt;You can connect rep mailboxes over OAuth, and for logging &lt;em&gt;human&lt;/em&gt; activity into a CRM that's the right call — the pipeline recipe does precisely that with &lt;code&gt;message.created&lt;/code&gt; and &lt;code&gt;event.created&lt;/code&gt; across connected grants. But campaign sends from a rep's address mean the rep's sender reputation absorbs the campaign's bounces, the rep's inbox absorbs the replies, and offboarding the rep breaks the integration. A dedicated agent identity isolates all three. You can even provision one agent per customer domain — &lt;code&gt;sales-agent@customer-a.com&lt;/code&gt;, &lt;code&gt;sales-agent@customer-b.com&lt;/code&gt; — each with its own quota and reputation, all in one application.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick answers
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Do I need one webhook per agent?&lt;/strong&gt; No — a single subscription covers every grant in your Nylas application, agents and connected rep accounts alike. Branch on the grant's provider (&lt;code&gt;"nylas"&lt;/code&gt;) to tell agent deliveries apart from connected-grant deliveries.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What about contact data?&lt;/strong&gt; Reply classification gets you intent; the pipeline recipe pairs it with a nightly job that pulls contacts from the Contacts API (paginated 50 at a time with &lt;code&gt;next_cursor&lt;/code&gt;) and patches fresh phone numbers, job titles, and companies into the CRM. Real-time webhooks for activity, periodic sync for data that rots slowly.&lt;/p&gt;

&lt;p&gt;If you want to test the shape of this without committing, provision an account on a trial &lt;code&gt;*.nylas.email&lt;/code&gt; domain, point the webhook at a tunnel, and run a five-prospect campaign against your own test addresses. The classification step is where the interesting tuning lives — what label set are you using for reply intent? I'd genuinely like to hear what taxonomies are working for people in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>email</category>
      <category>sales</category>
      <category>agents</category>
    </item>
    <item>
      <title>Build an Email Support Triage Agent With Its Own Inbox</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:52:55 +0000</pubDate>
      <link>https://dev.to/qasim157/build-an-email-support-triage-agent-with-its-own-inbox-3c10</link>
      <guid>https://dev.to/qasim157/build-an-email-support-triage-agent-with-its-own-inbox-3c10</guid>
      <description>&lt;p&gt;Every shared support inbox eventually becomes a triage problem: 80 unread messages, no agreement on what "urgent" means, and the one person who knows which customer is about to churn is on PTO. Teams keep solving this with labels and heroics. It's a better fit for an LLM — as long as the LLM has somewhere safe to live.&lt;/p&gt;

&lt;p&gt;That's the case for giving the triage agent its own mailbox. Nylas &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/" rel="noopener noreferrer"&gt;Agent Accounts&lt;/a&gt; (currently in beta) are hosted mailboxes you create entirely through the API. A &lt;code&gt;support@yourcompany.com&lt;/code&gt; Agent Account receives every inbound support email, gets six system folders out of the box (&lt;code&gt;inbox&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;drafts&lt;/code&gt;, &lt;code&gt;trash&lt;/code&gt;, &lt;code&gt;junk&lt;/code&gt;, &lt;code&gt;archive&lt;/code&gt;), and exposes the same &lt;code&gt;grant_id&lt;/code&gt;-based endpoints as any connected Gmail or Outlook account.&lt;/p&gt;

&lt;p&gt;Creating one is a single request:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/connect/custom"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$NYLAS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "provider": "nylas",
    "settings": { "email": "support@yourcompany.com" }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Save the &lt;code&gt;grant_id&lt;/code&gt; from the response — every other call hangs off it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Four buckets beat five
&lt;/h2&gt;

&lt;p&gt;The classification scheme from the &lt;a href="https://developer.nylas.com/docs/cookbook/agents/email-triage-agent/" rel="noopener noreferrer"&gt;email triage agent recipe&lt;/a&gt; sorts mail into exactly four categories:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Bucket&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;th&gt;Action&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;URGENT&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Production incident, executive ask&lt;/td&gt;
&lt;td&gt;Draft a reply within the hour&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;ACTION&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Code review, meeting follow-up&lt;/td&gt;
&lt;td&gt;Draft a reply same-day&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;FYI&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Status update&lt;/td&gt;
&lt;td&gt;Leave it alone&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOISE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Newsletter, automated alert&lt;/td&gt;
&lt;td&gt;Archive&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Four is deliberate. Three loses fidelity — everything collapses into "important." Five and the model starts confusing adjacent categories.&lt;/p&gt;

&lt;p&gt;The prompt runs with &lt;code&gt;temperature=0&lt;/code&gt; and &lt;code&gt;max_tokens=10&lt;/code&gt;, and the model only sees sender + subject + a 200-character snippet, not the full body. That's enough for over 90% accuracy. Here's the prompt verbatim from the recipe:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You triage email into one of four categories:

URGENT  — production incidents, executive requests; reply within 1 hour
ACTION  — code reviews, meeting follow-ups; reply same day
FYI     — informational, no response needed
NOISE   — newsletters, marketing, automated notifications

From:    {sender}
Subject: {subject}
Snippet: {snippet}

Return ONLY the category name. Nothing else.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Validate the output against the four valid strings (LLMs occasionally invent a category) and fall back to &lt;code&gt;FYI&lt;/code&gt; on anything unrecognized.&lt;/p&gt;

&lt;p&gt;The cost math is almost a rounding error. GPT-4o-mini runs about $0.15 per million input tokens; a 200-character snippet plus the prompt is roughly 150 tokens, so 100 emails is about 15K tokens — call it $0.002. Drafting uses a stronger model (GPT-4o, around $2.50 per million input tokens), but only on the URGENT and ACTION subset, typically under 20% of the inbox. A heavy day at 200 unread emails costs roughly a nickel. And if some of that mail can't leave your infrastructure, point the same OpenAI client at a local Ollama endpoint — Llama 3.1 classifies almost as well as GPT-4o-mini for this task, though drafting quality drops noticeably below a 70B-parameter model.&lt;/p&gt;

&lt;h2&gt;
  
  
  Drafting is where you add a second gate
&lt;/h2&gt;

&lt;p&gt;Classifying wrong is cheap. Replying wrong is expensive. The &lt;a href="https://developer.nylas.com/docs/cookbook/agents/email-support-agent/" rel="noopener noreferrer"&gt;support agent pattern&lt;/a&gt; layers two independent checks before any draft exists:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Confidence gating&lt;/strong&gt; on the knowledge-base match. At a score of &lt;code&gt;0.85&lt;/code&gt; or higher, draft directly from the matched article. Between &lt;code&gt;0.60&lt;/code&gt; and &lt;code&gt;0.85&lt;/code&gt;, draft conservatively and cite the article inline so a reviewer can verify. Below &lt;code&gt;0.60&lt;/code&gt;, don't draft at all — flag for manual handling with the best-guess article attached.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Risk tiering&lt;/strong&gt;, which doesn't care about confidence. Password resets and FAQ-shaped questions get drafted. Refunds and billing changes get drafted with extra scrutiny. Legal threats, regulatory matters, and fraud reports skip the model entirely and escalate to a human with full context. A high-confidence KB match for a refund question still goes through review — the Air Canada chatbot ruling is the canonical reminder of why.&lt;/p&gt;

&lt;p&gt;In both recipes, the agent never hits send. Drafts land in the drafts folder; a human approves. On an Agent Account that drafts folder belongs to the agent itself, so the review queue and the audit trail are the same mailbox.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;handle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="n"&gt;question&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;extract_question&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;kb&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;question&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;classify_risk&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;escalate_to_human&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;reason&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;high-risk topic&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.60&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="nf"&gt;flag_for_review&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;return&lt;/span&gt;

    &lt;span class="n"&gt;draft&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;generate_draft&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;cite_inline&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;conf&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mf"&gt;0.85&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="nf"&gt;queue_for_approval&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;draft&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;article&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Rules filter before the model ever runs
&lt;/h2&gt;

&lt;p&gt;Here's the part a borrowed human inbox can't do. Agent Accounts support inbound rules that run at the mail layer: block known spam domains at the SMTP stage, auto-route invoices to a finance folder with &lt;code&gt;assign_to_folder&lt;/code&gt;, mark VIP senders for immediate attention. The junk never reaches your classification prompt, which also means injection-laden garbage never enters the model's context. Rules and allow/block lists are covered in the &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/" rel="noopener noreferrer"&gt;Agent Accounts overview&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Inbound mail fires the standard &lt;code&gt;message.created&lt;/code&gt; webhook — identical in shape to the same event for any other grant — so the trigger side of your pipeline is whatever you already run for connected accounts. If webhooks are more infrastructure than you want on day one, polling works; the support recipe suggests every 5–15 minutes, which is fine latency for support.&lt;/p&gt;

&lt;h2&gt;
  
  
  Rollout advice from the recipes
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Start at 5 tickets per cycle&lt;/strong&gt; while you tune the KB matcher and risk classifier. Bump to 20 once the false-positive rate is acceptable.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Group similar tickets.&lt;/strong&gt; If the agent sees three "where's my receipt?" tickets in a row, batch them — same KB article, same draft template, one reviewer pass.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Track what the agent can't match.&lt;/strong&gt; Low-confidence tickets are the strongest signal of where your knowledge base has holes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mind the send cap.&lt;/strong&gt; A free-plan Agent Account sends up to 200 messages per account per day; paid plans have no daily cap by default, and a policy can set a stricter quota.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cap reply length in the prompt.&lt;/strong&gt; The drafting prompt in the triage recipe says "three sentences max," and that constraint is load-bearing — without it, drafts read like a politely overcompensating intern.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Log everything&lt;/strong&gt; — every classification, KB lookup, and approval decision — to your own store. Support is the workload where you'll be asked "why did it say that?" months later.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The cheapest way to evaluate this is to run classification-only for a week: create the Agent Account, point a copy of your support flow at it, and log the four-bucket output without drafting anything. Compare against what your humans actually prioritized. If the agreement rate clears 90%, you've earned the right to turn on drafting.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>email</category>
      <category>agents</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Give Your AI Agent Its Own Email Address (Not Access to Yours)</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Fri, 12 Jun 2026 00:52:40 +0000</pubDate>
      <link>https://dev.to/qasim157/give-your-ai-agent-its-own-email-address-not-access-to-yours-18ja</link>
      <guid>https://dev.to/qasim157/give-your-ai-agent-its-own-email-address-not-access-to-yours-18ja</guid>
      <description>&lt;p&gt;Most "AI agent + email" tutorials start the same way: connect the agent to a human's inbox over OAuth, hope the token doesn't expire mid-run, and pray the agent never replies to the wrong thread on someone's behalf.&lt;/p&gt;

&lt;p&gt;There's a different model: give the agent its &lt;strong&gt;own&lt;/strong&gt; email address. Nylas recently shipped &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/" rel="noopener noreferrer"&gt;Agent Accounts&lt;/a&gt; (currently in beta) — fully functional, Nylas-hosted mailboxes you create and control entirely through the API. Each one is a real &lt;code&gt;name@company.com&lt;/code&gt; address that sends, receives, hosts calendar events, and RSVPs to invitations. To anyone interacting with it, it's indistinguishable from a human-operated account.&lt;/p&gt;

&lt;p&gt;I work on the docs at Nylas, so I've spent a lot of time with this API. Here's a tour of what it does and how to get a mailbox running in a few minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why not just connect the agent to a human inbox?
&lt;/h2&gt;

&lt;p&gt;You can — that's what OAuth grants are for, and they're the right tool when the agent works &lt;em&gt;on behalf of&lt;/em&gt; a person. But a lot of agent workflows want a first-class identity instead:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;System mailboxes&lt;/strong&gt; (&lt;code&gt;sales@&lt;/code&gt;, &lt;code&gt;support@&lt;/code&gt;, &lt;code&gt;scheduling@&lt;/code&gt;) that your app owns end-to-end. No OAuth consent screen, no user offboarding breaking your integration.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Ephemeral inboxes&lt;/strong&gt; for test automation — provision a fresh address per run, sign up for a service, grab the OTP from the verification email, tear it down.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Per-customer identities&lt;/strong&gt; in multi-tenant apps: &lt;code&gt;scheduling@customer-a.com&lt;/code&gt;, &lt;code&gt;scheduling@customer-b.com&lt;/code&gt;, each with its own send quota and sender reputation, all in one Nylas application.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A scheduling bot with its own calendar&lt;/strong&gt; that proposes slots, sends invites, and shows up as a normal participant in Google Calendar, Microsoft 365, and Apple Calendar.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The key design decision: an Agent Account is just another &lt;a href="https://developer.nylas.com/docs/v3/auth/" rel="noopener noreferrer"&gt;grant&lt;/a&gt;. It gets a &lt;code&gt;grant_id&lt;/code&gt; that works with every existing Nylas endpoint — Messages, Drafts, Threads, Folders, Attachments, Calendars, Events, Webhooks. If you've already built against connected accounts, nothing new to learn.&lt;/p&gt;

&lt;h2&gt;
  
  
  Create a mailbox with one API call
&lt;/h2&gt;

&lt;p&gt;Every Agent Account lives on a domain — either a Nylas-provided &lt;code&gt;*.nylas.email&lt;/code&gt; trial subdomain (instant, good for testing) or your own domain with MX and TXT records configured. With a domain registered, creation is a single request to the same Bring Your Own Auth endpoint Nylas uses for other providers, with &lt;code&gt;"provider": "nylas"&lt;/code&gt;. No refresh token needed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/connect/custom"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$NYLAS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "provider": "nylas",
    "settings": {
      "email": "test@your-application.nylas.email"
    }
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The response contains a &lt;code&gt;grant_id&lt;/code&gt; — save it, it's the handle for everything else. The mailbox is live immediately with six system folders (&lt;code&gt;inbox&lt;/code&gt;, &lt;code&gt;sent&lt;/code&gt;, &lt;code&gt;drafts&lt;/code&gt;, &lt;code&gt;trash&lt;/code&gt;, &lt;code&gt;junk&lt;/code&gt;, &lt;code&gt;archive&lt;/code&gt;) and a primary calendar.&lt;/p&gt;

&lt;p&gt;If you prefer a CLI, it's one command after &lt;code&gt;nylas init&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent account create &lt;span class="nb"&gt;test&lt;/span&gt;@your-application.nylas.email
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Receive email like any other grant
&lt;/h2&gt;

&lt;p&gt;Inbound mail fires the standard &lt;code&gt;message.created&lt;/code&gt; webhook — identical in shape to the same event for a Gmail or Outlook grant. Register one and Nylas calls your URL the moment a message arrives:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/webhooks"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$NYLAS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "trigger_types": ["message.created"],
    "callback_url": "https://yourapp.example.com/webhooks/nylas"
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If your app handles both connected grants and Agent Accounts, branch on the grant's &lt;code&gt;provider&lt;/code&gt; field (&lt;code&gt;"nylas"&lt;/code&gt;) to tell the deliveries apart. Polling &lt;code&gt;GET /v3/grants/{grant_id}/messages&lt;/code&gt; works too if you don't want webhook infrastructure yet.&lt;/p&gt;

&lt;h2&gt;
  
  
  Send from the agent's own address
&lt;/h2&gt;

&lt;p&gt;Outbound mail uses the same send endpoint as any connected grant:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;--request&lt;/span&gt; POST &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--url&lt;/span&gt; &lt;span class="s2"&gt;"https://api.us.nylas.com/v3/grants/&lt;/span&gt;&lt;span class="nv"&gt;$GRANT_ID&lt;/span&gt;&lt;span class="s2"&gt;/messages/send"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Authorization: Bearer &lt;/span&gt;&lt;span class="nv"&gt;$NYLAS_API_KEY&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--header&lt;/span&gt; &lt;span class="s2"&gt;"Content-Type: application/json"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--data&lt;/span&gt; &lt;span class="s1"&gt;'{
    "subject": "Hello from my Agent Account",
    "body": "This message was sent by a Nylas Agent Account.",
    "to": [{ "email": "you@yourdomain.com", "name": "You" }]
  }'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The recipient sees a normal message from the agent's address — no "sent via" branding, no relay footer. Replies land back in the agent's inbox and thread normally, so multi-turn conversations (the thing LLM agents are actually good at) work out of the box.&lt;/p&gt;

&lt;h2&gt;
  
  
  The calendar is real too
&lt;/h2&gt;

&lt;p&gt;Every Agent Account ships with a primary calendar that speaks standard iCalendar/ICS. The agent can:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;create events and invite people (&lt;code&gt;POST /v3/grants/{grant_id}/events&lt;/code&gt;)&lt;/li&gt;
&lt;li&gt;accept or decline invitations it receives (&lt;code&gt;POST /v3/grants/{grant_id}/events/{id}/send-rsvp&lt;/code&gt;)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Because it's plain ICS under the hood, every major calendar client treats the agent as a regular participant. A scheduling agent can own its availability instead of borrowing a human's.&lt;/p&gt;

&lt;h2&gt;
  
  
  Guardrails: policies, rules, and lists
&lt;/h2&gt;

&lt;p&gt;Letting an autonomous agent send email is exactly the kind of thing that deserves guardrails, and this is where Agent Accounts get interesting. You can attach &lt;a href="https://developer.nylas.com/docs/v3/agent-accounts/policies-rules-lists/" rel="noopener noreferrer"&gt;policies&lt;/a&gt; that bundle send limits, spam detection, attachment restrictions, and retention settings — and assign one policy to many accounts. Rules match on sender, recipient, or message type and run actions like &lt;code&gt;block&lt;/code&gt;, &lt;code&gt;mark_as_spam&lt;/code&gt;, or &lt;code&gt;assign_to_folder&lt;/code&gt;, with allow/block lists of domains, TLDs, or addresses referenced through an &lt;code&gt;in_list&lt;/code&gt; operator.&lt;/p&gt;

&lt;p&gt;So your support triage agent can block known spam domains at the SMTP stage before your LLM ever sees the message — which also keeps prompt-injection-laden junk out of the model's context.&lt;/p&gt;

&lt;h2&gt;
  
  
  Limits worth knowing (beta, free plan)
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Send rate:&lt;/strong&gt; 200 messages per account per day on the free plan (paid plans have no daily cap by default)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; 3 GB per organization on the free plan&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Retention:&lt;/strong&gt; 30 days inbox, 7 days spam on the free plan (configurable via policy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Outbound message size:&lt;/strong&gt; 40 MB total&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domains:&lt;/strong&gt; unlimited — one application can manage accounts across any number of registered domains&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Agent Accounts are in beta, so the API may change before general availability.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;The &lt;a href="https://developer.nylas.com/docs/v3/getting-started/agent-accounts/" rel="noopener noreferrer"&gt;quickstart&lt;/a&gt; goes from zero to a sending-and-receiving mailbox in under 5 minutes. If you want recipes, the cookbook covers &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/handle-replies/" rel="noopener noreferrer"&gt;handling replies&lt;/a&gt;, &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/multi-turn-conversations/" rel="noopener noreferrer"&gt;multi-turn conversations&lt;/a&gt;, &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/extract-otp-code/" rel="noopener noreferrer"&gt;OTP extraction&lt;/a&gt;, and &lt;a href="https://developer.nylas.com/docs/cookbook/agent-accounts/sign-up-for-a-service/" rel="noopener noreferrer"&gt;signing up for services autonomously&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;Curious what people are building with agent-owned identities — if you've given an agent its own inbox (with Nylas or anything else), I'd love to hear how it went in the comments.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>email</category>
      <category>api</category>
      <category>agents</category>
    </item>
    <item>
      <title>Manage Nylas Applications, Connectors, and Grants from the CLI</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Mon, 11 May 2026 02:09:02 +0000</pubDate>
      <link>https://dev.to/qasim157/manage-nylas-applications-connectors-and-grants-from-the-cli-3f30</link>
      <guid>https://dev.to/qasim157/manage-nylas-applications-connectors-and-grants-from-the-cli-3f30</guid>
      <description>&lt;p&gt;The &lt;code&gt;nylas admin&lt;/code&gt; commands let you manage your Nylas infrastructure without the web dashboard. Create applications, configure provider connectors, manage OAuth credentials, and monitor grants — all from the terminal.&lt;/p&gt;

&lt;h2&gt;
  
  
  What nylas admin does
&lt;/h2&gt;

&lt;p&gt;Full API management surface: applications, callback URIs, provider connectors (Google, Microsoft, IMAP), credentials, and grants. Useful for CI/CD pipelines, multi-tenant setups, and infrastructure-as-code workflows.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;brew &lt;span class="nb"&gt;install &lt;/span&gt;nylas/nylas-cli/nylas
nylas init
nylas admin applications list
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key commands
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas admin applications list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all applications&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas admin applications create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a new application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas admin connectors list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List provider connectors&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas admin connectors create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a Google/Microsoft/IMAP connector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas admin credentials list &amp;lt;connector-id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List credentials for a connector&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas admin credentials create&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Add OAuth credentials&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas admin callback-uris list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List OAuth callback URIs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas admin callback-uris create --url &amp;lt;url&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Register a new callback URI&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas admin grants list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all grants on the application&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas admin grants stats&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show grant statistics&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Creating an application and connector
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create app&lt;/span&gt;
nylas admin applications create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"prod-email"&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;

&lt;span class="c"&gt;# Add Google connector&lt;/span&gt;
nylas admin connectors create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--provider&lt;/span&gt; google &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--client-id&lt;/span&gt; &lt;span class="nv"&gt;$GOOGLE_CLIENT_ID&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--client-secret&lt;/span&gt; &lt;span class="nv"&gt;$GOOGLE_CLIENT_SECRET&lt;/span&gt;

&lt;span class="c"&gt;# Register callback URI&lt;/span&gt;
nylas admin callback-uris create &lt;span class="nt"&gt;--url&lt;/span&gt; https://myapp.com/oauth/callback
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Monitoring grants
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# How many users are connected?&lt;/span&gt;
nylas admin grants stats &lt;span class="nt"&gt;--json&lt;/span&gt;

&lt;span class="c"&gt;# List all grants with their status&lt;/span&gt;
nylas admin grants list &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="s1"&gt;'.[] | {id, provider, status}'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/qasim157/from-gmail-oauth-hell-to-one-line-agent-identity-cpp"&gt;From Gmail OAuth hell to one-line agent identity&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/qasim157/track-every-action-your-ai-agent-takes-audit-logs-auth-management-compliance-for-cli-automation-33de"&gt;Track Every Action Your AI Agent Takes&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;All commands:&lt;/strong&gt; &lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI Command Reference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get started:&lt;/strong&gt; &lt;code&gt;brew install nylas/nylas-cli/nylas&lt;/code&gt; — &lt;a href="https://cli.nylas.com/guides/getting-started" rel="noopener noreferrer"&gt;other install methods&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>devops</category>
      <category>api</category>
      <category>tutorial</category>
    </item>
    <item>
      <title>Agent Policies and Rules — Control What Your AI Agent Can Send and Receive</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Mon, 11 May 2026 02:08:56 +0000</pubDate>
      <link>https://dev.to/qasim157/agent-policies-and-rules-control-what-your-ai-agent-can-send-and-receive-1aen</link>
      <guid>https://dev.to/qasim157/agent-policies-and-rules-control-what-your-ai-agent-can-send-and-receive-1aen</guid>
      <description>&lt;p&gt;The &lt;code&gt;nylas agent policy&lt;/code&gt; and &lt;code&gt;nylas agent rule&lt;/code&gt; commands define guardrails for AI agent email accounts. Set who the agent can email, what triggers are allowed, and what actions to take on inbound messages — all enforced server-side.&lt;/p&gt;

&lt;h2&gt;
  
  
  What policies and rules do
&lt;/h2&gt;

&lt;p&gt;A policy is a named container for rules. Rules define conditions and actions: "if the sender is external, block" or "if sending to a non-internal domain, require approval." These fire before the agent sees the message, so prompt injection can't bypass them.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent policy create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"production-guardrails"&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt;
nylas agent rule create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"internal-only-outbound"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--condition&lt;/span&gt; &lt;span class="s2"&gt;"recipient_domain != company.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--action&lt;/span&gt; block
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key commands
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas agent policy list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all policies&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas agent policy create --name N&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a named policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas agent policy get &amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show policy details&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas agent policy delete &amp;lt;id&amp;gt; --yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a policy&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas agent rule list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List all rules&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas agent rule create --name N --condition C --action A&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create a rule from flags&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas agent rule create --data-file rule.json&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create from JSON file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas agent rule get &amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show a rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas agent rule delete &amp;lt;id&amp;gt; --yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a rule&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas agent account create &amp;lt;email&amp;gt; --policy-id &amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Attach policy to agent&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Example: restrict outbound to internal domains only
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Create policy&lt;/span&gt;
&lt;span class="nv"&gt;POLICY&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;nylas agent policy create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"internal-only"&lt;/span&gt; &lt;span class="nt"&gt;--json&lt;/span&gt; | jq &lt;span class="nt"&gt;-r&lt;/span&gt; &lt;span class="s1"&gt;'.id'&lt;/span&gt;&lt;span class="si"&gt;)&lt;/span&gt;

&lt;span class="c"&gt;# Add rule: block any outbound to non-company domains&lt;/span&gt;
nylas agent rule create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--policy-id&lt;/span&gt; &lt;span class="nv"&gt;$POLICY&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"block-external-send"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--condition&lt;/span&gt; &lt;span class="s2"&gt;"direction == outbound AND recipient_domain != mycompany.com"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--action&lt;/span&gt; block

&lt;span class="c"&gt;# Attach to agent account&lt;/span&gt;
nylas agent account create agent@mycompany.nylas.email &lt;span class="nt"&gt;--policy-id&lt;/span&gt; &lt;span class="nv"&gt;$POLICY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Example: auto-archive marketing emails
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas agent rule create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"archive-marketing"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--condition&lt;/span&gt; &lt;span class="s2"&gt;"sender_domain IN (marketing.example.com, promo.example.com)"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--action&lt;/span&gt; archive
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/qasim157/i-gave-my-claude-code-agent-an-email-address-14hc"&gt;I gave my Claude Code agent an email address&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/qasim157/ai-agents-need-email-your-mcp-setup-is-incomplete-without-it-ic6"&gt;AI agents need email — your MCP setup is incomplete&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/qasim157/5-nylas-cli-commands-every-ai-agent-should-have-access-to-2e87"&gt;5 Nylas CLI commands every AI agent should have&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;All commands:&lt;/strong&gt; &lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI Command Reference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get started:&lt;/strong&gt; &lt;code&gt;brew install nylas/nylas-cli/nylas&lt;/code&gt; — &lt;a href="https://cli.nylas.com/guides/getting-started" rel="noopener noreferrer"&gt;other install methods&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>aiagents</category>
      <category>security</category>
      <category>email</category>
    </item>
    <item>
      <title>Manage Email Signatures from the CLI — Create, List, and Attach to Sends</title>
      <dc:creator>Qasim Muhammad</dc:creator>
      <pubDate>Mon, 11 May 2026 02:08:52 +0000</pubDate>
      <link>https://dev.to/qasim157/manage-email-signatures-from-the-cli-create-list-and-attach-to-sends-2154</link>
      <guid>https://dev.to/qasim157/manage-email-signatures-from-the-cli-create-list-and-attach-to-sends-2154</guid>
      <description>&lt;p&gt;The &lt;code&gt;nylas email signatures&lt;/code&gt; commands manage reusable email signatures stored per-grant. Create HTML signatures once, attach them to any send with a flag — no more pasting footers manually.&lt;/p&gt;

&lt;h2&gt;
  
  
  What email signatures do
&lt;/h2&gt;

&lt;p&gt;Signatures are HTML blocks stored server-side and attached to outgoing emails via &lt;code&gt;--signature-id&lt;/code&gt;. Create different signatures for different contexts (formal, casual, department) and switch between them per-send.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick Start
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas email signatures create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Work"&lt;/span&gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"&amp;lt;p&amp;gt;— Qasim&amp;lt;br&amp;gt;Staff SRE, Nylas&amp;lt;/p&amp;gt;"&lt;/span&gt;
nylas email send &lt;span class="nt"&gt;--to&lt;/span&gt; alice@example.com &lt;span class="nt"&gt;--subject&lt;/span&gt; &lt;span class="s2"&gt;"Hello"&lt;/span&gt; &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"..."&lt;/span&gt; &lt;span class="nt"&gt;--signature-id&lt;/span&gt; SIG_ID
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Key commands
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Command&lt;/th&gt;
&lt;th&gt;What it does&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas email signatures list&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;List stored signatures&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas email signatures show &amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Show signature HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas email signatures create --name N --body B&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create from inline HTML&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas email signatures create --name N --body-file FILE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Create from HTML file&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas email signatures update &amp;lt;id&amp;gt;&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Update a signature&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;nylas email signatures delete &amp;lt;id&amp;gt; --yes&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Delete a signature&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  Creating a signature from a file
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; sig.html &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt; &lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
&amp;lt;p style="font-family: sans-serif; font-size: 14px;"&amp;gt;
  &amp;lt;strong&amp;gt;Qasim Muhammad&amp;lt;/strong&amp;gt;&amp;lt;br&amp;gt;
  Staff SRE · Nylas&amp;lt;br&amp;gt;
  &amp;lt;a href="https://cli.nylas.com"&amp;gt;cli.nylas.com&amp;lt;/a&amp;gt;
&amp;lt;/p&amp;gt;
&lt;/span&gt;&lt;span class="no"&gt;EOF

&lt;/span&gt;nylas email signatures create &lt;span class="nt"&gt;--name&lt;/span&gt; &lt;span class="s2"&gt;"Default"&lt;/span&gt; &lt;span class="nt"&gt;--body-file&lt;/span&gt; sig.html &lt;span class="nt"&gt;--json&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Using a signature when sending
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nylas email send &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--to&lt;/span&gt; team@company.com &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--subject&lt;/span&gt; &lt;span class="s2"&gt;"Weekly update"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--body&lt;/span&gt; &lt;span class="s2"&gt;"Here's the summary..."&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--signature-id&lt;/span&gt; &amp;lt;sig-id&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The signature HTML is appended to the email body automatically.&lt;/p&gt;

&lt;h2&gt;
  
  
  Related posts
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;&lt;a href="https://dev.to/qasim157/send-email-from-any-linux-server-in-60-seconds-no-smtp-config-11ac"&gt;Send email from any Linux server in 60 seconds&lt;/a&gt;&lt;/li&gt;
&lt;li&gt;&lt;a href="https://dev.to/qasim157/automate-your-entire-email-workflow-from-the-terminal-no-smtp-no-sendmail-5apo"&gt;Automate Your Entire Email Workflow from the Terminal&lt;/a&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;&lt;strong&gt;All commands:&lt;/strong&gt; &lt;a href="https://cli.nylas.com/docs/commands" rel="noopener noreferrer"&gt;Nylas CLI Command Reference&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Get started:&lt;/strong&gt; &lt;code&gt;brew install nylas/nylas-cli/nylas&lt;/code&gt; — &lt;a href="https://cli.nylas.com/guides/getting-started" rel="noopener noreferrer"&gt;other install methods&lt;/a&gt;&lt;/p&gt;

</description>
      <category>cli</category>
      <category>email</category>
      <category>productivity</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
