Extending the adapter - camunda-community-hub/vanillabp-camunda7-adapter GitHub Wiki

Most applications never need this page. It is for somebody who writes an EXTENSION of this adapter. That is a VanillaBP extension which runs inside the engine this adapter built. The page was written for the VanillaBP Business Cockpit. If you use the adapter and do not extend it, read Configuration instead.

This page exists because Camunda 7 runs embedded. An extension of this adapter runs in the same JVM, on the same execution tree and against the same process definitions. So the adapter has already looked up every fact the extension needs. If the extension reads the engine's internals a second time, that works only until the next engine release.

  1. Hooks inside the engine Where a parse listener and a history event handler go.
  2. What the adapter answers The facts it already looked up, per configured adapter id.
  3. Whose transaction you are in The one answer which used to be given twice, differently.

Hooks inside the engine

VanillaBP builds the plain Camunda engine itself. So Camunda's Spring Boot starter and its engine plugins are not there. Instead of a plugin, an extension uses Camunda7EngineCustomizer (package io.vanillabp.camunda7.engine). On Spring Boot it is a bean of that type, on Quarkus a CDI bean.

Method What it contributes
parseListenersBefore(adapterId) parse listeners running before VanillaBP's own, so a model is seen untouched
parseListenersAfter(adapterId) parse listeners running after VanillaBP's own, which is the usual case
historyEventHandler(adapterId) a handler for the engine's history events, installed next to the engine's own
customize(adapterId, config) the engine configuration itself, for what the three above do not cover

Every method has a default, so add only what you need. The adapter asks a customizer once per configured camunda7 adapter id and passes that id, because two ids are two engines.

A parse listener from parseListenersAfter may attach a built-in task listener. That task listener runs after the ones VanillaBP attaches. An extension which tracks user tasks needs this order. The history event handler is added as a composite next to the engine's own handler. So the history level and everything else about history stay as configured.

What the adapter answers

The package io.vanillabp.camunda7.api holds the facts this adapter already looked up. It is the API of this adapter, not part of the VanillaBP adapter SPI (service provider interface). No other BPMS shares any of these mechanisms.

Entry point What it answers
Camunda7MultiInstances.of(...) the multi-instance scopes an execution runs in, outermost first and across the call activities of one workflow aggregate
Camunda7TaskDefinitions.of(formKey, elementId) what a user task is called: the form key, the element id where there is none
Camunda7TaskDefinitions.formKeyOf(...) the form key as the modeller wrote it
Camunda7Executions.rootProcessInstanceIdOf(...) the root of a workflow, an instance nobody called being its own root
Camunda7EngineFacts the tenant, the transaction answer, the task registry and the deployed version

Camunda7EngineFacts exists once per configured camunda7 adapter id, on both platforms. On Spring Boot it is a bean named Camunda7_EngineFacts_<id>. On Quarkus it is an entry of the produced List<Camunda7EngineFacts>, and adapterId() tells two engines apart.

It gives you two lookups which are worth knowing. The first is taskRegistry().resolve(tenantId, processDefinitionKey). It leads from what the engine reports back to the workflow module and the plain BPMN process id. It is a lookup. Nobody cuts a prefix off a string. The registry fills up while the deployment pipeline runs. It is complete for a workflow module once VanillaBP has wired that module, and complete for the application once the pipeline has finished. An empty answer means that this application deployed no such process. That is normal on a database which another application shares. The second is definitionOf(processDefinitionId). It returns the deployed version from the cache which a running workflow has already filled. Call displayVersion() on the result to get the version as an operator reads it, the same way for every BPMS.

One rule is easy to get wrong. A form key which is an expression resolves to its own text: ${formOf(task)} stays ${formOf(task)}. It never resolves to the value the engine computes from it. The computed value differs per workflow instance. Otherwise a task would reach you under as many identities as it has instances. And the same task would look different while a model is parsed and while a workflow runs on it. Camunda7TaskDefinitions.formKeyOf(...) reads the written text from a model, from a parsed task definition and from a definition the engine holds. So all three give you the same string.

Whose transaction you are in

Camunda7EngineFacts.joinsTheApplicationTransaction() tells you. It is the adapter's own answer, not a property you read yourself.

It is true while the engine runs on the application's data source. That is the normal case, and it is the reason to choose an embedded engine. Anything your extension writes then belongs in the caller's transaction. It happens if and only if the work which caused it was committed.

It is false once the adapter id has a data source of its own, set through vanillabp.adapters.<id>.data-source-name. The engine then writes to a resource which the application's persistence does not take part in. So the two commit separately, and your write needs a transaction of its own. This holds on Quarkus as well, where the engine is built on the container's transaction manager. A JTA (Jakarta Transactions) transaction around two independent data sources is still two commits. The same answer makes such an adapter id deliver repeatable tasks and name its deliveries, see Transactional behavior.