Воронки и триггеры
Воронка — это граф. Запуск — это строка. Ожидание — это время.
Это вся модель, и каждое сложное свойство этого продукта следует из этих трёх предложений. Благодаря им безопасно редактировать работающую воронку. Благодаря им повторная попытка не может отправить письмо дважды. Благодаря им человек, отписавшийся во вторник, не получит ничего в четверг.
Что происходит на самом деле при запуске
Публикация воронки создаёт неизменяемую версию. Кто-то входит — и появляется строка: один запуск, для этого контакта, привязанный к той версии, с текущим узлом, временем пробуждения и собственным контекстом.
Воркер берёт в аренду запуски, у которых время пробуждения наступило, захватывая их с SKIP LOCKED, чтобы два воркера не взяли один и тот же. Он выполняет узлы, пока не дойдёт до ожидания или терминального узла, затем записывает следующее время пробуждения и отпускает запуск.
Узел — это чистая функция от запуска, контакта и самого узла — и его эффекты фиксируются в той же транзакции, что и продвижение запуска. Именно это единственное свойство делает повтор безопасным. Воркер, убитый в полсекунды между «отправить» и «продвинуть», оставляет либо оба изменения, либо ни одного — никогда одно.
И поскольку время подаёт приложение, а не база данных, строка, записанная сейчас, становится актуальной сейчас. Столбец расписания с значением по умолчанию из часов базы данных в момент записи ещё не актуален и пропускает тот самый тик, который его создал, — баг, который никак не проявляется, пока сообщение не опоздает на час.
- 1Аренда
wake_at <= now(),FOR UPDATE SKIP LOCKED. Очередь без аренд по умолчанию отправляет дважды. - 2ВыполнениеУзлы по порядку, пока не встретится ожидание или терминальный узел. Каждый записывает шаг: что он решил и что отправил.
- 3КоммитЭффекты и продвижение — одна транзакция. В этом весь фокус.
- 4СпитЗаписано следующее
wake_at. Строк ниже по потоку ещё нет — и они не нужны.
Один запуск на контакт на версию
Четырнадцать триггеров
Что может запустить запуск
Часть из них — то, что делаете вы. Большинство — то, что делает или не делает ваш клиент, — и отсутствие действия служит триггером не хуже самого действия.
Они приходят
form_submit, list_join, contact_created, tag_added и segment_enter. Сегмент — это живой предикат, поэтому «вошёл в сегмент» покрывает любое «стал», которое вы можете выразить: три заказа, девяносто дней тишины, траты выше порога.
Они что-то делают
page_visit из сниппета сайта, message_opened, link_clicked, booking_made и payment_made. Открытие сообщения само по себе событие на контакте, поэтому одна воронка может ждать реакции на то, что она последней отправила.
Время — или вы
date_field со смещением — день рождения, продление, «год с того момента», за семь дней до или через три дня после. schedule — для повторяющегося. manual — для анонса, который вы отправляете по собственному решению.
Ваше собственное приложение
api_event — это пользовательское событие, которое вы отправляете из своего кода со своим токеном. Всё, что знает ваш продукт, становится триггером: активировался, пригласил коллегу, достиг лимита, отменился, экспортировал. Именно это превращает инструмент рассылок в часть вашего продукта.
Повторный вход, решённый один раз
Воронка либо допускает повторный вход, либо нет, и база данных это гарантирует: один запуск на контакт на воронку, если вы не указали иное. Двойное срабатывание одного триггера в одну и ту же секунду не может случайно создать два запуска.
Условия входа
Триггер срабатывает, и спецификация входа решает, подходит ли именно этот контакт, — предикат сегмента перед дверью. Дешевле, чем узел условия на первом шаге, и граф остаётся о том, что происходит, а не о том, кого пускают внутрь.

Четырнадцать вещей могут его запустить. Шестнадцать вещей могут произойти дальше.
Шестнадцать действий и семь узлов потока
Что может делать воронка и как она принимает решения
Отправить что-то
send_email, send_sms, send_push, send_letter — четыре канала, один граф, — плюс notify_team, когда тот, кого нужно уведомить, работает на вас.
Изменить контакт
add_tag, remove_tag, add_to_list, remove_from_list, set_field. Помечать контакты по ходу — именно это делает сегмент следующей воронки простым.
Выйти наружу
http_request куда угодно, create_payment по вашему собственному подключённому счёту и book_slot по ресурсу бронирования. Плательщик остаётся вашим; никто не становится нашим клиентом.
Переходить между воронками
enter_funnel и exit_funnel. Постройте одну welcome-цепочку и вызывайте её из четырёх мест, вместо того чтобы поддерживать четыре копии, которые к марту разойдутся.
Генерировать в момент отправки
ai_generate заполняет переменные подстановки для каждого контакта, списываясь с вашей квоты и кэшируясь по хэшу креатива и атрибутов контакта, — иначе вы платили бы за каждого получателя при каждой отправке.
Ожидание, ветка, разделение, объединение
wait принимает длительность, дату и время, день недели со временем или поле даты со смещением. condition — это предикат сегмента с ветками «да» и «нет». split — это A/B/n с весами. join снова сводит ветки вместе.
goal — тот, что имеет значение
Цель завершает прохождение для каждого, кто её достиг. Человек забронировал, купил, ответил: остальная часть последовательности отменяется именно для него. Никто не получит «мы по вам скучаем» на следующий день после возвращения — именно этот узел это предотвращает.
throttle и quiet_hours
Throttle сглаживает всплеск отправки, чтобы почтовый ящик с низким лимитом скорости не был завален до блокировки. Тихие часы удерживают сообщение до приличного времени — применяется на уровне сообщения, а не копируется в каждую ветку графа.
Проверяется до запуска
Валидатор отклоняет одно и то же и для вас, и для ИИ
Граф проверяется перед публикацией, и проверка не носит рекомендательный характер. Это один и тот же код независимо от того, нарисовал ли воронку человек или сгенерировала модель — и только поэтому сгенерированным воронкам вообще можно доверять.
Ровно один вход
Два входных узла — это неоднозначность, которую никто не замечает, пока половине списка не придёт не та последовательность.
Каждый узел достижим
Осиротевшая ветка — это либо ошибка, либо сообщение, которое, как вы думаете, уйдёт, но не уйдёт. О том и другом стоит узнать.
Каждый цикл должен содержать ожидание
Цикл без ожидания — это бесконечный цикл с отправкой внутри. Это правило отличает воронку от инцидента.
У каждой отправки есть креатив
Узел send_email без креатива и темы не может быть опубликован. Сообщение с правильной темой и пустым телом — реальный сценарий сбоя; он отклоняется здесь.
Каждая переменная известна
Переменные подстановки проверяются при публикации. {{frist_name}} — опечатка, которую вы видите при редактировании, а не «Здравствуйте ,» в сорока тысячах почтовых ящиков.
Затем смоделируйте её
Прогоните реальный контакт через весь граф в тестовом режиме и прочитайте пройденный путь и каждое сообщение, которое было бы отправлено, с указанием дня. Ничто не покидает систему.
Неудобные случаи
Семь вещей, которые ломают наивный движок
Каждый из них — это тест с детерминированными часами, потому что каждый ломает реализацию, которая выглядит абсолютно корректной, пока не встречается с реальностью.
- 1Воронка, зацикленная на себяЛегитимно, распространено и допустимо — при условии, что цикл содержит ожидание, чего требует валидатор.
- 2Кто-то отписывается на ходуСписок подавления проверяется при отправке, а не при постановке в очередь. Проверка при постановке в очередь отправит письма тем, кто ушёл во время ожидания.
- 3Воронка редактируется, когда внутри десять тысяч человекОни продолжают идти по версии, на которую попали, до конца. Новые участники идут по новой.
- 4Один и тот же триггер срабатывает дважды за секундуОдин запуск, потому что уникальность — это индекс, а не проверка с последующей вставкой.
- 5Контакт удаляется во время прохожденияПрохождение завершается. Удаление, после которого последовательность всё ещё адресована кому-то, — это не удаление.
- 6Ожидание охватывает переход на летнее время«Три дня в девять утра» означает девять утра по обе стороны перевода часов.
- 7Воркер убит между отправкой и продвижениемЭффекты и продвижение — одна транзакция. Либо произошло и то и другое, либо ничего.
Детерминированные часыКаждый тест ломает правдоподобную реализацию
Кампании — вторая половина
Воронка реагирует. Кампания — это разовая рассылка: аудитория, расписание, ограничение частоты и отправка. Это сущность другой формы, и она намеренно выделена отдельно, а не притворяется воронкой с одним узлом.
Два правила делают это безопасным. Аудитория фиксируется один раз и отказывается запускаться повторно — повторное разрешение незаметно добавило бы всех, кто присоединился после начала отправки, а индекс дедупликации скрыл бы это, предотвращая дубликаты, которые сам же создал. И счётчики пересчитываются из событий доставки, а не увеличиваются на месте, поэтому повторный вебхук не может исказить отчёт.
Что использовать: если это происходит из-за действия человека — это воронка. Если из-за решения, которое вы приняли во вторник, — это кампания. Удивительно много маркетинговой автоматизации ломается из-за того, что второе принудительно втискивают в форму первого.