# Automate email with Nylas Workflows

Source: https://developer.nylas.com/docs/cookbook/workflows/

A workflow watches for something happening in Nylas and sends an email when it does. A booking is made, a meeting gets recorded, an account stops syncing, and the right person hears about it without you writing a webhook handler or a send call.

Every recipe in this section follows the same shape. This page covers the parts they share: the objects involved, the triggers available, and the one constraint that decides which triggers you can safely build on.

## What are Nylas Workflows?

A workflow binds one trigger event to one email template and one sender address. When the event fires, Nylas renders the template against the event payload and sends the result. There's no code step in between, so a workflow can send email and nothing else.

Three objects are involved, created in this order:

| Object | Holds | Endpoint |
| --- | --- | --- |
| Template | Subject and HTML body with `{{variable}}` placeholders | [`/v3/templates`](/docs/reference/api/application-level-templates/) |
| Workflow | Trigger event, template ID, sender, delay | [`/v3/workflows`](/docs/reference/api/application-level-workflows/) |
| Trigger source | The Scheduler configuration, Notetaker session, or grant that produces the event | varies by recipe |

The template does the work. Every field in the event payload is available as a variable, so the email can name the meeting, format the time in the recipient's timezone, and link to the recording. [Templates and workflows](/docs/v3/email/templates-workflows/) documents the variable model and the `formatDate` helper.

## Which events can trigger a workflow?

Workflows accept 34 trigger events across 9 categories, covering Scheduler, Notetaker, the calendar, the mailbox, and the account lifecycle.

| Category | Triggers |
| --- | --- |
| Scheduler | `booking.created`, `booking.pending`, `booking.rescheduled`, `booking.cancelled`, `booking.reminder` |
| Notetaker | `notetaker.created`, `notetaker.updated`, `notetaker.deleted`, `notetaker.meeting_state`, `notetaker.media` |
| Grants | `grant.created`, `grant.updated`, `grant.expired`, `grant.deleted`, `grant.imap_sync_completed` |
| Events | `event.created`, `event.updated`, `event.deleted` |
| Calendar | `calendar.created`, `calendar.updated`, `calendar.deleted` |
| Folders | `folder.created`, `folder.updated`, `folder.deleted` |
| Messages | `message.created`, `message.updated`, `message.send_success`, `message.send_failed`, `message.bounce_detected` |
| Message tracking | `message.opened`, `message.link_clicked`, `thread.replied` |
| Contacts | `contact.updated`, `contact.deleted` |

Every trigger except 2 accepts either sender: omit `from` to send from the mailbox of the grant that triggered the event, or set `from` to send through transactional send on your verified domain. `grant.expired` and `grant.deleted` require `from`, because the grant that triggered them can no longer send mail.

The deliverability triggers aren't available. `message.transactional.bounced`, `message.transactional.complaint`, `message.transactional.delivered`, `message.transactional.rejected`, and the 4 equivalent Agent Accounts triggers are all rejected. Handle those through a [webhook](/docs/reference/notifications/) and your suppression list instead, because answering a bounce or a complaint with another email is the wrong response.

## Which triggers are safe to build on?

Workflows don't filter. An enabled workflow sends once for every matching event in its scope, so the trigger's natural volume is the volume of email your recipients get. A template can change its wording with `eq`, but it can't skip the send.

| Volume | Triggers | Suitable for |
| --- | --- | --- |
| One per deliberate action | `booking.*`, `grant.*`, `notetaker.*` | Customer-facing email. Start here. |
| One per calendar change | `event.*`, `calendar.*` | Customer-facing email, if your users' calendars aren't busy |
| One per message | `message.created`, `message.updated`, `message.opened`, `message.link_clicked`, `thread.replied` | Internal alerts, or grant-level workflows on low-volume mailboxes. See below. |

The message triggers are the ones to size first. A `message.created` workflow sends once per message arriving in every connected mailbox in scope, so on an application with a few thousand grants that's thousands of sends an hour from your domain. Use a webhook when you need to decide per event whether to send, and workflows when every occurrence of the event deserves an email.

## Should the workflow live on the application or the grant?

Application-level workflows fire for every grant in the application and are managed from the Dashboard. Grant-level workflows fire for a single grant and let each of your customers have their own branding, at the cost of creating and maintaining one workflow per grant.

| | Application-level | Grant-level |
| --- | --- | --- |
| Endpoint | `POST /v3/workflows` | `POST /v3/grants/{grant_id}/workflows` |
| Fires for | Every grant in the application | One grant |
| How many to manage | 1 per trigger | 1 per trigger per grant |
| Visible in the Dashboard | Yes | No |
| Good for | One product voice for all users | Per-customer branding and copy |

Both levels fire independently. An application-level workflow and a grant-level workflow on the same trigger produce 2 messages to the same person, and because the Dashboard lists only application-level workflows, the grant-level one is easy to forget. If a recipient reports duplicate email, list both scopes before anything else.

[Customize Scheduler email for your app](/docs/cookbook/workflows/scheduler-booking-email/) and [Customize Scheduler email for each user](/docs/cookbook/workflows/scheduler-email-per-user/) work through the same feature at both levels, including what per-customer templates cost to run.

## Which sender should the workflow use?

A workflow sends from one of 2 places, and the choice is the first thing to settle. Omit `from` and the message goes out through the grant that triggered the event, from that user's real mailbox. Set `from` and it goes through transactional send on a domain you registered with Nylas.

| | Send from the grant | Transactional send |
| --- | --- | --- |
| Set `from`? | no | yes |
| Appears to come from | the user's own address | your product's domain |
| Setup needed | mail send scopes on the grant | a registered domain and 4 DNS records |
| Replies go to | the user's mailbox | wherever your domain routes |
| Lands in the user's Sent folder | yes | no |
| Counts against the provider's sending limits | yes | no |
| Still works when the grant breaks | no | yes |

Send from the grant when the message should look like it came from a person. A Scheduler confirmation from the host's own address reads like the host sent it, arrives in the same thread as their other correspondence, and puts replies in the host's inbox where they expect them. It also needs no domain setup at all, which makes it the fastest path to a working workflow.

Use transactional send when the message is from your product rather than from a person, when you need consistent branding across every customer, or when you can't rely on the grant. Account lifecycle email is the clearest case: a reconnect prompt can't go through the mailbox that just stopped working.

Two triggers have no choice. `grant.expired` and `grant.deleted` require `from`, and return `400 This event type requires transactional send. Please provide a sender email address.` without it, for exactly that reason.

Grant sending goes out through the provider's own API rather than through Nylas infrastructure. On a Google grant, a message sent this way arrives with `Received: by gmailapi.google.com with HTTPREST`, a Gmail-issued `Message-Id`, and no `Reply-To`, so replies return to the sender's address. Because it's a real send from that mailbox, it's subject to the provider's per-mailbox sending limits in a way transactional send isn't.

The failure mode for grant sending is quiet. A grant authenticated with identity-only scopes can't send mail, so the workflow writes `Grant is not valid or does not have required scopes to send emails` to the log, delivers nothing, and returns no error to you. If you rely on grant sending, check the scopes when the grant is created rather than when a user reports a missing email.

## What do I need before I start?

An API key, a template, and, if you chose transactional send above, a registered sending domain. Workflows that send through the grant need no domain setup, so skip ahead and go straight to creating the workflow.

This is the same domain registration that [Nylas Agent Accounts](/docs/v3/agent-accounts/) use, with one difference: workflows only send, so you only publish the outbound records. Nylas tracks 5 verification types, and send-only needs 4 of them.

| Type | Record | Needed for workflows | Needed for Agent Accounts |
| --- | --- | --- | --- |
| `ownership` | TXT | yes | yes |
| `dkim` | TXT | yes | yes |
| `spf` | TXT | yes | yes |
| `feedback` | MX | yes | yes |
| `mx` | MX | no | yes |

The `mx` type is the one to leave off. It routes inbound mail to Nylas, so on a domain that already receives email it replaces the MX records pointing at Google Workspace or Microsoft 365. Skip it and your existing mail keeps flowing untouched while the domain sends through Nylas. Add it only when you also want [Agent Accounts](/docs/v3/agent-accounts/) receiving replies at that domain.

The `feedback` MX is safe despite being an MX record, because it goes on a subdomain used as the return path rather than on the domain you receive mail at.

### Register through the Dashboard or the API

Use the [Dashboard](https://dashboard-v3.nylas.com/organization/domains) if you're setting up one domain for your own product. It lists every record with a copy button and a **Copy zone file** button that copies them all at once.

Use the [Manage Domains API](/docs/v3/email/domains/) when your customers bring their own sending domains and you want the flow inside your product. The sequence is 3 calls:

```text
# 1. register the domain
POST /v3/admin/domains

# 2. fetch the DNS records to show the customer, once per verification type
POST /v3/admin/domains/{domain_id}/info

# 3. check them once the customer has published them
POST /v3/admin/domains/{domain_id}/verify
```

One thing to plan for: the Manage Domains API uses [Nylas Service Account authentication](/docs/v3/auth/nylas-service-account/), which signs each request with the `X-Nylas-Kid`, `X-Nylas-Timestamp`, `X-Nylas-Nonce`, and `X-Nylas-Signature` headers. A plain API key returns `401 Nonce is required`, so this runs from your own server with credentials Nylas issues separately, not from the same key your workflow calls use. The API also needs a contract that includes domain management. [Nylas Support](mailto:support@nylas.com) issues the Service Account credentials.

Some record values expire, so call `/info` again for fresh values rather than caching what you showed the customer last week. Nylas recommends a subdomain such as `mail.example.com` for sending, which keeps the records away from your main domain.

### Create the workflow

Decide which of the 2 senders the workflow uses, because it changes what you have to set up and what the recipient sees. Omitting `from` sends through the grant that fired the event, so the message comes from that user's own mailbox. Setting `from` sends through transactional send on a domain you registered, so it comes from your product.

```bash
curl -X POST 'https://api.us.nylas.com/v3/workflows' \
  -H "Authorization: Bearer $NYLAS_API_KEY" -H 'Content-Type: application/json' \
  -d '{
    "name": "Booking confirmed",
    "trigger_event": "booking.created",
    "template_id": "<TEMPLATE_ID>",
    "delay": 0,
    "is_enabled": true,
    "from": { "email": "hello@yourdomain.com", "name": "Your Company" }
  }'
```

`delay` is in minutes and `0` sends immediately. Set a delay when the recipient might act before the email is useful, such as a reconnect prompt that would cross with the user reconnecting.

## Recipes

**Scheduler**

- [Customize Scheduler email for your app](/docs/cookbook/workflows/scheduler-booking-email/) replaces the built-in booking email for every user
- [Customize Scheduler email for each user](/docs/cookbook/workflows/scheduler-email-per-user/) gives each customer their own branding
- [Tell Scheduler attendees the meeting is recorded](/docs/cookbook/workflows/recording-notice-scheduler/) pairs a booking with Notetaker

**Calendar**

- [Email attendees when a meeting is booked](/docs/cookbook/workflows/event-booked-email/) covers meetings created through the Events API
- [Tell Events API attendees the meeting is recorded](/docs/cookbook/workflows/recording-notice-events/) does the same for calendar-created meetings

**Notetaker**

- [Send the recording when Notetaker media is ready](/docs/cookbook/workflows/notetaker-media-ready/) delivers the recording and transcript
- [Tell the host when Notetaker couldn't join](/docs/cookbook/workflows/notetaker-failed-to-join/) catches waiting-room failures

**Accounts**

- [Welcome users when they connect an account](/docs/cookbook/workflows/welcome-connected-account/) confirms what was granted
- [Reduce churn by catching inactive accounts](/docs/cookbook/workflows/reduce-churn-inactive-accounts/) recovers grants before the user leaves

**Debugging**

- [Why isn't my workflow sending email?](/docs/cookbook/workflows/workflow-not-sending/) covers the silent failure everyone hits first