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.

  1. External tasks are not supported
  2. A job due later waits for the next acquisition cycle
  3. A timer without a zone counts in the JVM's zone
  4. An error nobody catches ends the workflow as completed
  5. An escalation nobody catches is simply dropped
  6. A transaction subprocess is the engine's alone
  7. No element history at history level none
  8. An overdue open task is only reported
  9. Compensation handlers are undone in no guaranteed order
  10. A compensation runs in one transaction
  11. A standard loop is refused while deploying
  12. A task definition named like a bean or a shared attribute is refused
  13. Quarkus: JVM mode only
  14. 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 @WorkflowEnded method gets the kind COMPLETED. 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