Repository navigation
Timers
Most business processes wait somewhere. An offer goes out only after a cooling-off period. A request is chased after a deadline. BPMN writes this waiting as a timer, and the BPMS (business process management system) does the waiting. Nothing of your application runs in the meantime, no thread of yours and no scheduler of yours.
VanillaBP has no timer of its own. There is no annotation for one and no method on ProcessService. A timer
belongs to the model, and the engine below the adapter executes it. What VanillaBP adds is the value such a timer
reads, because that value comes out of your workflow aggregate.
-
The three forms a timer takes
What BPMN lets a timer say, and the two questions it leaves to the engine. -
Where a timer sits in a model
The three placements, and the one your application notices. -
A deadline out of the workflow aggregate
Reading the time from an attribute instead of writing it into the model, and what ajava.timevalue looks like once it is there. -
How exact the firing is
A timer says the earliest moment it may fire. The rest is up to the engine. -
Which time zone a timer counts in
The question BPMN does not answer, and the shape of a value which survives a change of BPMS.
A BPMN timer event has exactly one of three attributes, and each of them is written as ISO-8601:
| Attribute | What it says | Example |
|---|---|---|
timeDate |
one point in time | 2026-12-24T09:00:00Z |
timeDuration |
how long to wait from now |
PT3S, P14D
|
timeCycle |
a repeating interval |
R1/PT5S, R5/PT10S
|
The same text works on every BPMS, because the attribute is plain BPMN and not a dialect. The blueprint
bpmn-timer has PT3S in its Camunda 7
model and in its Camunda 8 model, and the element is written the same way in both files. An engine may accept
more than these three. The engine's own BPMN reference says what it accepts beyond BPMN, and your
adapter's Configuration page links that reference.
No adapter checks whether that text is a valid date, duration or cycle. VanillaBP asks a start event whether it has a timer at all, because a workflow which the BPMS starts on its own needs an aggregate. Beyond that, reading the value is the engine's job. So a malformed value is the engine's error. It shows up in the engine, not while your application boots. The one thing an adapter does look for inside a timer is an expression, see a deadline out of the workflow aggregate.
There are two more things BPMN does not settle. How often does a cycle run when it names no repetition count? And
what happens to a date which is already in the past when the model is deployed? The engine answers both, not
VanillaBP. So write the repetition count (R1/, R5/) where the number matters, and read your engine's
documentation for the other question.
A timer can sit in three places, and each place means something different.
On a start event, the timer makes the BPMS start a workflow on its own, once or on a cycle. Your application
builds the workflow aggregate before anything else of that process runs, in a @WorkflowStartedByBpms method.
A workflow nobody started through VanillaBP
shows what that looks like. Not every BPMS can do it. The
comparison has a row for it. The Process-Engine-API adapter refuses such a model
while deploying, because its API never reports a start which the application did not ask for.
On a catch event in the sequence flow, the timer is a plain wait. The workflow stops there and goes on when the timer fires. Your application has nothing to do with it and hears nothing about it.
On the boundary of an activity, the timer is a deadline or a reminder, depending on whether it interrupts. An interrupting timer removes the activity, and the token leaves along the path behind the timer. A non-interrupting timer leaves the activity alone and starts a second path next to it.
Only the interrupting boundary event reaches your code, and only if the activity is a task your application left
open. Such a task is canceled. If a handler declared @TaskEvent CANCELED, it is called once more, so it can drop
the task id it kept. When that call arrives differs per BPMS. If the workflow walks into another timer right after
the boundary event, the call may come much later, see
completing and canceling asynchronous tasks.
Answering a task which is gone does nothing on every BPMS, so correctness never depends on that call arriving.
A literal in the model belongs to the process. To change it, you need a new version of the process, and the workflows which already started keep running on the old one. That is the right shape for a cooling-off period which the process itself defines. A deadline which belongs to a contract or to a customer is a different matter. Both Camunda engines take an expression in place of the literal. The expression reads the shared values of your workflow aggregate, so the attribute it names has to travel to the BPMS, see what is shared, and when. This is exactly why Camunda 7 reads the three timer attributes while your application starts. It collects the expressions written in them and reports those which use an attribute the BPMS never gets to see.
What arrives in the model is the text which the Java type writes for itself, and ISO-8601 has more shapes than
java.time writes. Know four points before a model reads such an attribute
(why):
- A
Durationcovers days down to seconds, and aPeriodcovers years down to days. Neither of them writes the combinedP3Y6M4DT12H30M5S. So a span which mixes months and hours has to be two attributes or aString. -
java.timewrites a duration of a day or more in hours, soDuration.ofDays(14)reaches the model asPT336H. The timer fires at the same moment either way, because the engine reads both forms. Only the text an operator reads in the engine is different. - A cycle such as
R5/PT10Sis nojava.timevalue at all. Share it as aString. - A point in time travels as an
Instant, anOffsetDateTimeor aZonedDateTime. Ajava.util.Datetravels as the instant it holds. Ajava.util.Calendaris the one type VanillaBP refuses to share. An aggregate with a shared one does not boot, and the message asks for anInstant(why).
A timer says the earliest moment it may fire. How long it then takes until the workflow actually moves on is up to the engine. Know this before a process promises somebody a reaction within the minute.
Camunda 7 is the one with a number in these wikis. A job which is due later, a timer for example, waits for the next acquisition cycle of the engine's job executor. This cycle is five seconds and grows up to sixty while nothing happens. You can tell the adapter to wait for the due date instead, see a job due later waits for the next acquisition cycle.
On the other two, something which VanillaBP does not run schedules the timer. For Camunda 8 that is a remote cluster. For the Process-Engine-API it is whatever engine sits behind it. Each one's own documentation says what it promises about the moment. Its adapter's Deviations page names what VanillaBP cannot make up for.
BPMN does not say. The words time zone, UTC and daylight saving do not appear in the specification at all. A cycle which means "every Monday at 9am" does not say whose nine o'clock that is. So the engine decides, and the engines do not agree. VanillaBP sets no zone and no clock on any of them. It never reads the date, the duration or the cycle, so there is nothing it could fix. Each adapter's Deviations page says what its BPMS does with a value which names no zone.
A value which carries its own zone or offset travels unchanged. An Instant is shared as
2026-12-24T08:00:00Z, an OffsetDateTime as 2026-12-24T09:00:00+01:00 and a ZonedDateTime as
2026-12-24T09:00:00+01:00[Europe/Berlin]. A timeDate which reads one of those means the same moment wherever
the engine runs. A bare duration does not travel that way. Is P14D fourteen times twenty four hours, or fourteen
calendar days? That is the engine's arithmetic. The two differ by an hour whenever a daylight-saving change falls
in between.
So when the business rule is "in fourteen days at nine in the morning, local time", work out that point in time in
your application, not in the model. Share it as an Instant or a ZonedDateTime, and let the model read it as a
timeDate. Your code knows the customer's calendar. The engine has only the text.
If you need a zone next to the point in time, it travels as a value of its own. A java.time.ZoneId and a
java.util.TimeZone both reach the BPMS as the id of the zone, Europe/Berlin. A java.util.Calendar does
not, and an aggregate which shares one does not boot
(why).
Working days, public holidays and business hours are not part of a BPMN timer at all. So they belong in your application for the same reason.
