Пошаговые инструкции Для начинающих 13 мин

Как создать HTML-таблицу в n8n для письма или отчёта

Соберите заказы в HTML-таблицу: 10 шагов, готовый workflow и образец отчёта. Сохраните кириллицу, нули и номера, проверьте текст каждой ячейки.

n8n отчеты HTML

Когда данные уже собраны в n8n, их удобно показать человеку в виде таблицы: список заказов, остатки, заявки или итоги дня. Сделаем такую таблицу штатным узлом HTML. На выходе получим HTML-фрагмент для письма и полную страницу отчёта, которую можно открыть в браузере.

Результат инструкции

Соберём три вымышленных заказа с четырьмя колонками: номер, товар, количество и цена в рублях. Сохраним ведущие нули в номерах, кириллицу и нулевые значения. После создания таблицы прочитаем её обратно HTML-парсером и сравним каждую ячейку с источником.

Схема ничего не отправляет: нет SMTP, получателей и записи в рабочие системы. Это законченный этап подготовки отчёта. Проверка verified: true означает совпадение ячеек с исходными данными, а не отправку письма, проверку заказов по базе или универсальную защиту любого HTML.

Что понадобится

  • Отдельный новый workflow в n8n. Проверено на локальном n8n 2.42.4, HTML 1.2 и Code 2.
  • Для первого запуска достаточно учебных данных из статьи. Аккаунты внешних сервисов и платные API не нужны.
  • Браузер для просмотра готового образца. Импортируйте workflow без публикации: он запускается вручную.

Как установить n8n локально через Docker

Шаг 1. Посмотрите, какой отчёт получится

Открыть готовую HTML-таблицу заказов

В образце три строки: «Чай & кофе», «Блокнот», «Лампа "Старт"». У блокнота количество и цена равны нулю. Номера выглядят как 001, 002 и 003, не как числа 1, 2 и 3. Это файл, полученный успешным запуском опубликованной схемы, а не отдельный макет.

Внешний вид в браузере задаёт небольшая статическая CSS-таблица стилей. Внутри письма оформление может отличаться: почтовые клиенты поддерживают CSS по-разному. Отправку и отображение в почтовых клиентах в этой инструкции не проверяли.

Шаг 2. Импортируйте workflow

Скачать n8n-orders-html-table.json

Создайте новый workflow, выберите Import from File и загрузите JSON. Сохраните схему под отдельным названием. В ней шесть узлов:

text
Manual Trigger → Sample Orders → Prepare Rows → Build Table → Read Cells → Verify Table

Sample Orders, Prepare Rows и Verify Table — узлы Code в режиме Run Once for All Items, язык JavaScript. Build Table и Read Cells — два узла HTML с разными операциями. Названия важны: последний Code обращается к Sample Orders и Build Table по имени. Не включайте Continue On Fail или Always Output Data.

Шаг 3. Задайте учебные строки

Откройте Sample Orders. При ручной сборке вставьте следующий код:

javascript
return [
  {
    "order_id": "001",
    "product": "Чай & кофе",
    "quantity": 2,
    "price_rub": 1500
  },
  {
    "order_id": "002",
    "product": "Блокнот",
    "quantity": 0,
    "price_rub": 0
  },
  {
    "order_id": "003",
    "product": "Лампа \"Старт\"",
    "quantity": 1,
    "price_rub": 499
  }
].map(json => ({ json }));

Каждый объект становится отдельным item. Именно отдельные items узел таблицы превращает в строки. Один item с полем orders, внутри которого лежит массив, здесь не подойдёт. Номера оставляем строками: преобразование 001 в число уберёт ведущие нули ещё до создания HTML.

Цены в примере — целые рубли, не копейки и не результат финансового расчёта. Для реального отчёта заранее согласуйте единицы и округление. Не подменяйте отсутствующую цену нулём: «неизвестно» и «бесплатно» означают разное.

Шаг 4. Подготовьте безопасный текст ячеек

В Prepare Rows вставьте код ниже. Он проверяет четыре поля, уникальные номера, текст товара и неотрицательные целые числа. Затем формирует четыре фиксированных русских заголовка в нужном порядке:

javascript
const items = $input.all();
if (items.length < 1 || items.length > 20) throw new Error('rows_limit');
const ids = new Set();
const keys = ['order_id', 'product', 'quantity', 'price_rub'];
const escapeText = value => String(value).replace(/[&<>"']/g, char => ({
  '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;', "'": '&#39;'
})[char]);
return items.map((item, index) => {
  const row = item.json;
  if (Object.keys(row).length !== keys.length || keys.some(key => !Object.hasOwn(row, key))) {
    throw new Error('fields_required');
  }
  if (typeof row.order_id !== 'string' || !/^\d{3}$/.test(row.order_id) || ids.has(row.order_id)) {
    throw new Error('order_id_invalid_or_duplicate');
  }
  ids.add(row.order_id);
  if (typeof row.product !== 'string' || !row.product.trim() || row.product !== row.product.trim() || row.product.length > 120 || /[\u0000-\u001f\u007f]/.test(row.product)) {
    throw new Error('product_text_required');
  }
  for (const key of ['quantity', 'price_rub']) {
    if (!Number.isSafeInteger(row[key]) || row[key] < 0) throw new Error('nonnegative_integer_required');
  }
  return { json: {
    'Номер': escapeText(row.order_id),
    'Товар': escapeText(row.product),
    'Количество': row.quantity,
    'Цена, руб.': row.price_rub
  }, pairedItem: { item: index } };
});

Важная деталь: Convert to HTML Table в проверенной версии вставляет содержимое ячеек как есть. Если передать <b>Чай</b>, получится жирный текст, а не буквальная строка. Поэтому escapeText заменяет специальные символы HTML только в текстовых значениях перед построением таблицы. Амперсанд тоже экранируется, иначе готовые HTML-сущности могут изменить исходный текст.

Названия колонок, подпись таблицы и атрибуты заданы нами, не приходят от пользователя. Нельзя переносить эту функцию в URL, CSS или JavaScript и считать результат безопасным для любого контекста. Это не очистка произвольного HTML: по этому сценарию в ячейках разрешён только текст.

Лимит — от одной до двадцати строк, название товара — до 120 символов, без управляющих символов и пробелов по краям. В n8n 2.42.4 обратное извлечение текста убирало крайние пробелы даже с выключенными Trim Values и Clean Up Text. Здесь неоднозначные входные данные отклоняются, а не меняются молча. При пустом выходе Sample Orders дальнейшие узлы вообще не выполняются: это не готовый пустой отчёт.

Шаг 5. Настройте создание таблицы

В Build Table выберите HTML → Operation: Convert to HTML Table. Параметры:

Настройка Значение Зачем
Capitalize Headers Выключено Оставить наши заголовки без изменения
Custom Styling Включено Не добавлять стандартные inline-стили узла
Caption Учебные заказы Постоянная подпись над таблицей
Table Attributes aria-label="Учебные заказы" Постоянное доступное имя таблицы

Header Attributes, Row Attributes и Cell Attributes оставьте пустыми. Не вставляйте в эти поля входящие строки. В этом примере Custom Styling не рисует таблицу сам: оформление полной страницы задаётся позже статическим CSS. При выключенном режиме узел добавляет своё стандартное оформление.

У Build Table на выходе один item с полем table. Внутри — готовый HTML-фрагмент с thead, tbody и тремя строками. Это не binary-файл и не полная страница с head. Входные JSON-поля не сохраняются рядом с результатом автоматически.

Шаг 6. Прочитайте ячейки обратно

В Read Cells выберите HTML → Extract HTML Content, Source Data: JSON, JSON Property: table. В Extraction Values добавьте четыре значения. Return Array включите у каждого:

Key CSS Selector Return Value
headers table thead th Text
cells table tbody td Text
rows table tbody tr HTML
unsafe table script, table iframe, table img, table input, table a, table svg, table object HTML

В Options выключите Trim Values и Clean Up Text. Селектор cells читает все ячейки в порядке строк. Для трёх заказов с четырьмя полями ожидаются двенадцать текстовых значений. При Text HTML-сущности декодируются: &amp; снова станет обычным амперсандом. Именно это позволяет сравнить видимый текст с источником.

unsafe должен быть пустым массивом. Мы не ожидаем ссылок, изображений, checkbox или скриптов внутри таблицы. Этот список — дополнительная проверка конкретной схемы с фиксированной разметкой, не полный HTML-санитайзер. Нельзя принять чужой HTML только потому, что этот селектор ничего не нашёл.

Шаг 7. Добавьте сверку и страницу отчёта

В Verify Table оставьте JavaScript и Run Once for All Items:

javascript
const parsed = $input.first().json;
const source = $('Sample Orders').all().map(item => item.json);
const headers = ['Номер', 'Товар', 'Количество', 'Цена, руб.'];
const same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
const expected = source.flatMap(row => [row.order_id, row.product, String(row.quantity), String(row.price_rub)]);
if (!same(parsed.headers, headers)) throw new Error('headers_mismatch');
if (!Array.isArray(parsed.cells) || !same(parsed.cells, expected)) throw new Error('cells_mismatch');
if (!Array.isArray(parsed.rows) || parsed.rows.length !== source.length) throw new Error('rows_mismatch');
if (!Array.isArray(parsed.unsafe) || parsed.unsafe.length) throw new Error('unexpected_elements');
const table = $('Build Table').first().json.table;
if (typeof table !== 'string' || !table.startsWith('<table ')) throw new Error('table_required');
const html = '<!doctype html><html lang="ru"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><title>Учебные заказы</title>' +
  '<style>body{font:16px Arial,sans-serif;margin:0;color:#202324;background:#fff}main{max-width:900px;margin:auto;padding:32px 20px}h1{font-size:28px;letter-spacing:0}.table-wrap{overflow-x:auto}table{width:100%;border-collapse:collapse}caption{text-align:left;padding:12px 0;font-weight:bold}th,td{padding:12px;border:1px solid #d9dfe1;text-align:left}thead{background:#edf4f1}tbody tr:nth-child(even){background:#f6f7f8}</style></head><body><main><h1>Учебные заказы</h1><p>Вымышленные данные для проверки workflow n8n.</p><div class="table-wrap">' +
  table + '</div></main></body></html>';
return [{ json: { rows: source.length, verified: true, table_html: table, html, action_performed: false } }];

Код сверяет заголовки, порядок всех ячеек и число строк. Он сравнивает не уже экранированные строки из Prepare Rows, а исходные значения Sample Orders. Поэтому двойное экранирование, потерянная ячейка или изменение порядка не пройдут незаметно. Числовые значения для сравнения переводятся в строку, ноль при этом остаётся нулём.

Результат содержит table_html — фрагмент таблицы и html — полную страницу с UTF-8 и оформлением. action_performed: false означает, что внешнее действие не выполнялось. Если схема падает, не заменяйте её результат вручную на verified: true. Исправьте вход или настройку, затем запустите цепочку заново.

Шаг 8. Выполните всю цепочку

Нажмите Execute workflow от Manual Trigger. Проверьте зелёное завершение всех шести узлов, затем откройте JSON последнего. Ключевые поля должны выглядеть так:

json
{
  "rows": 3,
  "verified": true,
  "action_performed": false
}

Кроме этих трёх полей будут длинные table_html и html. В Read Cells сравните первые четыре значения: 001, Чай & кофе, 2, 1500. Следующие четыре — 002, Блокнот, 0, 0. Последние — 003, Лампа "Старт", 1, 499. Не проверяйте только число строк: неправильные значения внутри тоже должны быть замечены.

Шаг 9. Проверьте спецсимволы и ошибку входа

В Sample Orders временно замените первый product на строку <b>Чай</b> и выполните весь workflow ещё раз. Ожидается успех: в Read Cells будет буквальная строка с угловыми скобками. В table_html она записана как &lt;b&gt;Чай&lt;/b&gt;. Кавычки внутри значения не требуют HTML-атрибутов: это обычный текст ячейки.

Затем сделайте quantity равным -1 и повторите запуск. Prepare Rows должен остановить обработку с nonnegative_integer_required. Отрицательное количество в другой предметной области может быть допустимо, но наша учебная схема его не принимает. Верните исходные три заказа и снова убедитесь в verified: true.

Не отключайте экранирование ради оформления названия товара. Для подчёркивания, ссылок или других элементов нужна отдельная согласованная разметка. В тесте с отключённым escapeText и строкой <b>Чай</b> сверка остановилась с cells_mismatch: видимый текст уже не совпал с источником.

Шаг 10. Используйте готовый HTML

Для отдельного отчёта скопируйте только значение поля html, без внешних JSON-кавычек, в новый локальный файл orders-report.html в кодировке UTF-8. Откройте его браузером. Сверьте три строки, русские буквы, номера и нули с исходником. Готовый образец по ссылке в первом шаге позволяет посмотреть результат без сохранения своего файла.

Для будущей отправки письма передайте table_html в HTML-тело вашего почтового узла. Не путайте его с обычным текстовым телом: иначе получатель увидит теги. Но сначала отдельно настройте адресата, разрешённую отправку и inline-стили, затем проверьте тестовое письмо в нужных клиентах. Этот workflow не содержит узла отправки и не подтверждает доставку.

Если нужен именно скачиваемый файл внутри n8n, следующий отдельный этап — Convert to File для поля html с MIME text/html и кодировкой UTF-8. В текущей схеме binary не создаётся: не ищите файл во вкладке Binary последнего узла.

Если результат отличается

Симптом Причина Что сделать
В таблице одна строка с массивом Все записи внутри одного JSON-поля Передавать отдельный item на заказ
Появился undefined или лишняя колонка Разные наборы полей Вернуть Prepare Rows и четыре ключа
Теги стали оформлением Пропущено экранирование Вернуть escapeText до Build Table
cells_mismatch Текст, порядок или селектор изменились Сравнить Read Cells и Sample Orders
Последний узел не запустился Входной узел не выдал items Проверить Sample Orders; это не пустой отчёт

Как заменить учебные данные своими

Сначала подключите только небольшой разрешённый набор, без секретов и персональных данных. Приведите его к тем же четырём полям вместо Sample Orders, сохраните строки номеров и согласуйте единицы цены. Если переименовали источник, поправьте ссылку в Verify Table. Прочие настройки оставьте прежними до успешной проверки.

Не расширяйте лимит до десятков тысяч строк без оценки размера отчёта и памяти. HTML из этого workflow, как и исходные данные, может сохраняться в истории executions. Настройте доступ и хранение перед работой с закрытой информацией. Наличие HTML-таблицы само по себе не делает передачу данных разрешённой.

Как выгрузить таблицу XLSX из n8n

Что проверено редакцией

18 октября 2026 года схема проверена в отдельном Docker-экземпляре n8n 2.42.4. Пройдены 32 сценария: точный публичный workflow, одна и двадцать строк, обратный порядок, кириллица, Unicode, нули, кавычки, амперсанд, HTML-сущности и буквальный текст с тегами; неверные типы, номера, дубли, пропущенные и лишние поля, отрицательные, дробные и небезопасные числа, длинный текст и управляющие символы; пропуск экранирования, неверный селектор и выключенный Return Array. Пустой источник завершился без отчёта, не с verified. Крайние пробелы отклонены входной проверкой после наблюдения потери пробелов при нативном извлечении текста. Таблица строилась и читалась штатными HTML-узлами, образец страницы получен из фактического результата. Учебный контейнер остановлен, существующие workflows не менялись. Cloud, реальные письма, почтовые клиенты и пользовательские базы не тестировались. Обложка — AI-иллюстрация.

Частые вопросы

Нужно ли вручную писать HTML-таблицу?

Нет. Строки и ячейки строит HTML → Convert to HTML Table. Код перед ним проверяет значения и экранирует текст, а код после сверяет результат и добавляет оболочку отчёта.

Почему вместо одной таблицы у меня массив внутри ячейки?

На вход должны прийти отдельные items, по одному на строку. Один объект с вложенным массивом заказов не превращается в несколько строк автоматически.

Можно вставлять HTML в название товара?

В этой схеме нет: название остаётся буквальным текстом. Операция таблицы в проверенной версии сама значения не экранирует, поэтому подготовка через escapeText обязательна.

Как сохранить нули и номера 001?

Передавайте номер строкой, а количество и цену — числами. Не заменяйте нулевое значение через value || пустая строка и не преобразуйте строковый номер в Number.

Можно сразу отправить результат письмом?

table_html можно использовать как HTML-тело отдельно настроенного почтового узла. Текущая схема не отправляет письма. Получателя, разрешение, оформление и доставку нужно проверить отдельно.

Дальше по теме

Похожие материалы