n8n HTML Node: Generate, Inject, and Manipulate HTML in Your Workflows [Free Workflow JSON]
The n8n HTML node lets you build HTML strings from workflow data — generating email bodies, PDF templates, report snippets, or dynamic web fragments without leaving your automation.
This is distinct from the HTML Extract node (which reads HTML from pages). The HTML node creates HTML output.
What the HTML Node Does
The HTML node has one operation: Generate HTML. It takes a Handlebars/Mustache-style template that you write inline, merges it with incoming item data, and outputs an HTML string per item.
Key properties:
-
Template — your HTML with
{{ expression }}placeholders (uses n8n's expression engine, not Handlebars — same{{ $json.fieldName }}syntax as the rest of n8n) -
Output field name — the field on the output item that holds the resulting HTML string (default:
html)
Basic Example: Generating an HTML Email Body
Suppose you have a Stripe charge event and want to send a formatted HTML receipt:
{
"customer_email": "alice@example.com",
"amount": 4900,
"currency": "usd",
"description": "Pro Plan — Monthly"
}
HTML node template:
<!DOCTYPE html>
<html>
<body>
<h2>Thanks for your payment!</h2>
<p>Hi {{ $json.customer_email }},</p>
<p>We received your payment of <strong>${{ ($json.amount / 100).toFixed(2) }} {{ $json.currency.toUpperCase() }}</strong>
for <em>{{ $json.description }}</em>.</p>
<p>Questions? Reply to this email.</p>
</body>
</html>
Output item:
{
"customer_email": "alice@example.com",
"amount": 4900,
"currency": "usd",
"description": "Pro Plan — Monthly",
"html": "<!DOCTYPE html>..."
}
Wire this into a Send Email node (Gmail, Outlook, or SMTP) and set HTML as the body type — that's a complete HTML email workflow.
Pattern 1: Weekly Status Report Email
Workflow: Schedule Trigger → Postgres (pull last week's KPIs) → HTML node (format as table) → Send Email
<h2>Weekly Report — {{ $now.toFormat('yyyy-MM-dd') }}</h2>
<table border="1" cellpadding="6" cellspacing="0">
<tr><th>Metric</th><th>Value</th></tr>
<tr><td>New signups</td><td>{{ $json.new_signups }}</td></tr>
<tr><td>MRR</td><td>${{ $json.mrr_cents / 100 }}</td></tr>
<tr><td>Churn</td><td>{{ $json.churn_count }}</td></tr>
<tr><td>Active users</td><td>{{ $json.active_users }}</td></tr>
</table>
Free workflow JSON: Download below
Pattern 2: Dynamic Invoice / PDF Template
Workflow: Webhook (order payload) → HTML node (invoice template) → Convert to File (html→pdf via headless, or just send the HTML) → Email attachment
<!DOCTYPE html>
<html>
<head>
<style>
body { font-family: Arial, sans-serif; margin: 40px; }
table { width: 100%; border-collapse: collapse; }
th, td { border: 1px solid #ddd; padding: 8px; }
.total { font-weight: bold; font-size: 1.2em; }
</style>
</head>
<body>
<h1>Invoice #{{ $json.invoice_number }}</h1>
<p><strong>Bill to:</strong> {{ $json.customer_name }}<br>
{{ $json.customer_email }}</p>
<p><strong>Date:</strong> {{ $json.date }}</p>
<table>
<tr><th>Description</th><th>Qty</th><th>Unit Price</th><th>Total</th></tr>
{{ $json.line_items.map(item => `<tr><td>${item.name}</td><td>${item.qty}</td><td>$${item.unit_price}</td><td>$${item.qty * item.unit_price}</td></tr>`).join('') }}
</table>
<p class="total">Total: ${{ $json.total }}</p>
</body>
</html>
Pattern 3: Conditional Alert Email with Color Coding
Workflow: Cron → pull metrics → IF node (threshold check) → HTML node (format alert) → Slack or Email
<div style="background: {{ $json.error_rate > 0.05 ? '#fee2e2' : '#dcfce7' }}; padding: 16px; border-radius: 8px;">
<h3 style="color: {{ $json.error_rate > 0.05 ? '#dc2626' : '#16a34a' }};">
{{ $json.error_rate > 0.05 ? '🔴 High Error Rate Alert' : '✅ System Healthy' }}
</h3>
<p>Error rate: <strong>{{ ($json.error_rate * 100).toFixed(2) }}%</strong></p>
<p>Checked at: {{ $now.toISO() }}</p>
</div>
Gotchas
1. It's n8n expressions, not Handlebars
The template uses n8n's {{ }} expression syntax — full JavaScript expressions work ($json.items.length, .map(), .toFixed()). Don't try Handlebars {{#each}} — it won't work.
2. Multi-item loops inside the template
The HTML node processes one item at a time. To generate a single HTML document with a table of all items, aggregate first with the Item Lists node (Aggregate operation → array field), then use .map() in the template:
{{ $json.rows.map(r => `<tr><td>${r.name}</td><td>${r.value}</td></tr>`).join('') }}
3. XSS in your own workflows
If user-supplied data goes directly into the template, it will be interpolated as-is. This is fine for internal reports — but if the HTML is ever shown to end users in a browser context, sanitize inputs first (e.g., via a Code node with a simple replace for <, >, &).
4. HTML node vs Code node
You can build HTML in a Code node with template literals too. The HTML node is cleaner for multiline templates since it has a dedicated editor. For complex logic with conditionals and loops, Code node often wins.
Free Workflow JSON
{
"name": "HTML Node — Weekly KPI Email",
"nodes": [
{
"parameters": { "rule": { "interval": [{ "field": "weeks" }] } },
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [240, 300]
},
{
"parameters": {
"values": {
"number": [
{ "name": "new_signups", "value": 42 },
{ "name": "mrr_cents", "value": 299700 },
{ "name": "churn_count", "value": 3 },
{ "name": "active_users", "value": 187 }
]
}
},
"name": "Mock KPI Data",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [460, 300]
},
{
"parameters": {
"html": "<h2>Weekly Report — {{ $now.toFormat('yyyy-MM-dd') }}</h2><table border=\"1\" cellpadding=\"6\" cellspacing=\"0\"><tr><th>Metric</th><th>Value</th></tr><tr><td>New signups</td><td>{{ $json.new_signups }}</td></tr><tr><td>MRR</td><td>${{ $json.mrr_cents / 100 }}</td></tr><tr><td>Churn</td><td>{{ $json.churn_count }}</td></tr><tr><td>Active users</td><td>{{ $json.active_users }}</td></tr></table>",
"options": {}
},
"name": "HTML",
"type": "n8n-nodes-base.html",
"typeVersion": 1.2,
"position": [680, 300]
}
],
"connections": {
"Schedule Trigger": { "main": [[{ "node": "Mock KPI Data", "type": "main", "index": 0 }]] },
"Mock KPI Data": { "main": [[{ "node": "HTML", "type": "main", "index": 0 }]] }
}
}
Drop this into n8n (Settings → Import Workflow), wire a Send Email node to the HTML output, and you have a weekly KPI email in under 10 minutes.
When to use the HTML node vs alternatives
| Need | Best tool |
|---|---|
| Simple HTML email body | HTML node |
| Complex logic with conditionals | Code node (template literal) |
| Scrape/parse HTML from a page | HTML Extract node |
| Convert HTML string to PDF | HTML node → Convert to File (or external API) |
| Generate Markdown | Code node |
Got a template you've built with the HTML node? Drop it in the comments — what are you generating?
Top comments (0)