A user has just handed your product access to their mailbox. That’s the moment they’re most likely to wonder what they agreed to, and the moment a clear email does the most good. A grant.created workflow sends one automatically.
This is also the cheapest workflow to build, because grant.created needs no configuration and fires exactly once per connection.

What does grant.created give me?
Section titled “What does grant.created give me?”More than the other grant events. It’s the only one carrying the user’s display name and the list of scopes they approved, which is what makes a useful welcome email possible.
| Field | Example | Present on other grant events |
|---|---|---|
email | [email protected] | yes |
provider | google | yes |
name | John Doe | no |
scope | ["gmail.readonly", "calendar"] | no |
grant_id | <NYLAS_GRANT_ID> | yes |
login_id | <LOGIN_ID> | no |
code | 25012 | yes, with a different value |
Variables sit at the payload root, so it’s {{email}} and {{provider}}, not {{grant.email}}. There’s no booking_info wrapper and no recipient object, so use {{email}} for the account address and guard everything else.
Why does listing the permissions matter?
Section titled “Why does listing the permissions matter?”It answers the question your support inbox gets. “Why does this app want access to my email” often arrives days after the connection, when the user has forgotten what they clicked.
Printing scope back to them turns that into a record they can check. It also gives your support team something to point at when a user claims they never granted calendar access.
{{#if scope}}{{#each scope}}{{this}}<br />{{/each}}{{else}}not reported{{/if}}{{this}} is the current item inside {{#each}}, since scope is an array of strings rather than objects. The {{else}} branch covers a payload with no scope list.
Consider translating the raw scope strings into plain descriptions in your own copy. https://www.googleapis.com/auth/gmail.readonly is accurate and means nothing to a user, while “read your email, but not send or delete” is the sentence they need. Either keep the raw values as a reference list, as this template does, or map the scopes your product requests with eq inside the loop: {{#each scope}}{{#if (eq this "https://www.googleapis.com/auth/gmail.readonly")}}Read your email{{/if}}{{/each}}.
How do I create the workflow?
Section titled “How do I create the workflow?”Create a template, then an application-level workflow. grant.created accepts either sender. Omit from and the welcome arrives from the user’s own address, which reads oddly for an onboarding email, so this is a good case for transactional send.
curl -X POST 'https://api.us.nylas.com/v3/workflows' \ -H "Authorization: Bearer $NYLAS_API_KEY" -H 'Content-Type: application/json' \ -d '{ "name": "Welcome, account connected", "trigger_event": "grant.created", "template_id": "<TEMPLATE_ID>", "delay": 0, "is_enabled": true, "from": { "email": "[email protected]", "name": "Your Company" } }'Without from, the workflow sends through the grant that was just created. That mostly works here, since the grant is alive, but it means your welcome email arrives from the user’s own address, which reads oddly at best. A grant authenticated with identity-only scopes can’t send at all, and that failure is silent.
delay: 0 is right. The user is still in your product, and a welcome email that arrives an hour later has lost the context it depends on.
Should I use this to provision other things?
Section titled “Should I use this to provision other things?”No. A workflow can only send email. If connecting an account should also create grant-level workflows, seed a database row, or start a sync, do that from a grant.created webhook in your own code.
Running both is fine and common: the webhook does the provisioning, the workflow sends the email. They’re independent, so a failure in your handler doesn’t stop the welcome going out.
Customize Scheduler email for each user covers the provisioning side, where grant.created is the hook that creates a new customer’s grant-level workflows.
The complete template
Section titled “The complete template”This greets the user by name when the provider supplies one, falls back cleanly when it doesn’t, and lists the granted scopes.
<html>
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Your account is connected</title> <style> body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; line-height: 1.6; color: #333333; margin: 0; padding: 0; background-color: #f9fbfe; font-size: 16px; }
.container { max-width: 600px; margin: 0 auto; padding: 20px; }
.header { text-align: center; padding: 20px 0; }
.content { background-color: #ffffff; padding: 30px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); }
.detail-label { color: #666666; font-size: 14px; padding: 6px 16px 6px 0; vertical-align: top; white-space: nowrap; }
.detail-value { padding: 6px 0; vertical-align: top; }
.footer { text-align: center; padding: 20px 0; color: #666666; font-size: 14px; }
@media only screen and (max-width: 600px) { .container { width: 100% !important; padding: 10px !important; }
.content { padding: 20px !important; } } </style></head>
<body> <div class="container"> <div class="header"> <img src="https://brand.nylas.com/assets/site_images/Nylas-Logo_Horizontal-Blue.png" alt="Nylas Logo" style="max-width: 150px;" /> </div>
<div class="content"> <h1 style="margin-top: 0; color: #1A1A1A;">Your account is connected</h1>
{{#if name}}<p>Thanks, {{name}}.</p>{{else}}<p>Hi there,</p>{{/if}}<p>{{#if email}}<strong>{{email}}</strong>{{else}}Your account{{/if}} is connected and syncing. Here is exactly what you granted access to.</p>
<hr style="border: 0; border-top: 1px solid #eeeeee; margin: 20px 0;" />
<h2 style="color: #4D4D4D; font-weight: 500;">Details</h2>
<table cellpadding="0" cellspacing="0" border="0" style="width: 100%;"> <tr><td class="detail-label">Account</td><td class="detail-value">{{#if email}}{{email}}{{else}}your connected account{{/if}}</td></tr> <tr><td class="detail-label">Provider</td><td class="detail-value">{{#if provider}}{{provider}}{{else}}not reported{{/if}}</td></tr> <tr><td class="detail-label">Permissions</td><td class="detail-value">{{#if scope}}{{#each scope}}{{this}}<br />{{/each}}{{else}}not reported{{/if}}</td></tr> </table> <div style="text-align: center; margin: 30px 0;"><a href="https://example.com/dashboard" style="background-color: #0D6EFD; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Open your dashboard</a></div> <p style="font-size: 14px; color: #666666; text-align: center;">You can disconnect at any time from your account settings.</p> </div>
<div class="footer"> <p>© 2026 <a href="https://www.nylas.com/" style="color: #0D6EFD;">Nylas</a></p> <p style="font-size: 12px; color: #999999; margin-top: 4px;"> {{#if grant_id}}Grant: {{grant_id}}{{/if}} </p> </div> </div></body>
</html>The greeting guard is worth noting, because name is absent on providers that don’t return a profile:
{{#if name}}<p>Thanks, {{name}}.</p>{{else}}<p>Hi there,</p>{{/if}}Render it with POST /v3/templates/render against a payload containing only grant_id as well as a full one. If the stripped version renders, the guards hold.
What’s next
Section titled “What’s next”- Reduce churn by catching inactive accounts is the other half of the account lifecycle
- Customize Scheduler email for each user provisions grant-level workflows from this same event
- Grant notifications for the full
grant.createdpayload - Authentication for the OAuth flow that produces the grant