n8n: долгий AI-workflow и быстрый ответ вебхуку с номером задачи

Форма на сайте отправляет документ в n8n. Дальше workflow извлекает текст, обращается к модели, проверяет поля и записывает итог в CRM. Иногда это занимает минуты. Браузер тем временем ждёт ответа на исходный POST, показывает ошибку по таймауту и посетитель нажимает кнопку ещё раз. Два длинных запуска могут создать две записи. Здесь нужно изменить контракт формы: POST подтверждает приём задачи, а готовый результат приходит отдельным способом.

Два коротких шага до ответа

Первый workflow начинается узлом Webhook для POST. После проверки обязательных полей создайте job_id и сохраните запись со статусом queued в постоянном хранилище. Сохраните также ключ повтора от клиента, если он передаётся, чтобы повторный POST вернул прежний job_id. Затем узел Respond to Webhook отправляет HTTP 202 и JSON, например:

{
  "job_id": "job_7f3a",
  "status": "queued",
  "status_url": "/api/jobs/job_7f3a"
}

HTTP 202 означает, что запрос принят к обработке, а итог ещё не готов. job_id должен быть настоящим идентификатором сохранённой записи, а status_url — доступным клиенту адресом. Не возвращайте 202 до успешного сохранения задания: иначе клиент получит номер, по которому нечего искать. В n8n настройте у Webhook параметр Respond как Using 'Respond to Webhook' node, а у ответного узла — JSON и Response Code: 202. Узел отвечает по первому входящему item, поэтому сформируйте один объект с номером задачи.

Как продолжить работу после ответа

Быстрый путь родителя теперь имеет строгий порядок: Webhook → проверка входа → запись queued → Respond to Webhook. Запуск длинной задачи не должен зависеть от того, успел ли родитель перейти к следующему узлу после ответа. Отдельный workflow с Schedule Trigger периодически выбирает сохранённые задачи. Он атомарно меняет состояние выбранной записи на running, назначает новую попытку и срок аренды. Если два запуска расписания увидят одну запись, обработку получает лишь тот, кому удалось захватить её. Затем dispatcher передаёт job_id и attempt_id дочернему workflow через Execute Sub-workflow с выключенным Wait for Sub-Workflow Completion.

Если задания хранятся в PostgreSQL, один узел Postgres в dispatcher может получить работу таким запросом; таблица jobs здесь предполагает поля job_id, status, attempt_id типа UUID, lease_until и result:

WITH picked AS (
  SELECT job_id FROM jobs
  WHERE status = 'queued'
     OR (status = 'running' AND lease_until < now())
  ORDER BY job_id
  FOR UPDATE SKIP LOCKED
  LIMIT 1
)
UPDATE jobs AS j
SET status = 'running',
    attempt_id = gen_random_uuid(),
    lease_until = now() + interval '10 minutes'
FROM picked
WHERE j.job_id = picked.job_id
RETURNING j.job_id, j.attempt_id;

Пустой результат означает, что запускать нечего. Срок аренды выбирайте больше обычного времени AI-ветки или продлевайте во время обработки; иначе медленное задание подхватят повторно ещё до завершения первого запуска. При сбое узел расписания вернётся к нему после истечения аренды.

Дочерний workflow читает вход по job_id и вызывает AI. Результат и статус записываются одним условным обновлением только при совпадении пары job_id и attempt_id и статусе running: UPDATE jobs SET status = 'done', result = $3 WHERE job_id = $1 AND attempt_id = $2 AND status = 'running' RETURNING job_id. Ветка ожидаемой ошибки API тем же условием ставит failed и сохраняет код причины. Передавайте значения параметрами узла Postgres, а не склеивайте SQL из выражений.

После обрыва процесса dispatcher подбирает running запись с истёкшей арендой и назначает новый attempt_id. Если старый worker поздно завершится, его условное обновление вернёт пустой результат: он уже не может перезаписать статус и результат новой попытки. Это защищает запись о задаче, но не отменяет внешний эффект, который старый worker мог успеть выполнить. Для создания объекта в CRM используйте поддерживаемый CRM идемпотентный ключ job_id или уникальное ограничение по нему. Если CRM не даёт такой гарантии, неизвестный исход отправляйте на ручную сверку перед повтором записи.

Для клиента сделайте отдельный GET webhook статуса. Он ищет запись по job_id и возвращает текущее состояние; готовый результат отдаёт только при done. Защитите доступ так, чтобы чужой номер задачи не раскрывал документ или итог. Если внешняя система умеет принимать callback, после сохранения результата отправьте ей событие с тем же job_id; в этом случае GET остаётся запасным способом проверить исход. Callback отправляйте после фиксации статуса: повторную доставку можно распознать по идентификатору.

Проверьте путь на одном медленном документе: POST должен быстро вернуть 202, статус пройти queued → running → done, а результат содержать ссылку на исходный документ. Затем принудительно завершите AI-ветку ошибкой и убедитесь, что статус стал failed. Отдельно остановите dispatcher после записи queued и запустите его снова: он должен подобрать задачу по расписанию. Это другой вопрос, чем повтор уже упавшего исполнения или настройка queue mode: здесь клиент перестаёт держать HTTP-соединение до конца AI-задачи.

В платном курсе «Продвинутый n8n: production-автоматизации и AI-агенты» вы разделяете синхронные и асинхронные sub-workflows, защищаете webhooks, отрабатываете идемпотентность и проверяете сценарии на контролируемых сбоях. Это как раз следующие шаги после ответа 202: довести фоновую задачу до результата, который клиент сможет надёжно получить. Откройте программу курса и посмотрите практику по таким интеграциям.

Продолжить на практике