Notetaker joins meetings as a guest. If the meeting has a waiting room, or is limited to people in the organization, somebody has to let it in. When nobody does within 10 minutes of the join time it gives up, and without a notice the host finds out when they look for a recording that doesn’t exist.
A notetaker.meeting_state workflow tells them while the meeting is still happening, when they can still fix it.

When does notetaker.meeting_state fire?
Section titled “When does notetaker.meeting_state fire?”On every change to the Notetaker’s state during a meeting. The payload carries 2 fields that together say what happened: status holds one of 4 top-level states, and meeting_state holds one of 20 sub-states explaining the reason.
status | Means |
|---|---|
connecting | Joining the meeting |
attending | In the meeting |
disconnected | No longer in the meeting |
failed_entry | Never got in |
meeting_state is where the reason lives, and the 20 values group into 5 kinds of outcome.
| Group | meeting_state values | Who can fix it |
|---|---|---|
| Waiting or refused entry | waiting_for_entry, entry_denied, admission_timeout, sign_in_required, meeting_capacity_reached | The host, during the meeting |
| Bad link or code | bad_meeting_code, bad_meeting_link, cannot_join | Whoever scheduled it |
| Ran normally | recording_active, meeting_ended, no_participants, no_activity | Nobody, this is the happy path |
| Removed from the meeting | kicked, api_request, cancelled | Depends on who removed it |
| Something broke | network_error, no_response, error, internal_error, unknown | You, from the logs |
The first 2 groups are the ones worth emailing about, because somebody can act on them while the meeting is still running. admission_timeout and waiting_for_entry are the most common: Notetaker joins as a guest, so a waiting room or an org-only meeting needs somebody to admit it, and it gives up 10 minutes after the scheduled join time.
sign_in_required and entry_denied are worth separating in your copy. The first means the platform wants an account, the second means a human refused the bot, and the advice differs.
How many of these will the workflow send?
Section titled “How many of these will the workflow send?”One per state change. A workflow on notetaker.meeting_state sends on every transition, including connecting and attending on a meeting that went fine.
Two ways to handle that:
Write the template to suit every state. The template in this recipe branches on status and meeting_state with the eq helper, so each message describes what actually happened: joining, recording, left, or couldn’t get in. This suits an internal or operations address, or a host who wants live updates.
Use the webhook to send only on failures. Subscribe to notetaker.meeting_state, check status in your handler, and send only on failed_entry through transactional send with the same template. Use this for customer-facing mail where one message per meeting is the right amount.
What should the email say?
Section titled “What should the email say?”Explain the cause, because failed_entry and admission_timeout mean nothing to the person reading them. The host doesn’t know Notetaker joins as a guest, and will assume the integration is broken rather than that a waiting room blocked it.
For a waiting-room failure, the template states three things: the recording doesn’t exist, the Notetaker was waiting to be admitted, and it waited 10 minutes. The fix is for the host to admit the bot next time. A bad link or a sign-in requirement gets its own sentence, because the fix there is on the calendar event or the meeting settings instead.
Include the meeting link so the host can jump back in if the call is still running. meeting_link is in the payload, and on a 60-minute meeting a notice at minute 10 still leaves time to admit the bot and capture what remains.
How do I create the workflow?
Section titled “How do I create the workflow?”Point an application-level workflow at notetaker.meeting_state.
curl -X POST 'https://api.us.nylas.com/v3/workflows' \ -H "Authorization: Bearer $NYLAS_API_KEY" -H 'Content-Type: application/json' \ -d '{ "name": "Notetaker join failure", "trigger_event": "notetaker.meeting_state", "template_id": "<TEMPLATE_ID>", "delay": 0, "is_enabled": true, "from": { "email": "[email protected]", "name": "Your Company" } }'Use delay: 0. A notice about a meeting in progress is worthless five minutes late.
The notetaker.meeting_state payload has no participant list. Send a test to confirm which address receives the message before you enable this for customers.
How does the template handle the payload?
Section titled “How does the template handle the payload?”The template picks its wording with eq, checking status first and then meeting_state for the detail. Handlebars {{else if}} keeps the chain flat:
{{#if (eq status "failed_entry")}} {{#if (eq meeting_state "bad_meeting_link")}}The meeting link didn't work. {{else if (eq meeting_state "entry_denied")}}Somebody declined to admit the Notetaker. {{else}}The Notetaker was waiting to be admitted. {{/if}}{{else if (eq status "disconnected")}} The Notetaker has left the meeting.{{else if (eq status "attending")}} The Notetaker is in the meeting and recording.{{else}} The Notetaker is joining.{{/if}}eq on a missing top-level field is false rather than an error, so a transition without meeting_state falls through to the {{else}} branch. There’s no or helper, so each value you want to handle gets its own {{else if}}.
The Details table prints status and meeting_state as they arrive, each in its own guard:
{{#if status}}{{status}}{{/if}}{{#if meeting_state}}{{meeting_state}}{{/if}}The raw values are there on purpose. failed_entry and waiting_for_entry are what a host will paste into a support conversation, and matching the string they saw to the string in your logs saves a round of questions.
meeting_provider is guarded inline so the sentence reads correctly whether or not it’s present:
your {{#if meeting_provider}}{{meeting_provider}} {{/if}}meetingWith the provider it reads “your Google Meet meeting”, without it “your meeting”. The trailing space inside the guard is deliberate.
The complete template
Section titled “The complete template”<html>
<head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Notetaker update</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"> {{#if (eq status "failed_entry")}} <h1 style="margin-top: 0; color: #1A1A1A;">Your Notetaker couldn't join</h1> {{#if recipient}}{{#if recipient.first_name}}<p>Hi {{recipient.first_name}},</p>{{else}}<p>Hi there,</p>{{/if}}{{else}}<p>Hi there,</p>{{/if}} <p>The Notetaker couldn't get into your {{#if meeting_provider}}{{meeting_provider}} {{/if}}meeting, so there is no recording or transcript for it.</p> {{#if (eq meeting_state "bad_meeting_link")}}<p>The meeting link didn't work. Check the link on the calendar event and send the Notetaker again.</p> {{else if (eq meeting_state "bad_meeting_code")}}<p>The meeting code didn't work. Check the code on the calendar event and send the Notetaker again.</p> {{else if (eq meeting_state "sign_in_required")}}<p>The meeting only admits signed-in users. Allow guests to join, then send the Notetaker again.</p> {{else if (eq meeting_state "entry_denied")}}<p>Somebody in the meeting declined to admit the Notetaker.</p> {{else if (eq meeting_state "meeting_capacity_reached")}}<p>The meeting was full.</p> {{else}}<p>The Notetaker joins as a guest. If the meeting has a waiting room, or is limited to people in your organization, somebody has to admit it. It gives up 10 minutes after the scheduled join time.</p> {{/if}} {{else if (eq status "disconnected")}} <h1 style="margin-top: 0; color: #1A1A1A;">Your Notetaker left the meeting</h1> {{#if recipient}}{{#if recipient.first_name}}<p>Hi {{recipient.first_name}},</p>{{else}}<p>Hi there,</p>{{/if}}{{else}}<p>Hi there,</p>{{/if}} {{#if (eq meeting_state "kicked")}}<p>Somebody removed the Notetaker from your {{#if meeting_provider}}{{meeting_provider}} {{/if}}meeting. It recorded up to that point.</p> {{else if (eq meeting_state "meeting_ended")}}<p>The meeting ended and the Notetaker has left. The recording and transcript are being processed.</p> {{else}}<p>The Notetaker has left your {{#if meeting_provider}}{{meeting_provider}} {{/if}}meeting. The reason is below.</p> {{/if}} {{else if (eq status "attending")}} <h1 style="margin-top: 0; color: #1A1A1A;">Your Notetaker is in the meeting</h1> {{#if recipient}}{{#if recipient.first_name}}<p>Hi {{recipient.first_name}},</p>{{else}}<p>Hi there,</p>{{/if}}{{else}}<p>Hi there,</p>{{/if}} <p>The Notetaker is in your {{#if meeting_provider}}{{meeting_provider}} {{/if}}meeting and recording.</p> {{else}} <h1 style="margin-top: 0; color: #1A1A1A;">Your Notetaker is joining</h1> {{#if recipient}}{{#if recipient.first_name}}<p>Hi {{recipient.first_name}},</p>{{else}}<p>Hi there,</p>{{/if}}{{else}}<p>Hi there,</p>{{/if}} <p>The Notetaker is joining your {{#if meeting_provider}}{{meeting_provider}} {{/if}}meeting. If the meeting has a waiting room, admit it when it appears.</p> {{/if}}
<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">Meeting</td><td class="detail-value">{{#if meeting_link}}<a href="{{meeting_link}}" style="color: #0D6EFD;">{{meeting_link}}</a>{{/if}}</td></tr> <tr><td class="detail-label">Status</td><td class="detail-value">{{#if status}}{{status}}{{/if}}</td></tr> <tr><td class="detail-label">Reason</td><td class="detail-value">{{#if meeting_state}}{{meeting_state}}{{/if}}</td></tr> </table> <div style="text-align: center; margin: 30px 0;"><a href="{{#if meeting_link}}{{meeting_link}}{{/if}}" style="background-color: #0D6EFD; color: white; padding: 12px 24px; text-decoration: none; border-radius: 4px; display: inline-block;">Open the meeting</a></div> </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 id}}Notetaker: {{id}}{{/if}} </p> </div> </div></body>
</html>Render it with POST /v3/templates/render once for each status value, and once with only id present. Notetaker troubleshooting covers the join failures this email reports.
What’s next
Section titled “What’s next”- Send the recording when Notetaker is ready for the successful path
- Notetaker troubleshooting explains each join failure and how to prevent it
- Why isn’t my workflow sending email? for guard rules and render testing
- Notetaker notifications for the full
notetaker.meeting_statepayload