Notifications
Keel lets you send transactional emails directly from your custom functions, action hooks, jobs, subscribers, and flows using the notify API. No schema changes are required to start sending emails.
Every notification sent is logged and can be viewed in the Console, including its rendered content, per-recipient delivery status, and any attachments.
Sending an email
Import notify from @teamkeel/sdk and call notify.email().
import { notify } from '@teamkeel/sdk';
await notify.email({
recipients: { to: { emails: 'customer@example.com' } },
subject: 'Your order has shipped',
content: { body: 'Your order #1234 is on its way.' },
});notify.email() can be called from anywhere your function code runs - a write action's function, an action hook, a job, a subscriber responding to an event, or a step in a flow.
Recipients
The recipients input has to, cc, and bcc groups. Each group accepts one or more plain email addresses, User/Identity records (or their ids), or team names - Keel resolves these to actual email addresses when the notification is sent.
model Order {
fields {
customer Customer
total Decimal
}
}
model Customer {
fields {
name Text
email Text
}
}await notify.email({
recipients: {
to: { identities: order.customer.identityId },
cc: { emails: ['ops@keel.so', 'billing@keel.so'] },
bcc: { teams: 'Finance' },
},
subject: `Order #${order.id} has shipped`,
content: { body: 'Your order is on its way.' },
});You can also set replyTo, using the same shape as a recipient group, to control where replies to the email are sent.
await notify.email({
recipients: { to: { emails: customer.email } },
replyTo: { emails: 'support@example.com' },
subject: 'Order confirmed',
content: { body: 'Thanks for your order!' },
});Template emails
The content field accepts either a structured object or a raw HTML string. Passing an object renders your email through Keel's built-in stock template, giving you a consistent, on-brand layout without writing any HTML.
await notify.email({
recipients: { to: { emails: customer.email } },
subject: 'Order confirmed',
content: {
title: 'Thanks for your order!',
body: `Hi ${customer.name},\n\nWe've received order #${order.id} and it's being prepared for shipping.`,
actions: [
{ label: 'Track your order', url: `https://shop.example.com/orders/${order.id}` },
],
},
});title- an optional heading shown at the top of the emailbody- plain text content. Newlines are rendered as line breaks; markdown is not interpretedactions- an optional list of{ label, url }buttons rendered below the body
The stock template is the only built-in template. If you need full control over layout and branding, pass a raw HTML string as content instead.
Custom HTML emails
To take full control of the email's design, pass content as a string. Keel sends it as-is, with no template applied.
await notify.email({
recipients: { to: { emails: customer.email } },
subject: 'Order confirmed',
content: `
<html>
<body style="font-family: sans-serif; color: #1a1a1a;">
<h1>Thanks for your order, ${customer.name}!</h1>
<p>Order #${order.id} is confirmed and being prepared for shipping.</p>
<a href="https://shop.example.com/orders/${order.id}" style="color: #2563eb;">
Track your order
</a>
</body>
</html>
`,
});When content is a string, Keel does not wrap or modify it in any way. Your HTML must be a complete, self-contained document - include your own <html>/<body> tags and inline styles, since most email clients ignore <style> blocks.
Attachments
Attach files using the attachments input, which accepts the same File and InlineFile types used elsewhere in Keel. See the Files documentation for more on the difference between the two.
- A
File(for example a value read from a model field) is copied server-side when the notification is sent, so it stays attached even if the original record is later changed or deleted. - An
InlineFileis generated in your function and stored fresh when the notification is sent.
import { notify, InlineFile } from '@teamkeel/sdk';
const summary = new InlineFile({ filename: 'summary.pdf', contentType: 'application/pdf' });
summary.write(pdfBuffer);
await notify.email({
recipients: { to: { emails: customer.email } },
subject: 'Your invoice',
content: { body: 'Please find your invoice and order summary attached.' },
attachments: [order.invoice, summary],
});Attachments are limited to 10MB per file, with a combined limit of 40MB across the subject, content, and all attachments in a single email.
Viewing notifications in the Console
Every notification sent from an environment appears on the Notifications page in the Console, with a list and detail view for each one showing:
- The subject and rendered email content
- Recipients, grouped as To / Cc / Bcc, plus the reply-to address if set
- A delivery status for each recipient - Queued, Sent, or Failed (with the failure reason)
- Any attachments, which can be downloaded directly from the Console
Viewing notifications in the Console requires the Notifications role. This role is held automatically by the Administrators team, and can be assigned to other teams from Settings > Team.
Setting up email notifications
Before your environment can send emails, you need to configure and verify a sender.
- In the Console sidebar, navigate to Notifications, then its Settings sub-menu item.
- Under the Email channel, enter a Sender email (for example
no-reply@yourdomain.com) and, optionally, a Sender name (for exampleAcme). - Click Save sender. Keel generates CNAME DNS records for your sender's domain.
- Add the CNAME records shown in the Console to your domain's DNS configuration with your DNS provider.
- Once the records have propagated, the sender's status updates to Verified. You can trigger a manual check with Check verification.
- Turn on the Email channel toggle so notifications can start being delivered.
DNS propagation can take anywhere from a few minutes up to 24 hours, depending on your DNS provider.
Verification is checked at the domain level. Changing only the sender name or the part of the address before the @ keeps your existing verification. Changing the domain itself resets verification, and email sending pauses until the new domain is verified. A domain can only be verified for one project at a time.