Эта статья продолжает цикл BPMN: Beyond the Basics, и сегодня мы поговорим о том, как BPMN управляет временем. Спойлер: весьма посредственно.

Нотация BPMN изначально создавалась как инструмент для описания последовательности действий, а не временных зависимостей. Основное внимание в моделях BPMN уделяется тому, что должно произойти, в каком порядке, при каких условиях — но не когда именно.

BPMN и иллюзия точного времени

Когда аналитик ставит на диаграмму таймер, он обычно думает, что событие произойдет точно в назначенное время. На самом деле это не так, но пока речь идет о пользовательских задачах, незначительную погрешность можно игнорировать, люди все равно тормозят больше. Другое дело, когда при помощи BPMN начинают моделировать оркестрацию сервисов — здесь мы вступаем в зону, где интервалы таймеров малы и становятся сопоставимы с погрешностью их выполнения, что может привести к неработоспособности процесса.

В инженерной практике существуют системы реального времени (RTS) — например, QNX, RTEMS, FreeRTOS, где время — это первоклассная сущность, и гарантии временного исполнения встроены в ядро.

Системы BPM — совсем другая история. Они построены поверх универсальных операционных систем и виртуальных машин, таких как JVM, и не могут гарантировать исполнение задач с точной временной меткой. Таймеры в BPM — это скорее ориентиры, нежели жесткие дедлайны.

Когда наступает момент срабатывания таймера в BPM-движке, сам таймер отмечается как достигнутый и процесс продолжает выполнение. Однако реальное время срабатывания таймера зависит от нагрузки, приоритетов и работы планировщика. Поэтому для BPM-движка задержка в несколько секунд или даже десятков секунд — обычное дело.

Характеристика

Система реального времени (RTS)

JVM

BPM-движок

Точность времени

Обычно миллисекунды, но можно добиться точности даже до микросекунд

Миллисекунды и выше, зависит от нагрузки

Секунды и больше, нет жестких гарантий

Планирование задач

Аппаратные прерывания, мгновенная реакция

Планировщик потоков ОС, кооперативная многозадачность

Асинхронные задачи с очередями, отложенное исполнение

Цель системы

Критичные задачи, управление оборудованием

Общие вычисления и приложения

Автоматизация бизнес-процессов

В общем, не пытайтесь моделировать процесс с точностью до миллисекунд и даже до секунд. Иногда это работает, но в самый неподходящий момент может и тормознуть. Просто примите как факт. А вот почему это так, давайте разберемся.

Освежим теорию

Прежде чем двигаться дальше, напомним о видах таймеров, которые используются в BPMN.

Первый — таймер на старте процесса (Start Timer Event). Он используется, когда процесс должен запускаться по расписанию: каждый понедельник в 9 утра, раз в час или, скажем, ровно 1 января. Такой таймер автоматически инициирует экземпляр процесса в заданное время, и это часто используется для плановых задач.

Второй — промежуточный таймер (Intermediate Timer Event). Он работает внутри процесса и используется для пауз: «подождать 10 минут», «задержать выполнение до завтра» и так далее. Это удобный способ сделать паузу между шагами — например, чтобы дать время на ручную проверку.

И, наконец, таймер на границе задачи (Boundary Timer Event). Он прикрепляется к конкретной задаче и срабатывает, если пользователь не справился в срок. Такой таймер часто используется для эскалаций: если задача зависла, система может автоматически передать ее другому исполнителю или уведомить руководителя, если это была пользовательская задача.

Причем он может быть прерывающим или непрерывающим, то есть при его срабатывании задача либо прерывается и процесс и дет по новому пути вслед за таймером, либо таймер запускает еще одну ветку выполнения, не прерывая задачу.

И, наконец, бывает еще непрерывающий стартовый таймер, который встречается только в событийных подпроцессах.

Таймеры глазами аналитика

Для аналитика таймер — это «будильник на диаграмме», когда зазвенит, тогда и двигаемся дальше. Причем время срабатывания обычно задается просто текстом: «каждый вторник в 11 утра», «подождать 3 минуты», «напомнить через 10 минут», «эскалировать через 30 минут».

Для обсуждения процесса с бизнесом такой ментальной модели в принципе достаточно. Но вы же понимаете, что для движка эти надписи на диаграмме ничего не значат, движку надо задать конкретные параметры.

Здесь мы переходим к настройке параметров таймеров, которые записываются в XML-файл процесса и могут быть интерпретированы движком во время выполнения. Внешне все просто — согласно спецификации BPMN имеется три типа таймеров:

  • Duration (отложенный запуск)

  • Cycle (циклический)

  • Date (конкретная дата)

Семантика у них следующая: duration-таймер приостанавливает процесс на заданный период, аналогично sleep() в Java, циклический — то же самое, только с повторами, а последний, как следует из названия, срабатывает в заданную дату.

Чисто из логики промежуточный таймер не может быть циклическим — ведь процесс уже ушел дальше после его первого срабатывания. Однако, моделеры процессов, например Camunda, не запрещают ставить циклический таймер в середине процесса, полагаясь, видимо, на здравый смысл разработчиков. Также, вы, наверное, догадались, по жизни не может быть циклическим и прерывающий граничный таймер — но эту опцию можно выбрать в моделере.

Еще один момент: стартовые таймеры хороши для наглядности, но неудобны на практике. Расписания запуска могут меняться чаще, чем сами модели, а когда они зашиты в параметрах таймера, то придется передеплоивать весь процесс и плодить новые версии.

ISO 8601: стандарт для машин, не для людей

Отдельную боль доставляет откровенно недружелюбный стандарт ISO 8601, который используется для задания параметров таймеров. Он создавался как машино-читаемый стандарт для представления дат и времени, интервалов, длительностей и повторений, а user experience в его цели никогда не входил.

На первый взгляд все кажется простым:

  • PT5M — 5 минут

  • P1DT2H —1 день и 2 часа

  • R3/PT1H —3 повтора каждый час 2026-02-10 — 10 февраля 2026 года, никакого подвоха

  • R/2026-02-02T10:00/P1D — каждый день в 10:00, начиная со 2 февраля 2026

  • R/2026-02-02T10:00/P1D/2026-02-17T10:00 — а можно добавить и конечную точку, повторять до 17 февраля

Но вот по��вляются первые ловушки и неудобства:

  • P1M и PT1M — в первом выражении ’M’ значит месяц, во втором — минуту

  • P1W — одна неделя, но не все движки поддерживают символ ’W

  • PT1H60M — стандарт допускает и такую дичь, длительность 1 час и 60 минут, то есть два часа, но логичнее было бы PT2H или PT120M

  • P1DT0H — один день ноль часов, так тоже можно, но выглядит странно

  • 2026-02-10T10:00Z — время в формате UTC (zulu time), однозначно, но не бизнес-дружелюбно

  • 2026-02-10Т10:00+03:00 — здесь надо помнить, что это именно сдвиг по времени от UTC, а не поясное время, которое может меняться по сезонам или по воле властей

С переходом на зимнее/летнее время это вообще отдельная история, тут могут быть интересные казусы:

  • PT24H ≠ P1D формально оба выглядят как «сутки», но разница проявится в день перехода, когда календарные сутки составляют 23 или 25 часов, соответственно такие таймеры сработают в разное время

  • 2026-03-29T02:30 — внешне все окей, но для таймзоны Europe/Berlin такого времени просто не существует, этот час «исчезает», потому что включается летнее время

  • 2026-10-25T02:30— а это время наоборот наступит дважды при переключении на зимний режим

Теперь давайте посмотрим, как задаются циклические таймеры:

  • R1/PT1M —повторить один раз, это просто; ну или R100500/PT1M

  • R/PT1M — повторять бесконечно, но допустимо также и R-1/PT1M со значением параметра «-1»

  • R0/PT1M — синтаксически валидно, но семантически бессмысленно; это не выполнится ни разу

Стоит упомянуть еще один момент: ISO 8601 разрешает произвольную точность:

  • 2026-02-10T09:00:00.123456789 —с точки зрения стандарта это корректная запись, но движок с такой точностью не умеет работать, ему придется округлять или обрезать эту запись

Засада в том, что разработчики популярных BPM-инструментов оставили ISO 8601 в чистом виде прямо в UI своих продуктов. Формально это корректно, но для людей — неудобно и регулярно становится источником ошибок.

Таймеры в XML: строки с отложенным смыслом

Важное замечание: далее мы переходим к обсуждению конкретных технических реализаций на примере Jmix BPM/Flowable и Camunda 7.
В других движках это может быть реализовано иначе.

В BPMN таймеры в XML выглядят обманчиво просто, но под капотом скрывается несколько важных нюансов, которые часто ускользают от внимания аналитиков и даже разработчиков.

Таймер в BPMN описывается элементом <timerEventDefinition>, который вложен в событие одного из типов — стартовое, промежуточное или граничное. Например, так:

<intermediateCatchEvent id="timer-event" name="1 day">
      <incoming>Flow_1</incoming>
      <outgoing>Flow_2</outgoing>
      <timerEventDefinition id="TimerEventDefinition_1rah8yl">
        <timeDuration xsi:type="tFormalExpression">P1D</timeDuration>
      </timerEventDefinition>
</intermediateCatchEvent>

И в этом <timerEventDefinition> должен быть указан один из трех вложенных элементов:

  • <bpmn:timeDate></bpmn:timeDate>

  • <bpmn:timeDuration></bpmn:timeDuration>

  • <bpmn:timeCycle></bpmn:timeCycle>

Здесь-то и таится подвох: с точки зрения BPMN это просто строки, которые никак не валидируются. То есть, если что-то неправильно, это выявится только во время выполнения, когда процесс до этого таймера дойдет.

Однако сделано так не по недосмотру. Такое решение позволяет использовать выражения, вычисляемые в runtime:

  • <bpmn:timeDuration>${retryInterval}</bpmn:timeDuration>

  • <bpmn:timeDate>#{order.dueDate}</bpmn:timeDate>

В этом случае запись в формате ISO 8601 оказывается лишь частным случаем выражения, и становится логичным, что значение таймера вычисляется не при деплое процесса, а в момент активации события — при входе в activity или создании execution.

Есть и еще один важный нюанс: BPMN XML не фиксирует семантику длительностей. Например, P1D может интерпретироваться как календарный день или как ровно 24 часа — это полностью зависит от конкретной реализации движка.

Наконец, в ISO 8601 невозможно задать полноценную таймзону (например, Europe/Berlin), а только фиксированный сдвиг относительно UTC. Поэтому в процессах, работающих в нескольких часовых поясах, ответственность за корректную временную координацию полностью ложится на разработчика.

В результате таймер в BPMN XML — это не всегда точное указание времени, а некое описание, окончательный смысл которого определяется только в runtime конкретного движка.

Как таймер работает

Представим, что токен движется по BPMN-модели. Он последовательно проходит шаги — пользовательские задачи, шлюзы, события. И вот он подходит к таймеру.

Если это Intermediate Timer Event, токен как будто упирается в табличку: «Стоп. Жди». С этого момента таймер активируется, то есть создается так называемый timer job. Если у таймера была конкретная дата, то она и устанавливается как время срабатывания (due date), а если это было выражение или duration-timer, то дата вычисляется. Токен, тем временем, просто «замораживается» — он никуда не идет дальше, пока не наступит заданный момент. Процесс переходит в состояние ожидания.

Ключевую роль в этом играет Job Executor — компонент BPM-движка, который отвечает за выполнение асинхронных задач. Он регулярно просматривает все job’ы и определяет дальнейшие действия. Как видите, таймеры тоже находятся в его ведении.

Если же это Boundary Timer Event, то сценарий чуть сложнее. Токен заходит в задачу, и одновременно рядом в фоновом режиме заводится таймер. То есть, фактически создается новая параллельная ветка (execution), которая сразу оказывается в состоянии ожидания. Если задача завершится раньше, чем истечет срок таймера, то процесс продолжится по основному пути, а таймерный job аннулируется.

Но если время вышло — срабатывает таймер, а дальше в зависимости от типа таймера. Если он непрерывающий, то запустится новая параллельная ветка, а задача все также будет ждать завершения. В случае, когда таймер прерывающий, задача принудительно завершится, а токен уходит по альтернативному маршруту (например, в эскалацию).

А вот Start Timer Event вообще не зависит от токенов. Тут все наоборот: Job Executor сначала создает подписку на таймерное событие, а когда оно наступит, то запустится новый экземпляр процесса.

У Cyclic Timer, будь это стартовый или непрерывающий граничный, жизнь еще интереснее. Когда процесс ��оходит до циклического таймера с параметром, допустим, R3/PT1 (то есть, повторить три раза с интервалом одна минута), то в БД запишется выражение, где вместо длительности интервала стоит конкретная дата-время:

  • R3/2026-02-11T12:20:50.041+03:00/PT1M

Вы спросите, как же Job Executor считает количество повторов? Должна же быть ячейка с каким-то счетчиком срабатываний. — А ее нет! — Когда указанное время наступит, будет создан новый timer job со счетчиком на единицу меньше:

  • R2/2026-02-11T12:20:50.041+03:00/PT1M

А потом еще один:

  • R1/2026-02-11T12:20:50.041+03:00/PT1M

То есть, сервер каждый раз парсит таймерное выражение и генерит новое. И каждый раз заново создает job. Выглядит немного странно, но так это работает.

Где живут таймеры

Теперь посмотрим более технически, что происходит с таймерами во время выполнения.

Когда токен в процессе достигает таймера (будь то промежуточный или граничный), движок создает объект TimerJobEntity — это Java-объект, представляющий отложенное задание. Этот объект сериализуется и сохраняется в таблицу ACT_RU_TIMER_JOB (в Flowable) или ACT_RU_JOB (в Camunda). Он содержит точную дату и время исполнения (DUEDATE_), тип таймера (HANDLER_TYPE_), и ссылку на текущий контекст исполнения процесса (EXECUTION_ID_, PROC_INST_ID_).

Если таймер повторяющийся (в том числе, cron-выражение), в записи также присутствует поле REPEAT_, по которому Job Executor будет заново планировать следующий запуск, создавая новые job’ы.

Ниже состав полей таблицы ACT_RU_TIMER_JOB — не занудства ради, а чтобы показать, насколько это сложная штука таймеры, что приходится держать в БД аж 27 атрибутов, чтобы обеспечить работу с ними!

Поле

Назначение

Комментарий / смысл

ID_

Идентификатор job

Уникальный ID таймера

REV_

Версия записи

Optimistic locking для конкуренции executor’ов

CATEGORY_

Категория job

Обычно null

TYPE_

Тип job

Для таймеров — timer

LOCK_EXP_TIME_

Время истечения лока

После этого job может быть захвачен другим узлом

LOCK_OWNER_

Владелец лока

Идентификатор job executor’а / ноды

EXCLUSIVE_

Эксклюзивность

true по умолчанию

EXECUTION_ID_

Execution ID

Ветка, которая будет продолжена

PROCESS_INSTANCE_ID_

Экземпляр процесса

Удобно для администрирования, чтобы не искать процесс по execution

PROC_DEF_ID_

Process Definition ID

Определение процесса

ELEMENT_ID_

BPMN element id

ID таймерного события в модели

ELEMENT_NAME_

BPMN element name

Человекочитаемое имя (UI / логирование)

SCOPE_ID_

Scope ID

Обычно null

SUB_SCOPE_ID_

Sub-scope ID

Обычно null

SCOPE_TYPE_

Тип scope

Обычно null

SCOPE_DEFINITION_ID_

Definition scope ID

Обычно null

CORRELATION_ID_

Correlation ID

Обычно равен ID_

RETRIES_

Количество попыток

Внимание: это не количество повторов циклического таймера!

Это количество попыток завершить этот job

EXCEPTION_STACK_ID_

Stacktrace ID

Ссылка на ACT_GE_BYTEARRAY

EXCEPTION_MSG_

Сообщение об ошибке

Текст последнего исключения

DUEDATE_

Время срабатывания

Момент, когда job можно взять в работу, это не гарантия исполнения в указанный момент

REPEAT_

Повторение

Для циклических таймеров выражение в формате ISO-8601

HANDLER_TYPE_

Тип обработчика

timer-start-event, t`rigger-t`imer и т.п.

HANDLER_CFG_

Конфигурация обработчика

Например, {"activityId":"startEvent1"}

CUSTOM_VALUES_ID_

Custom payload

Ссылка на ACT_GE_BYTEARRAY

CREATE_TIME_

Время создания

Полезно для диагностики задержек

TENANT_ID_

Tenant

Multi-tenant изоляция

А часики-то тикают!

Копнем еще глубже! Нужно еще разобраться, как именно исполняются таймеры. Потому что, если этого не понимать, можно легко загнать систему в ступор.

Естественно, пока у вас немного инстансов с обычными «человеческими» таймерами на часы или дни, например, в документообороте, то эти тонкие детали можно игнорировать. Но если говорить об автоматизированных процессах и о большом количестве экземпляров, то такие мелочи становятся важными.

Вот довольно типичный сценарий обращения к какому-то сервису. Таймер используется, чтобы повторить попытку, если сервис не отвечает. (Можно, конечно, использовать механизм Fail retry, но это сути не меняет, он работает примерно так же.)

Допустим, таких процессов у вас сто тысяч. Итак, в БД лежат timer job’ы, в которых проставлен параметр due date. Что делает Job Executor? — Он с заданной частотой проверят, не пора ли какой-то процесс продвинуть дальше. Делается это довольно эффективно — таблица проиндексирована по полю DUE_DATE_ и выполняется примерно такой запрос:

SELECT *
FROM ACT_RU_TIMER_JOB
WHERE DUEDATE_ <= CURRENT_TIMESTAMP
  AND LOCK_OWNER_ IS NULL
ORDER BY DUEDATE_
LIMIT 10
FOR UPDATE;

Периодичность опроса БД во Flowable задается параметром asyncExecutorAcquireWaitTime со значением по умолчанию 1000 миллисекунд; в Camunda 7 за то же самое отвечает параметр waitTimeInMillis, равный обычно 5000 миллисекунд.

Пока таймеров немного, нагрузкой на базу данных от таких запросов можно пренебречь. Что такое «немного» вопрос неоднозначный, это зависит прежде всего от железа. Но какой бы мощный сервер вы ни взяли, с ростом нагрузки обработка таймеров станет узким местом.

Потому что чтением из базы все не заканчивается, нужно еще записать данные, чтобы продвинуть процесс дальше. А в это время продолжают поступать запросы на запись от новых таймеров…

Скажем прямо: архитектура BPM-движков семейства Activiti — Camunda 7 —Flowable на такой режим работы не рассчитана. Они проектировались как транзакционные процессные движки поверх реляционной БД, где таймер — это запись в таблице и периодический polling.

Camunda 8 устроена иначе: там нет SQL-polling и таблицы job’ов, таймеры обрабатываются внутри распределенного потокового движка (Zeebe) и партиций состояния. Это снимает проблему конкуренции за БД и позволяет лучше масштабировать массовые таймеры. Но и она не является системой реального времени — задержка исполнения по-прежнему зависит от нагрузки и скорости обработки событий внутри партиции.

Однако, люди любят таймеры. Часто ими обвешивают все что нужно и не нужно. Через пять минут напомнить исполнителю, через 10 минут сказать начальнику, потом еще выше…

В принципе, при помощи BPMN можно моделировать любые процессы, хоть высокочастотый трейдинг — но только на уровне логической модели! Переходя к имплементации, надо учитывать возможности и ограничения ко��кретных BPM-движков.

Практические рекомендации

Таймеры в BPM стоит использовать как инструмент управления бизнес-ожиданием, а не как механизм точного планирования или технического ретрая. Они хорошо подходят для эскалаций, SLA, напоминаний и отложенных проверок — то есть для ситуаций, где допустима погрешность в секунды и даже десятки секунд. Если же модель начинает требовать реакций с высокой частотой или жесткой синхронизации между сервисами, это уже сигнал, что задача выходит за пределы типичного применения BPM.

Не следует закладывать в модель короткие интервалы — секунды или десятки секунд — при массовом количестве экземпляров процесса. Даже при отсутствии нагрузки нижняя граница точности ограничена периодом опроса executor’а, а при росте числа таймеров возрастает конкуренция за базу данных. В результате попытка «ускорить» реакцию системы может привести к обратному эффекту — росту задержек и блокировок. Если требуется частый технический retry внешнего сервиса, лучше использовать встроенный механизм fail retry или внешние инструменты устойчивости (circuit breaker, очередь сообщений), оставляя BPM на уровне бизнес-логики.

Отдельного внимания требует работа с часовыми поясами и переходами на летнее/зимнее время. В модели стоит явно определить стратегию: хранить время в UTC, документировать системную таймзону и избегать смешивания фиксированных смещений с поясным временем. Если важно именно «каждый день в 10 утра по местному времени», поведение в дни DST должно быть заранее проверено и осмыслено, иначе возможны неожиданные пропуски или дубли срабатываний.

Циклические таймеры нужно применять осторожно. Бесконечные повторы без четкой бизнес-причины превращаются в генератор job’ов и создают постоянную нагрузку. Если цикл используется, у него должно быть либо ограничение числа повторов, либо бизнес-условие, которое гарантированно завершит ожидание. Таймер не должен быть способом организации бесконечного polling внутри процесса.

Наконец, параметры executor’а — это не просто технические детали по умолчанию, а часть архитектурного решения. Период опроса, размер пула потоков и лимит захватываемых job’ов напрямую влияют на поведение таймеров под нагрузкой. Их необходимо подбирать исходя из реального профиля процессов и тестировать на объёмах, близких к продакшену. BPM-движок надёжен в своём классе задач, но он остаётся инструментом бизнес-координации, а не системой высокоточного или высокочастотного планирования.


Таймер в BPM — это инструмент управления ожиданием в бизнес-процессе. Он не гарантирует точного времени исполнения, не предназначен для частотного планирования и не является заменой систем реального времени. Это механизм координации, а не синхронизации.