# How to customize email notifications

Email notifications 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[​](#overview "Direct link to 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[​](#prerequisites "Direct link to 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[​](#method-1-sending-a-simple-email-notification "Direct link to 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[​](#how-it-works "Direct link to 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[​](#method-2-sending-html-formatted-emails "Direct link to 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[​](#advanced-notifycontext-properties "Direct link to 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[​](#common-properties-explained "Direct link to 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[​](#example-using-advanced-properties "Direct link to 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[​](#method-3-using-liquidparser-for-dynamic-templates "Direct link to 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[​](#step-1-create-the-template "Direct link to 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[​](#step-2-create-the-rendering-function "Direct link to 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[​](#step-3-use-in-your-notification-function "Direct link to 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[​](#liquid-template-syntax-overview "Direct link to Liquid template syntax overview")

The LiquidParser supports standard Liquid syntax. For complete documentation on Liquid syntax, tags, and filters, see the [official Liquid documentation](https://shopify.github.io/liquid/basics/introduction/).

**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](https://shopify.github.io/liquid/filters/abs/) for a complete list.

## Use cases[​](#use-cases "Direct link to 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[​](#best-practices "Direct link to 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[​](#email-layout-options "Direct link to Email layout options")

### SimpleLayoutNotification[​](#simplelayoutnotification "Direct link to 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[​](#recordnotification "Direct link to 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[​](#troubleshooting "Direct link to Troubleshooting")

### Emails not being sent[​](#emails-not-being-sent "Direct link to 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[​](#html-not-rendering-correctly "Direct link to 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[​](#variables-not-showing-in-template "Direct link to 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[​](#recipients-not-receiving-emails "Direct link to 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[​](#example-complete-notification-action "Direct link to 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

## Related[​](#related "Direct link to Related")

* [Actions](/developer-guide/building-blocks/actions.md) - how to define actions and action logic where `__sendNotifications` lives
* [Server-side API](/developer-guide/reference/server-side-globals.md) - full reference for `LiquidParser`, `ComindServer`, and other server globals
