Урок курса
Merge и потеря связей между items
Продвинутый n8n: production-автоматизации и AI-агентыВ прошлом уроке мы разобрали, как pairedItem прокидывает ссылку от output-item к source item и как выражение $('Node').item использует эту цепочку, чтобы достать нужные данные. Теперь в игру входит Merge — нода с двумя входами и одним выходом. Именно здесь цепочка ломается чаще всего, и ломается предсказуемо.
Merge по позиции: механизм паросочетания и ошибка item linking
Режим Combine by Position работает по простому правилу: item с индексом 0 первого входа соединяется с item с индексом 0 второго, item с индексом 1 — с item с индексом 1, и так далее. Порядок в массиве — единственный критерий. Никакого поиска по содержимому полей, никакой проверки ключей.
В fixture обе ветки ответвляются от одних и тех же трёх запросов: R-101, R-102, R-103. Первая ветка сохраняет этот порядок и приходит на вход 1 Merge как [R-101, R-102, R-103]. Вторая ветка где-то по пути переставляет элементы и приходит на вход 2 как [R-103, R-101, R-102].
Merge по позиции формирует три пары:
| Индекс | Вход 1 | Вход 2 | Пара верная? |
|---|---|---|---|
| 0 | R-101 | R-103 | ✗ |
| 1 | R-102 | R-101 | ✗ |
| 2 | R-103 | R-102 | ✗ |
В этой конкретной циклической перестановке неверны все три пары. В общем случае любая перестановка делает позиционное соответствие ненадёжным и портит все затронутые позиции — поэтому на Combine by Position нельзя полагаться, если порядок веток не гарантирован явно.
Теперь важно понять, что происходит с item linking. Каждый из трёх output-items Merge несёт pairedItem, который указывает на входные items обоих входов. Output с индексом 0 ссылается на R-101 (вход 1) и на R-103 (вход 2) — два разных source item из разных исходных запросов. Когда downstream-нода пишет $('Source Requests').item, n8n должен подняться по цепочке pairedItem и найти один source item. Но ancestry output-item с индексом 0 одновременно ведёт к двум разным исходным записям.
Поведение n8n в этой ситуации определено: если item-link thread указывает более чем на один item предыдущей ноды, выражение .item бросает ошибку Multiple matching items for expression. Произвольного выбора одного из двух источников не происходит — n8n отказывается резолвить выражение.
Важно отличать этот симптом от другого: сами JSON-поля в output-item Merge объединены тихо и без ошибок — вы получите объект с данными R-101 из первого входа и данными R-103 из второго. Если downstream-нода читает только эти уже объединённые поля напрямую, ошибки не будет, но данные окажутся перемешаны незаметно. Ошибка Multiple matching items for expression возникает именно при попытке подняться по цепочке pairedItem через выражение .item, а не при чтении полей из объединённого JSON.
Диагностика через execution data: нахождение первого расхождения request_id
Зная, что паросочетание сломано, нужно точно установить, в каком output-item это происходит впервые. Execution data даёт всё необходимое — но здесь есть техническая сложность: оба входа Merge используют поле с одним и тем же именем request_id. По умолчанию при конфликте имён Merge перезаписывает значение, отдавая приоритет Input 2. В итоге в output-item остаётся только одно поле request_id, и сравнить оба исходных значения «в одной строке» напрямую невозможно.
Чтобы обойти это ограничение, до Merge добавьте в каждую ветку ноду Edit Fields и явно переименуйте поле: в первой ветке request_id → request_id_input1, во второй request_id → request_id_input2. После этого Merge объединит два различимых поля без конфликта, и их значения будут видны в output-item рядом. Добавив эти ноды, вы сможете воспроизвести таблицу ниже.
Алгоритм диагностики. Сначала откройте execution data нод обеих веток непосредственно перед Merge — там вы видите два независимых массива items с их request_id. Порядок в fixture:
- Вход 1:
["R-101", "R-102", "R-103"] - Вход 2:
["R-103", "R-101", "R-102"]
Это уже достаточное доказательство проблемы: если входные массивы упорядочены по-разному, позиционное соответствие заведомо неверно. Данные до Merge — первичный и наиболее надёжный источник.
Чтобы подтвердить расхождение в output, посмотрите на поля request_id_input1 и request_id_input2 в каждом output-item:
| Output-item | request_id_input1 | request_id_input2 | Совпадают? |
|---|---|---|---|
| 0 | R-101 | R-103 | ✗ |
| 1 | R-102 | R-101 | ✗ |
| 2 | R-103 | R-102 | ✗ |
Первое расхождение — сразу на item с индексом 0. Именно здесь pairedItem output-item тянется к двум несовместимым source items. Если downstream-нода использует выражение вроде $('Source Requests').item.json.payload, n8n пытается подняться по этому сломанному pairedItem и бросает ошибку Multiple matching items for expression: однозначно выбрать один source item невозможно.
Важный вывод для практики: основной диагностический шаг — сравнение порядка входных массивов до Merge, а не чтение output. Output с переименованными полями лишь подтверждает расхождение наглядно. Когда несовпадение видно уже в первой строке, проверять остальные нет необходимости: любая перестановка делает ненадёжными все затронутые позиции.
Исправление через Merge по request_id и проверка ancestry
Диагностика из предыдущего шага использовала временно переименованные поля (request_id_input1 / request_id_input2) — они нужны только чтобы увидеть оба значения рядом в output Merge. Перед исправлением уберите эти Edit Fields-ноды из workflow: поле request_id должно снова приходить на оба входа Merge под исходным именем. Только тогда режим Matching Fields сможет сопоставлять записи по одному и тому же ключу.
Переключите Merge в режим Mode: Combine → Combine By: Matching Fields и укажите Fields to Match = request_id. Теперь нода ищет совпадения по значению поля, а не по порядку в массиве: для каждого item из входа 1 она находит ровно один item из входа 2 с тем же request_id.
По умолчанию Output Type выставлен в Keep Matches — в output попадают только те пары, где request_id совпал на обоих входах. Поскольку request_id уникален на каждом входе и все три значения присутствуют в обеих ветках, результат детерминирован:
| Output-item | request_id (вход 1) | request_id (вход 2) | Совпадают? |
|---|---|---|---|
| 0 | R-101 | R-101 | ✓ |
| 1 | R-102 | R-102 | ✓ |
| 2 | R-103 | R-103 | ✓ |
Порядок во втором входе больше не имеет значения — Merge сам строит правильные пары.
После переключения первое, что нужно проверить, — количество output-items. Их должно быть ровно три. Если поле потеряно или переименовано в одной из веток, совпадений не будет и Keep Matches вернёт пустой массив. Если нужно сохранить items без пары, выберите Output Type = Keep Everything или Keep Non-Matches; для обогащения данных первого входа без отбрасывания несовпавших строк подойдёт Enrich Input 1.
Теперь верифицируйте, что паросочетание исправлено и downstream-выражение работает. Сделайте это в три шага.
Шаг 1. Убедитесь, что исходный request_id присутствует на обоих входах. Откройте execution data нод обеих веток непосредственно перед Merge и проверьте, что поле называется request_id (не request_id_input1/request_id_input2) и присутствует во всех items. Это предусловие: без него Matching Fields не найдёт ни одного совпадения.
Шаг 2. Проверьте output-items в панели Output Merge. Откройте вкладку Output ноды Merge и для каждого из трёх items убедитесь, что поле request_id имеет ожидаемое значение. Для этой проверки не требуется включать Clash Handling с добавлением номера входа к имени поля: достаточно проверить итоговый request_id и количество строк. Три совпавших строки подтверждают, что Merge построил верные пары.
Шаг 3. Убедитесь, что downstream-выражение разрешается без ошибки. Добавьте после Merge тестовую ноду (например, Set или Edit Fields) и введите в любом поле выражение {{ $('Source Requests').item.json.request_id }}. Запустите workflow. Если для каждого из трёх output-items выражение возвращает тот же request_id, что и в самом item, — цепочка pairedItem однозначна и выражение .item работает корректно. Если Keep Matches возвращает пустой output, проверьте наличие и имя поля сопоставления, а также значения на обоих входах.
Если output-items есть, но выражение бросает ошибку Multiple matching items for expression, проверьте их ancestry: для текущего output-item существует больше одного кандидата из Source Requests.
Когда все три шага дают ожидаемый результат, downstream-выражение работает корректно для каждой из трёх записей fixture.
Задача 1
В двух ветках одинаковые уникальные request_id, но строки идут в разном порядке. Как настроить Merge, чтобы соединять соответствующие обращения?
Задача 2
После какой ноды впервые появляется неверная пара R-101/R-103: Source Requests, Branch 1, Branch 2 или Merge?
Задача 3
Лаборатория: Merge и потеря связей между items
Один и тот же набор запросов объедините двумя способами: по позиции и по request_id. Покажите, почему позиционный Merge молча склеивает чужие записи после перестановки, и оставьте рабочую ветку с явным ключом.
Файлы лабораторной
Скачайте из блока «Файлы шага» n8n_lab_02_setup.py и n8n_lab_02_README.md. Запустите setup-скрипт в пустой папке: он восстановит рабочую структуру и создаст submission/workflow.json.
Зафиксированная среда
n8n
2.39.9; цель:either. Источник фиксации: Pinned course runtime 2.39.9; exact node typeVersions read from the supplied export on 2026-09-22.n8n-nodes-base.manualTriggertypeVersion1— Узел Manual Trigger в выданном exportn8n-nodes-base.codetypeVersion2— Узел Source Requests в выданном exportn8n-nodes-base.settypeVersion3.4— Узел Branch Original в выданном exportn8n-nodes-base.mergetypeVersion3.2— Узел Merge by Position в выданном export
Подготовка
- Создать рабочую копию. Выполните python assets/setup.py, затем импортируйте submission/workflow.json в n8n 2.39.9.
Ожидаемое наблюдение: Создан отдельный submission/workflow.json; исходный starter остаётся неизменным.
Запуск
Исправить workflow. Внесите требуемое lesson-specific изменение в импортированный граф и экспортируйте его поверх submission/workflow.json.
Ожидаемое наблюдение: Изменение присутствует в параметрах и connections экспортированного workflow.Проверить submission. Выполните python assets/run.py. Runner сам читает submission/workflow.json, прогоняет fixtures и перезаписывает observations/report; student_evidence не используется.
Ожидаемое наблюдение: Receipt N8N_LAB_02_OK появляется только для исправленного графа.
Матрица приёмки
position mismatch. Runner подаёт fixture в deterministic engine после проверки текущего submission/workflow.json; ручные actual и execution_id не принимаются. Наблюдения: Позиционный Merge даёт три несовпадающие пары.; Execution показывает, из каких двух входов пришли элементы.. Зафиксируйте: lesson_id, submission_sha256, case_id, derived_actual, observation_sha256, receipt.
key repair. Runner подаёт fixture в deterministic engine после проверки текущего submission/workflow.json; ручные actual и execution_id не принимаются. Наблюдения: Merge по request_id восстанавливает три однозначные пары.; В результате нет потерянных или повторённых request_id.. Зафиксируйте: lesson_id, submission_sha256, case_id, derived_actual, observation_sha256, receipt.
Сброс
- Сбросить derived evidence. Выполните python assets/reset.py. Чтобы вернуть starter, отдельно снова выполните python assets/setup.py.
Ожидаемое наблюдение: Удалены observations.json и verification_report.json; workflow не подменяется скрытым эталоном.
Что отправить
Сдайте receipt из assets/run.py и сохраните generated evidence/verification_report.json. Runner не читает student_evidence.
Обязательные поля: lesson_id, submission_sha256, case_id, derived_actual, observation_sha256, receipt.
В поле ответа вставьте значение receipt из отчёта runner.
Границы проверки
Runner исполняет детерминированную учебную модель узлов и state/stub-контрактов после разбора export; живой n8n 2.39.9 остаётся отдельной ручной проверкой.
Ни actual, ни execution_id, ни текстовое заявление learner_change не принимаются как доказательство.
Продолжить с сохранением прогресса
Перейдите к уроку в учебном кабинете.
