Skip to main content

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:

  1. sending simple text notifications
  2. sending HTML-formatted emails
  3. using advanced recipient options (CC, BCC, reply-to)
  4. creating dynamic content with LiquidParser templates
  5. 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

  1. Function naming: the __sendNotifications function is a special reserved name that the system recognizes for sending notifications. It will be triggered automatically after your action executes.

  2. Recipients array: the notifyTo array collects all recipients. You can add:

    • user IDs from relationship fields (like assignees_list__list)
    • direct email addresses as strings
    • any combination of both
  3. 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 line
    • to: array of recipients (user IDs or email addresses)
  4. Return value: returning the NotifyContext object 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
tip

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 copy
  • replyTo: email address that will receive replies
  • subject: email subject line
  • email_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 address
  • override_from_email_name: custom sender display name
  • attachment_fields: comma-separated list of attachment field names to include
  • email_note: additional note displayed at the top of the email
  • override_unsubscribe_text: custom unsubscribe link text
  • altBox: 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

  1. Test with different email clients: email rendering varies across clients (Gmail, Outlook, Apple Mail). Test your HTML emails in multiple clients.

  2. Use inline CSS: avoid external stylesheets and <style> blocks. Use inline style attributes instead.

  3. Keep it simple: complex layouts may not render consistently. Stick to basic HTML and tables for structure.

  4. Provide plain text alternative: consider the email_layout option. SimpleLayoutNotification is more reliable across email clients.

  5. Validate recipients: always filter out null/undefined recipients with .filter(Boolean) before sending.

  6. Handle missing data: check if fields exist before including them in templates to avoid errors.

  7. Use meaningful subjects: make subject lines descriptive and actionable.

  8. 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 __sendNotifications is 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 renderContext object
  • 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