How to customize email notifications
Email notifications are a powerful way to keep users informed about important events in your app. By customizing notifications, you can control the content, layout, recipients, and appearance of emails sent from your actions.
This guide shows you different methods to send and customize email notifications, from simple text messages to complex HTML templates using the LiquidParser template engine.
Overview
You can customize email notifications by:
- sending simple text notifications
- sending HTML-formatted emails
- using advanced recipient options (CC, BCC, reply-to)
- creating dynamic content with LiquidParser templates
- overriding sender information and email properties
Prerequisites
Before implementing custom email notifications, ensure you have:
- an action that triggers the notification (or create one)
- recipient email addresses or user IDs
- basic understanding of TypeScript
- for advanced scenarios: familiarity with Liquid template syntax
Method 1: Sending a simple email notification
The simplest way to send an email is by returning a NotifyContext object from a special action function.
In your actions/logic/index.ts file, create or modify an action to include the __sendNotifications function:
import { entity } from "#typings";
import type { ActionLogic } from "@comind/api";
import type { NotifyContext } from "@comind/api/server";
export default {
__sendNotifications: sendNotifications,
// ... other actions
} satisfies ActionLogic<typeof entity>;
function sendNotifications() {
const notifyTo = [];
notifyTo.push(...(entity.assignees_list__list || []).map((u) => u.id));
notifyTo.push("someone@company.com");
const notificationsContext: NotifyContext = {
email_body: "Almost empty notification",
email_layout: "SimpleLayoutNotification",
subject: entity.title,
to: notifyTo.filter(Boolean),
};
return notificationsContext;
}
How it works
-
Function naming: the
__sendNotificationsfunction is a special reserved name that the system recognizes for sending notifications. It will be triggered automatically after your action executes. -
Recipients array: the
notifyToarray collects all recipients. You can add:- user IDs from relationship fields (like
assignees_list__list) - direct email addresses as strings
- any combination of both
- user IDs from relationship fields (like
-
NotifyContext object: this object defines all notification properties:
email_body: the main content of the email (plain text or HTML)email_layout: the email template to use ('SimpleLayoutNotification'or'RecordNotification')subject: the email subject lineto: array of recipients (user IDs or email addresses)
-
Return value: returning the
NotifyContextobject triggers the email to be sent.
Method 2: Sending HTML-formatted emails
To send more visually appealing emails, you can include HTML in the email_body:
function sendNotifications() {
const notifyTo = [];
notifyTo.push(...(entity.assignees_list__list || []).map((u) => u.id));
notifyTo.push("example_email@gmail.com");
const htmlContent = `
<h2>Task Updated: ${entity.title}</h2>
<p>The following task has been updated:</p>
<ul>
<li><b>Status:</b> ${entity.state}</li>
<li><b>Priority:</b> ${entity.priority}</li>
<li><b>Due Date:</b> ${entity.due_date}</li>
</ul>
<p>Please review the changes and take appropriate action.</p>
`;
const notificationsContext: NotifyContext = {
email_body: htmlContent,
email_layout: "SimpleLayoutNotification",
subject: `Task Updated: ${entity.title}`,
to: notifyTo.filter(Boolean),
};
return notificationsContext;
}
You can use any HTML tags for formatting:
<h1>,<h2>,<h3>for headings<p>for paragraphs<b>and<strong>for bold text<i>and<em>for italic text<ul>and<li>for lists<a href="...">for links<table>,<tr>,<td>for tables
Use inline CSS styles for better email client compatibility. Many email clients strip out <style> tags and external stylesheets.
Advanced NotifyContext properties
The NotifyContext interface provides many options for customizing your emails:
export interface NotifyContext {
to?: NotifyRecipient;
cc?: NotifyRecipient;
bcc?: NotifyRecipient;
replyTo?: NotifyRecipient;
email_note?: string | null;
override_from_email?: string | null;
override_from_email_name?: string | null;
override_item_prefix?: string | null;
override_unsubscribe_text?: string | null;
attachment_fields?: string;
returnPath?: string | null;
subject?: string | null;
email_body?: string | null;
email_layout?: "RecordNotification" | "SimpleLayoutNotification";
altBox?: string | null;
access_email_data_as?: NotifyContextAccess;
}
Common properties explained
to,cc,bcc: recipient lists for main recipients, carbon copy, and blind carbon copyreplyTo: email address that will receive repliessubject: email subject lineemail_body: main email content (plain text or HTML)email_layout: choose between'SimpleLayoutNotification'(minimal wrapper) or'RecordNotification'(includes record details)override_from_email: custom sender email addressoverride_from_email_name: custom sender display nameattachment_fields: comma-separated list of attachment field names to includeemail_note: additional note displayed at the top of the emailoverride_unsubscribe_text: custom unsubscribe link textaltBox: alternative email box routing prefix for replies
Example using advanced properties
function sendNotifications() {
const notificationsContext: NotifyContext = {
to: [entity.c_responsible],
cc: [entity.c_supervisor],
bcc: ["admin@company.com"],
replyTo: "noreply@company.com",
subject: `Action Required: ${entity.title}`,
email_body: "<h2>Please review this item</h2>",
email_layout: "SimpleLayoutNotification",
override_from_email_name: "Project Management System",
attachment_fields: "attachments,c_contract",
};
return notificationsContext;
}
Method 3: Using LiquidParser for dynamic templates
For more complex scenarios, you can use the LiquidParser template engine to create dynamic email content with loops, conditionals, and variable substitution.
Step 1: Create the template
Define your Liquid template as a string constant:
export const digestTemplate = `
{% assign style = Vars.inline_style %}
<h1>{{ Vars.heading }}</h1>
<p>
This is daily digest: <a href="web2.aspx/{{ Vars.digest_url }}">{{ Vars.digest_url }}</a>
</p>
{% if Vars.records.size == 0 %}
<i>There are no records</i>
{% else %}
<table style="{{ style.table }}">
<tr class="headingRow">
<td style="{{ style.headingRowTd }}">Number</td>
<td style="{{ style.headingRowTd }}">Link</td>
<td style="{{ style.headingRowTd }}">State</td>
</tr>
{% for r in Vars.records %}
<tr>
<td style="{{ style.rowTd }}">{{ r.number }}</td>
<td style="{{ style.rowTd }}"><a href="{{ r.url }}">{{ r.title }}</a></td>
<td style="{{ style.rowTd }}">{{ r.state }}</td>
</tr>
{% endfor %}
</table>
{% endif %}
`;
Step 2: Create the rendering function
Create a function that prepares the data and renders the template:
import { LiquidParser } from "@comind/api/server";
function composeBodyWithTemplate(records: any[]) {
const renderContext = {
digest_url: `${entity.project_alias}/${entity.publishing_alias}${entity.number}`,
heading: entity.title,
inline_style: getPrettyTableInlineStyle(),
records,
};
const body = LiquidParser.parse(digestTemplate, renderContext);
return body;
}
function getPrettyTableInlineStyle() {
return {
table: "border-collapse: collapse; width: 100%; margin: 20px 0;",
headingRowTd:
"background-color: #f0f0f0; padding: 10px; border: 1px solid #ddd; font-weight: bold;",
rowTd: "padding: 8px; border: 1px solid #ddd;",
};
}
Step 3: Use in your notification function
function sendNotifications() {
// Fetch or prepare your data
const records = [
{
number: "TASK101",
title: "Complete documentation",
state: "Active",
url: "PROJECT/TASK101",
},
{
number: "TASK102",
title: "Review code",
state: "Elaborating",
url: "PROJECT/TASK102",
},
];
const htmlBody = composeBodyWithTemplate(records);
const notificationsContext: NotifyContext = {
email_body: htmlBody,
email_layout: "SimpleLayoutNotification",
subject: `Daily Digest: ${entity.title}`,
to: entity.c_subscribers__list?.map((u) => u.id) || [],
};
return notificationsContext;
}
Liquid template syntax overview
The LiquidParser supports standard Liquid syntax. For complete documentation on Liquid syntax, tags, and filters, see the official Liquid documentation.
Variables: {{ Vars.variable_name }}
Conditionals:
{% if Vars.condition %}
Content when true
{% else %}
Content when false
{% endif %}
Loops:
{% for item in Vars.items %}
{{ item.name }}
{% endfor %}
Assignments:
{% assign myVar = Vars.someValue %}
Filters:
{{ Vars.text | upcase }}
{{ Vars.number | plus: 5 }}
Common Liquid filters you can use include: upcase, downcase, capitalize, append, prepend, plus, minus, times, divided_by, date, join, split, replace, and many more. See the Liquid filters documentation for a complete list.
Use cases
Custom email notifications are particularly useful for:
- Task assignments: notify users when tasks are assigned to them
- Status updates: alert stakeholders when record status changes
- Daily/weekly digests: send periodic summaries of pending items
- Approval workflows: request approvals with formatted details
- Report distribution: send automated reports with data tables
- Deadline reminders: alert users about upcoming due dates
- Team collaboration: notify team members about comments or updates
Best practices
-
Test with different email clients: email rendering varies across clients (Gmail, Outlook, Apple Mail). Test your HTML emails in multiple clients.
-
Use inline CSS: avoid external stylesheets and
<style>blocks. Use inlinestyleattributes instead. -
Keep it simple: complex layouts may not render consistently. Stick to basic HTML and tables for structure.
-
Provide plain text alternative: consider the
email_layoutoption.SimpleLayoutNotificationis more reliable across email clients. -
Validate recipients: always filter out null/undefined recipients with
.filter(Boolean)before sending. -
Handle missing data: check if fields exist before including them in templates to avoid errors.
-
Use meaningful subjects: make subject lines descriptive and actionable.
-
Consider email volume: for high-frequency notifications, consider batching or digest options.
Email layout options
SimpleLayoutNotification
Minimal layout that wraps your content with basic email structure:
- Best for custom HTML content
- Includes minimal header and footer
- You control most of the content styling
- Recommended when using LiquidParser templates
RecordNotification
Full layout that includes record context:
- Displays record details automatically
- Includes app branding
- Shows record fields based on layout settings
- Best for standard record update notifications
Troubleshooting
Emails not being sent
- Verify that your action is being triggered
- Check that recipients array is not empty
- Ensure email addresses are valid strings
- Confirm that
__sendNotificationsis exported in the default object
HTML not rendering correctly
- Use inline CSS styles instead of style blocks
- Test with simple HTML first, then add complexity
- Avoid advanced CSS features (flexbox, grid)
- Use tables for layout structure
Variables not showing in template
- Verify that variable names match the
renderContextobject - Use
Vars.prefix in Liquid templates - Check for typos in variable names
- Ensure data is passed correctly to
LiquidParser.parse()
Recipients not receiving emails
- Check spam folders
- Verify email addresses are correct
- Ensure users have notification preferences enabled
- Check system email logs if available
Example: Complete notification action
Here's a complete example combining multiple techniques:
import { entity } from "#typings";
import type { ActionLogic } from "@comind/api";
import type { NotifyContext } from "@comind/api/server";
import { LiquidParser } from "@comind/api/server";
import moment from "moment";
export default {
send_weekly_summary: sendWeeklySummary,
__sendNotifications: sendNotifications,
} satisfies ActionLogic<typeof entity>;
const summaryTemplate = `
<h1>Weekly Summary: {{ Vars.week }}</h1>
<h2>Completed Tasks ({{ Vars.completed.size }})</h2>
{% if Vars.completed.size > 0 %}
<ul>
{% for task in Vars.completed %}
<li><b>{{ task.title }}</b> - completed by {{ task.assignee }}</li>
{% endfor %}
</ul>
{% else %}
<p><i>No tasks completed this week</i></p>
{% endif %}
<h2>Pending Tasks ({{ Vars.pending.size }})</h2>
{% if Vars.pending.size > 0 %}
<ul>
{% for task in Vars.pending %}
<li><b>{{ task.title }}</b> - assigned to {{ task.assignee }}</li>
{% endfor %}
</ul>
{% else %}
<p><i>No pending tasks</i></p>
{% endif %}
`;
function sendWeeklySummary() {
// Your action logic here
entity.c_last_summary_sent = new Date().toISOString();
}
function sendNotifications() {
// retrieve tasks (hardcoded for demo purposes)
const completedTasks = [
{ title: "Update documentation", assignee: "John" },
{ title: "Fix bug #123", assignee: "Sarah" },
];
const pendingTasks = [{ title: "Review pull request", assignee: "Mike" }];
const week = `${moment().startOf("week").format("MM/DD/YYYY")} - ${moment()
.endOf("week")
.format("MM/DD/YYYY")}`;
const renderContext = {
week,
completed: completedTasks,
pending: pendingTasks,
};
const emailBody = LiquidParser.parse(summaryTemplate, renderContext);
const notificationsContext: NotifyContext = {
to: entity.c_team_members__list?.map((u) => u.id) || [],
cc: [entity.c_supervisor],
subject: `Weekly Summary - Week of ${week}`,
email_body: emailBody,
email_layout: "SimpleLayoutNotification",
replyTo: "team-lead@company.com",
};
return notificationsContext;
}
This example demonstrates:
- Custom action that triggers the notification
- Liquid template with loops and conditionals
- Multiple recipient types (to and cc)
- Dynamic data preparation
- Helper functions for reusability