Machines process JSON; people process visuals. To build automations that truly provide value to a business, you must be able to present your data in a clean, professional, and actionable format.
1The Templating Engine
Data transformation is more than just swapping brackets for tags. It's about building a Templating Engine within your workflow. By initializing an HTML skeleton and looping through your JSON items, you create a dynamic document that expands or contracts based on your data.
Whether you have 5 leads or 500, your report always maintains structural integrity. In n8n, use a Code node with a simple JavaScript reduce or map function to iterate through the $input.all() array, generating a long string of HTML table rows.
// Converting JSON array to HTML table rows
const items = $input.all();
let htmlRows = items.map(item => {
const data = item.json;
return `
<tr>
<td>${data.name}</td>
<td>${data.email}</td>
<td>${data.score}</td>
</tr>
`;
}).join('');
return [{ json: { htmlRows } }];2Email Client Resilience
The biggest challenge in automated reporting is Email Client Compatibility. Unlike modern web browsers, email clients like Outlook or Gmail mobile have notoriously limited CSS support. External stylesheets and <style> blocks in the head are frequently stripped out.
To ensure your reports don't break, you must use Inline Styles directly on the HTML tags (e.g., <td style='border: 1px solid #ddd;'>). This 'defensive coding' strategy guarantees that your automation delivers value to stakeholders, looking just as good in their inbox as it does in your test environment.
// Defensive coding with inline styles
// Bad: Relies on <style> block (will break in Outlook)
const bad = `<td class="data-cell">${value}</td>`;
// Good: Inline styles guarantee rendering
const good = `
<td style="
padding: 12px;
border-bottom: 1px solid #eeeeee;
font-family: sans-serif;
color: #333333;
">
${value}
</td>
`;3Conditional Highlighting
Raw data is hard to parse at a glance. To make your automated reports actionable, introduce Conditional Logic during the HTML generation phase. If a lead score is above 90, render the text green; if below 40, render it red.
This technique shifts the cognitive load from the reader to the machine. You're no longer just sending data; you're sending insights. By embedding simple ternary operators in your template literal, you can dynamically adjust inline styles based on the specific values of the current JSON item.
// Applying conditional styles
const score = data.score;
// Ternary logic for color
const color = score >= 90 ? '#28a745' :
score < 40 ? '#d73a49' :
'#333333';
const cell = `
<td style="color: ${color}; font-weight: bold;">
${score}
</td>
`;4Step-by-Step Breakdown
Raw JSON is perfect for machines, but human stakeholders need clarity, not curly braces. In this lesson, we'll build a data transformation pipeline that turns raw JSON into clean, actionable HTML tables anyone can read at a glance.
An HTML table has three key building blocks: the table wrapper itself, tr for each row, and th or td for the header and data cells inside that row. Get this basic structure right and everything else — styling, loops, dynamic content — builds on top of it.
We generate that table row by row using a simple loop — for every item in the JSON array, we append a new tr with td cells filled from that item's fields, building up the full HTML string one row at a time until every record is represented.
Checkpoint: Which HTML tag is used to define an individual cell in a table row?
- →<tr> (Table Row)
- →<td> (Table Data)
Email clients are notoriously strict about CSS — most of them simply strip out a separate <style> block entirely and ignore it. That's why every style you want to survive in an email has to be written inline, directly on the element itself.
You can also dynamically pick the color a cell renders in based on its data — a ternary that checks whether score is above 80 and swaps the inline color between green and red is all it takes to build an automatic, color-coded report.
Checkpoint: Why do we use inline styles instead of a separate CSS block in automated emails?
- →It makes the code shorter
- →Many email clients ignore <style> blocks and only read inline styles
The final HTML string, built dynamically from your looped rows and inline styles, is now ready to drop straight into an email body or a webpage — no manual formatting, no copy-pasting from a spreadsheet.
Pro-tip: keep your HTML-generation logic in its own dedicated Code node, separate from the node that fetches your raw JSON data. This decoupled design makes it much easier to swap out the presentation layer later without touching your data pipeline.
Checkpoint: True or False: You can convert a JSON array of 1,000 items into a single HTML table using a single loop.
- →True
- →False
Status: presentable. You've turned raw, machine-only JSON into a polished HTML table that's ready for a real human inbox — the same transformation pattern scales whether you're formatting 3 rows or 3,000.
Next, we'll go deeper into the loops and arrays that power this kind of data transformation — the exact mechanics behind turning a JSON array into repeated, structured output like the tables you just built.
Render Real JSON as HTML. Finish turning a list of items into an HTML unordered list.
Level Up 🚀
Advanced cheat sheets, SEO tricks, and interview prep for this topic.
Browser Support
Fully supported.
Fully supported.
Fully supported.
Fully supported.
Accessibility (A11y)
1Generated Report Tables Still Need Real Table Semantics
When a Code node loops over JSON to build an HTML email report, it's tempting to hand-roll `<div>` grids with inline styles for visual layout. Screen readers announce tabular data far more usefully when it's built from actual `<table>`, `<th scope="col">`, and `<td>` elements, so the generated markup should keep semantic table structure even while using inline styles for the visual formatting email clients require.
<table role="table"><tr><th scope="col">Lead</th><th scope="col">Score</th></tr></table>SEO Implications
- 1
Programmatically Generated HTML Reports Are Typically Not Indexed, but the Same Template Pattern Powers Indexed Pages
Automated JSON-to-HTML reports sent by email or posted to a dashboard usually live outside crawlable URLs and carry no direct SEO weight. However, the exact same loop-and-template technique is commonly reused to generate public-facing HTML (e.g. a JSON feed of products rendered into a listing page), where malformed template literals producing invalid nested tags can break how a crawler parses the page's content structure.
Best Practices
Escape Dynamic Values Before Interpolating Them Into HTML
Never drop a raw JSON string field directly into a template literal without escaping angle brackets and ampersands. A lead's company name containing '<' or '&' (common in names like 'Smith & Sons') will break the generated markup or, worse, allow HTML injection if the data source isn't trusted.
Build the Full HTML String in One Pass With .map().join('')
Loop through the JSON array once, mapping each item to its HTML row string, then join them into a single output rather than repeatedly concatenating strings inside a for-loop. This is both faster for large datasets and easier to read than accumulating a mutable string across iterations.
Frequent Bugs
A report renders perfectly in the workflow's test preview (a modern browser) but arrives broken or unstyled in Outlook, because the template relied on a `<style>` block or CSS class instead of inline styles.
Always use inline `style` attributes directly on table cells and rows for any HTML destined for email, and test the actual rendered output in a real client like Outlook or Gmail — never trust a browser preview as a proxy for email rendering.
Real-World Examples
Generating a Daily Lead Summary Email from a JSON Array
A workflow pulls the day's new leads as a JSON array, then a Code node maps each lead to a table row, color-coding the score cell green or red based on a threshold, and joins the rows into a single HTML table wrapped for the email node — with a fallback string when the array is empty so the report doesn't send a broken, headerless table.
const rows = leads.length
? leads.map(l => `<tr><td style="padding:8px;">${l.name}</td><td style="color:${l.score>=90?'#28a745':'#333'};">${l.score}</td></tr>`).join('')
: '<tr><td colspan="2">No new leads today.</td></tr>';