Send Templated HTML Email Notifications
The Notify Via Email step composes its email as a rich HTML report from a Jinja template rendered over one or more of your workflow’s tables. You add each table to the step as a named frame, and the template renders those frames into a single email. The step opens directly into the Jinja editor, and that is its only authoring surface — there is no plain-text body box and no mode to switch to. This guide covers the editor and its formatting toolbar, the frames you add, the data available inside a template, the render_table helper, and the constraints that come from sending through email.
The Jinja Editor
Section titled “The Jinja Editor”Open a Notify Via Email step and it lands in the Jinja editor. Add one or more frames (see below), write your template in the body, and watch the Live Preview on the right update as you type. Send Test to Me delivers the rendered email to your own address so you can check it before you schedule the workflow. New steps are Jinja-only — there is no plain-text body to fill in and no rich-versus-plain toggle.
The Formatting Toolbar
Section titled “The Formatting Toolbar”The Jinja editor carries a toolbar for shaping the template without hand-typing boilerplate:
- Prettify reindents and normalizes the whole template — consistent indentation, tag spacing, and Jinja block layout — so a template you’ve been editing reads cleanly. It reformats only; it never changes what the template renders.
- Insert drops a ready-made snippet at the cursor — a
render_table(...)call pre-filled for a frame, the workspace name{{ cloud }}, any of your project or workflow variables, a frame’s row count, a{% for %}loop over a frame’s rows, and if/else and comment scaffolds. - Format applies a display filter to the value at the cursor — currency, thousands, percent, rounding, and date formats, plus text helpers (upper, lower, title case, trim) and list helpers (length, join, sum, and so on).
- Style wraps the selected text in HTML formatting — bold, italic, underline, three heading sizes, paragraphs, bulleted and numbered lists, block quotes, links, and line breaks. Every option emits only markup the email sanitizer keeps (see The Allowlisted-HTML Constraint), so a heading comes through as a larger bold line rather than an
h1, and underline rides an allowed tag.
Every button just inserts text you could type yourself; use them as a starting point and edit freely.
Add Frames
Section titled “Add Frames”A step reports on frames — named single-table selections you add to the step. Add one frame or several; each renders into the same email. For every frame you give:
- a Frame Name — the label you reference it by in the template (
Sales,Overdue, and so on); - one table from the workflow’s context;
- optional column selection, a where filter, and aggregation on that table.
A frame is always a single table. Joining or combining tables into one frame isn’t supported — add a second frame instead and reference each by its name.
Each frame is exposed inside the template through frames['<Frame Name>'], which carries that frame’s rows and columns (see below).
What’s Available Inside the Template
Section titled “What’s Available Inside the Template”The template exposes a narrow, explicit set of variables. Referencing an undefined variable fails the render, so only use what’s listed here:
| Variable | Contents |
|---|---|
cloud |
The workspace name. |
frames |
A dictionary of your frames, keyed by Frame Name. frames['Sales'].rows is that frame’s list of row objects (one per row); frames['Sales'].columns is its column list, in order. |
rows |
The first frame’s rows — a convenience binding so a single-frame template can use rows directly. |
columns |
The first frame’s columns — the same convenience binding as rows. |
variables |
Your project’s variables, and — in a workflow — the workflow’s variables, keyed by name: variables['Fiscal Year']. The built-in variables['project'], variables['model'], and variables['date'] (the current UTC time) are here too. Use the subscript form, since a variable name need not be a valid identifier. |
Values keep their native Python types — a Decimal stays a Decimal, a date stays a date, and a null column comes through as None — so you can format and compare them the same way you would in Python, rather than working from stringified text.
Render a Table with render_table
Section titled “Render a Table with render_table”Rather than hand-writing <table> markup, call the render_table macro with a frame’s rows and columns. Reference each frame by its name:
{{ render_table(frames['Sales'].rows, frames['Sales'].columns) }}This emits a correctly structured HTML table for that frame in one call. Call it once per frame you want to show.
A single-frame template can use the top-level rows and columns, which are bound to the first frame:
{{ render_table(rows, columns) }}A Complete Example
Section titled “A Complete Example”This template has two frames, Overdue and Paid. It renders the workspace name, a short summary line, and a table for each frame:
<p><strong>Invoice Status — {{ cloud }}</strong></p>
<p>{{ frames['Overdue'].rows|length }} invoice(s) are past due as of today.</p>
{{ render_table(frames['Overdue'].rows, frames['Overdue'].columns) }}
<p><strong>Recently Paid</strong></p>
{{ render_table(frames['Paid'].rows, frames['Paid'].columns) }}
{% if frames['Overdue'].rows|length == 0 %}<p>No overdue invoices — nothing further to review.</p>{% endif %}Point each frame at the table you want reported, and the rendered email shows a bold title line, a one-line summary computed from the row count, and a table per frame. With a single frame, you could write the same template against the bare rows and columns instead.
The Allowlisted-HTML Constraint
Section titled “The Allowlisted-HTML Constraint”Every rendered email passes through the same sanitizer as the rest of PlaidCloud’s transactional mail before it’s sent. The sanitizer strips anything outside a small allowlist:
- Allowed tags: the table tags
render_tableemits (table,tbody,tr,th,td), plus common text tags —p,br,strong/b,em/i,ul/ol/li,blockquote,code, anda. Headings (h1–h6),div, andspanare not on the list — for a title, use apwith astronginside it. - Allowed attributes:
classandstyleonly (plushrefona). - No
<thead>.render_tableemitstable/tbody/tr/th/td— a header row is atrofthcells insidetbody, not a separate<thead>section. - No
<style>blocks, and no external resources — aurl(...)reference or a tracking beacon is stripped along with any other tag or attribute not on the list.
In practice: build your report structure with render_table and plain text tags, and keep any custom styling to simple inline style attributes. Anything more elaborate — a <style> block, an external style sheet, custom fonts, tracking pixels — does not survive sanitization and does not appear in the delivered email.
Existing and Legacy Steps
Section titled “Existing and Legacy Steps”Steps created before Jinja authoring open straight into the editor with their old body already converted. A standard plain-text body — built from {project}, {model}, {cloud}, and {date} — is rewritten to the Jinja form ({{ project }}, {{ cloud }}, and so on) so you pick up editing from there.
A body the converter can’t rewrite safely — a non-standard token, or unbalanced braces — stays in a plain-text fallback box instead. That step keeps sending exactly as it did before, using the legacy {token} substitution; only its authoring stays plain text. New steps never use this path.
The editor lints variable names as you type and flags any that won’t resolve. A bare {{ project }}, {{ model }}, or {{ date }} carried over from a legacy body is flagged, because those names resolve only through variables now — use variables['project'], variables['model'], or variables['date'] instead, replace the token with a column from one of your frames, or remove it before you save.
Security and Egress
Section titled “Security and Egress”Frame reads go through the same row-level security as any other read in the project — a frame only ever returns the rows the running user or automation principal is allowed to see. Sending limits (recipients, size, and rate) are unchanged from the plain-text path; template mode does not relax or add to them.
Next Steps
Section titled “Next Steps”- Notify Via Email reference — field-level configuration
- Export Templated — the similarly shaped but separate engine for rendering documents from tables
- Manage Workflow Variables — the
{var}substitution that legacy plain-text bodies still use