Deviations - camunda-community-hub/vanillabp-camunda7-adapter GitHub Wiki
This page lists where this adapter does not deliver what VanillaBP promises on every platform. Each entry names the
gap and, if there is one, what will close it. Each entry also links the full explanation in the adapter's
README.md.
Everything not listed here works as the VanillaBP wiki describes it. An engine on its own datasource is not a deviation but a documented mode, see Configuration.
- External tasks are not supported
- A job due later waits for the next acquisition cycle
- A timer without a zone counts in the JVM's zone
- An error nobody catches ends the workflow as completed
- An escalation nobody catches is simply dropped
- A transaction subprocess is the engine's alone
- No element history at history level
none - An overdue open task is only reported
- Compensation handlers are undone in no guaranteed order
- A compensation runs in one transaction
- A standard loop is refused while deploying
- A task definition named like a bean or a shared attribute is refused
- Quarkus: JVM mode only
- Quarkus: no Cockpit, Tasklist and Admin
External tasks are not supported
This adapter does not serve a service task wired by camunda:topic. It delivers tasks through the engine's own
execution, not through the external-task API. Asynchronous work does not need that API. A @TaskId method leaves
the task open, and ProcessService#completeTask completes it later. Such a task is wired by
camunda:delegateExpression. For an engine which can only be reached over its REST API, a separate adapter
camunda7-external is planned.
The adapter refuses to deploy a task wired by camunda:topic, with a message which says what to do. The exception
is a configuration which says that a worker of your own polls the topic: implemented-externally: true for the
task, named by its element id or its topic
(a task something else serves).
If such a task is in a version the engine ALREADY holds, the adapter only logs a warning which names the version.
That model is only read, and nobody can change it any more.
Details
A job due later waits for the next acquisition cycle
A job which is due NOW starts right after the commit which wrote it, because the engine tells its own executor about
such jobs. A job due LATER, such as a timer or the retry of a failed job, waits for the next acquisition cycle. That
is 5 seconds, and it grows to 60 seconds while nothing happens. If you set sleep-until-something-is-due for the
adapter id, the executor waits for the due date instead. This also stops the polling of an idle application. The
key is off by default, because the adapter does not change the timing of an engine unless the application asks for
it. The operator has to add one database index for it. VanillaBP's own outbox keeps polling either way.
Details
A timer without a zone counts in the JVM's zone
A workflow module should behave the same on every BPMS, and a timer is one place where it does not. Nothing in this adapter sets a time zone or a clock on the engine. So the engine reads a timer value the way a Java program reads a date: a value without a zone is read in the zone the JVM runs in.
Measured on 2026-09-26 against the pinned engine 7.24.0, through the engine's own DueDateBusinessCalendar. The
timeDate value 2026-12-24T09:00:00 becomes 03:30 UTC in a JVM on Asia/Kolkata and 09:00 UTC in one on
Europe/Dublin. The same value written as 2026-12-24T09:00:00+02:00 becomes 07:00 UTC in both. So an offset or a
zone in the value settles the question.
Here a duration counts in wall-clock time, not in absolute hours. Measured the same day: the engine clock is on
2026-10-24 at 12:00 in Europe/Berlin. A timeDuration of P1D then comes due on 2026-10-25 at 12:00 local time.
That is 25 hours later, because the change from summer time falls in between. This is handy for a business
deadline. But the JVM's zone decides it, so if you move a container to another zone, the deadline moves with it.
There are two ways to make a timer mean the same thing everywhere. Write the zone or the offset into the value. Or
compute the point in time in your application and share it as an Instant, see
which time zone a timer counts in.
An error nobody catches ends the workflow as completed
BPMN leaves this case open on purpose. When no activity in the hierarchy catches a thrown error, the engine decides what the process does. VanillaBP hands the error over and does not follow it. So your operators see whatever the engine does. On Camunda 7 they see a workflow which looks finished.
Measured on 2026-09-26 against the pinned engine 7.24.0, with a service task throwing a BPMN error whose code no boundary event catches:
- the call which started the workflow returns normally, and so does the task when the job executor runs it,
- no incident is created, and nothing is logged at WARN or above,
- the workflow ends at that task. The end event behind it is never reached, and the history records the instance as completed,
- the end execution listener of the process fires. This adapter reports the end of a workflow from that listener.
The listener carries no delete reason, so a
@WorkflowEndedmethod gets the kindCOMPLETED. The end event it is told about is the id of the activity which threw, not an end event.
So an error code which nobody catches looks like a workflow which ran to its end. You can only find it in the model. Give every code you throw a boundary event. Or catch the codes you do not expect at the outermost activity and let the model decide what happens then, see where that error goes. A cluster does not have to answer the same way: Camunda 8 leaves an incident for this case.
An escalation nobody catches is simply dropped
BPMN says an escalation is not critical: whatever threw it carries on. BPMN does not say what happens to an escalation which finds no catch event. This engine drops it without a word.
Measured on 2026-09-26 against the pinned engine 7.24.0. A process throws an escalation whose code nothing in the model catches. The token walks on from the throw event to the end event, the instance completes, no incident is created and nothing is logged at WARN or above. The workflow looks exactly like one where nobody ever escalated.
VanillaBP does not notice either. At deployment the adapter adds a prefix to an escalation code, like to a message or a signal name, and that is all it does with it. Application code has no way to raise an escalation, see an escalation your code cannot raise. So a mistyped escalation code is found by reading the model and in no other way.
A transaction subprocess is the engine's alone
BPMN allows its cancel event only inside a transaction subprocess. VanillaBP knows neither of them. Nothing in this adapter asks what kind of container an element sits in. Its deployment checks read the processes, the service-like tasks, the user tasks and the start events. The parse listener hooks into the same kinds of element while the engine reads a model. A transaction subprocess is not among them. So nothing is added to it and nothing is refused, and a task drawn inside one is wired exactly like a task outside it.
The engine does run the construct. Measured on 2026-09-26 against the pinned engine 7.24.0: a transaction subprocess holding a cancel end event, with a cancel boundary event attached to it, deploys and executes. The cancel end event fires, the flow leaves the subprocess through the boundary event, and the workflow carries on after it. So the construct works here, and it works without VanillaBP knowing about it.
Everywhere else in these wikis, the word cancel means something different, cancelTask and
WorkflowEnd.Kind.CANCELED above all, see
this cancel is not BPMN's cancel event.
No element history at history level none
An engine with history level none records no activities. So getWorkflowHistory reports the definition the
workflow runs on and a null element history. In the SPI that null is documented as "not supported by the
underlying BPMS". It follows from your engine's history configuration, and a higher history level solves it.
Details
An overdue open task is only reported
A @TaskId handler can leave a task open. VanillaBP measures how long such a task has been waiting, and reports it
once it is older than vanillabp.delivery.max-task-age, see
an open task and how old it is.
This engine holds such a task as a waiting execution, not as a lock which somebody renews. So the adapter has
nothing to stop and no incident to raise. The report is all that happens here.
Compensation handlers are undone in no guaranteed order
BPMN asks for the reverse order here. Default compensation undoes the activities in the opposite order of the one
they ran in. This engine tries to do that. It sorts the compensation subscriptions by the time they were created and
signals the newest first. This fails when two activities were compensated within the same millisecond. That time is
a java.util.Date, and the sort is stable, so the two subscriptions keep the order the activities ran in. Measured
on 7.24, the same test produced both orders within an hour. It depended on whether the clock ticked between the two
activities. So if the undoing only works in one order, the model has to draw that order.
The engine does keep one promise: the handlers run one after the other. It first creates a compensating execution per handler and then signals them one at a time. So the second handler starts once the first one has returned. They also share one transaction, see A compensation runs in one transaction.
Still, both tokens exist from the moment the throw event runs. That is why the deployment reports compensation as two writers on one workflow aggregate. A handler which waits, such as a user task, keeps its token while the next handler starts. Your application may then complete two of them in transactions which overlap.
A compensation runs in one transaction
Here every service-like task of a model gets a job, and so a transaction of its own. A compensation handler is the
one place where the engine does not follow this. It starts such a handler outside the normal flow, where no job is
created. So every handler which a throw event starts runs in the transaction of that throw event. This adapter
writes asyncBefore on the handler like on any other service task, and the engine ignores it there.
Measured on 2026-10-01 against the pinned engine 7.24.0, with a throw event compensating two finished service tasks:
- both handlers ran in the same command context, which is the engine's own unit of work, and ONE commit covered the two of them,
- the two activities they compensated ran in a transaction each, so the flags do work where the engine makes a job of an activity,
- the workflow never held more than one job at a time, and with the job executor running both handlers ran on one thread,
- a handler which threw sent the other one back to work. The engine rolled the whole compensation back, counted one retry off the single job and ran both handlers again on the next attempt.
So five handlers and their side effects share one transaction, and the work of the first four is only as safe as the fifth. The deployment warns about this for every compensation throw event it finds. The warning names the event and the handlers it starts. It is a warning and not a refusal, because the model is right and no flag of this adapter changes what the engine does. (why) Details
What you can change is the handler. Write it so that running it twice does no harm. Keep work which this engine cannot roll back out of it, above all a call to another system. Camunda 8 hands out a job per handler, so it is expected to behave differently. That is not measured yet.
A standard loop is refused while deploying
Camunda 7 has no standard loop either. A modeller draws the marker standardLoopCharacteristics to repeat one
activity while a condition holds. An activity with this marker deploys in the engine without a word and then runs
exactly once. Measured on 7.24 with a loop condition of ${true} and a loop maximum of 3: the activity ran once, the workflow
ran to its end, and the engine wrote nothing above DEBUG about any of it. The string appears in camunda-engine only
in the BPMN schema file the jar ships, in no class of it, and BpmnParse reads multiInstanceLoopCharacteristics
alone.
Neither Camunda engine implements the marker, and both keep quiet about it. That is why the same entry is on the Camunda 8 Deviations page.
The adapter closes the gap. If a @WorkflowService class claims the process, the adapter refuses a model with a
standard loop while deploying it, and the boot ends there. The message names the activity, the BPMN process and the
workflow module. It also names the two forms which do repeat an activity: a loop in the sequence flow, or a
multi-instance activity, where @MultiInstanceElement, @MultiInstanceIndex and @MultiInstanceTotal work. Such a
model deployed in version 1, and it does not deploy any more, see
the upgrade notes.
A process nobody claims is somebody else's model, and the adapter does not look at it.
A version the engine already holds cannot be changed, so it is not refused. If workflows still run on such a version, the start logs a WARN. It names the version, the activity and how many workflows run on it. A version on which no workflow runs any more is skipped.
A task definition named like a bean or a shared attribute is refused
Camunda 7 finds the method of ${chargeCard} by asking for the name chargeCard. It asks its process variables
first, on Spring Boot the beans of the application next, and VanillaBP last. Whatever answers first wins. So a
Spring bean named chargeCard, or an attribute chargeCard which the workflow aggregate shares with the engine,
takes the expression away from the @WorkflowTask method. The method never runs. Camunda 8 has no such problem,
because a task there names a job type and no expression is resolved.
The adapter refuses such a model while it deploys it, and the boot ends there. The message names the task
definition, the BPMN process, the element and the workflow module. It also names the bean and its class, or the
attribute. To fix it, rename the task definition in the BPMN and in the @WorkflowTask method, rename the bean or
the attribute, or keep the attribute away from the engine with @NoSyncWithBPMS. A version 1 application with
such a bean no longer starts, see
the upgrade notes.
Its method never ran in version 1 either.
The check only knows the names which exist at the start. A process variable which the model or your code creates while a workflow runs, for example by an output mapping, is not checked. Do not give it the name of a task definition. A user task is not affected, because its form key is not evaluated as an expression. An element id is never an EL name, so a bean named like an element id changes nothing. But the expression of a task whose method is wired by the element id is checked like a task definition, because the engine resolves it the same way.
Quarkus: JVM mode only
The Quarkus extension does not support native images. The engine stack uses a lot of reflection: MyBatis, JUEL, scripting, and creating delegates by reflection. Camunda never supported native images, and neither do the forks. So this will not change. Details
Quarkus: no Cockpit, Tasklist and Admin
Camunda's web applications are a servlet application built on Spring, so they run on Spring Boot only. Nothing equivalent is planned for Quarkus. Details