Configuration - camunda-community-hub/vanillabp-camunda7-adapter GitHub Wiki

Two things have to fit for this adapter: its properties and your BPMN (Business Process Model and Notation) model. Every key below is optional. Say your application is happy with an engine on its own datasource and a tenant per workflow module. Then all it needs from this page is the minimal configuration and the section about the BPMN model.

Looking for a key you had in VanillaBP 1? It is still documented, in the migration guide of the platform wiki: Migrating from VanillaBP 1 to 2.

  1. The adapter's properties
    Every key of vanillabp.adapters.<id>.*, what a handler may assume about its thread, how an idle engine goes quiet, and how to create the engine tables yourself.
  2. The BPMN model for Camunda 7
    What your model has to carry so this engine wires it, and what the adapter adds for you. Which process a message starts, what a task of a called process learns about its iteration, and the listeners the adapter serves only if you ask for it.
  3. Decision tables
    How a business rule task finds the decision your module brought, and what your code gets back.
  4. What reaches the engine
    The shared values as process variables, when they are written, where the name of a workflow is kept, and what a workflow started under VanillaBP 1 still reads. A value this engine has no type for gets a page of its own, What becomes of a shared value.
  5. Keeping workflow modules apart
    What the three name-clash-avoidance modes deploy on Camunda 7 and what this engine needs for them. What the start asks the engine about the names you deploy, and when two workflow modules with one BPMN process id stop the boot.
  6. Choosing the datasource
    Which datasource the engine runs on, and what a datasource of its own changes about duplicate deliveries.
  7. Several engines side by side
    Two adapter ids of this type in one application, per platform, and the case where they share one datasource.
  8. The versions this engine still holds
    What the boot checks about older process versions, which methods a workflow of an older version reaches, and how to stop the check for versions you retired.

The adapter's properties

You configure the adapter at VanillaBP's usual place for each adapter, vanillabp.adapters.<id>.*. Here <id> is the id of the adapter instance (see Configure the adapters). All keys below are optional. This includes type, as long as the adapter id IS the type (see Minimal configuration):

vanillabp:
  prioritized-adapters:
    - c7
  adapters:
    c7:
      type: camunda7   # only needed because the id 'c7' is not the type
      # create/upgrade the engine schema on boot (engine values, e.g. true, false,
      # create-drop); default: true
      database-schema-update: true
      # engine-wide default history time-to-live (Camunda 7.24 rejects deployments
      # of processes without one); default: P180D; a process may override it via
      # camunda:historyTimeToLive
      history-time-to-live: P180D
      # OPTIONAL: the name of an APPLICATION-PROVIDED datasource this id's engine
      # runs on - see 'Several engines side by side' below
      data-source-name: legacy
      # OPTIONAL: the job executor waits until the next job is due instead of polling
      # every 5 to 60 seconds - see 'Letting an idle engine go quiet' below
      sleep-until-something-is-due: true
Key Default Description
type the adapter id must be camunda7. You may leave it out if the adapter id already IS camunda7
database-schema-update true creates or upgrades the engine schema (the ACT_* tables) on boot. The engine's values apply (true, false, create-drop, …). Turn it off if you manage the schema yourself, see Creating the engine tables yourself
history-time-to-live P180D default history TTL (time to live) for the whole engine. Camunda 7.24 rejects deployments of processes without one. A process may still override it with camunda:historyTimeToLive. Once the engine has cleaned up an ended workflow, viewing it raises the guiding WorkflowNotFoundException
data-source-name — name of a datasource your application provides, for this engine to run on. Required if the application provides several datasources. It is also one of the two ways to keep several adapter ids apart. Read the caveats below
accept-unscoped-identifiers false says that your identifiers are unique across all workflow modules. This silences the WARN logged wherever the mode none applies. The default mode is by-adapter, see Keeping workflow modules apart
tenant-id — names the Camunda tenant used by the name-clash-avoidance mode by-adapter (default: the workflow module id, as in version 1). You may set it for one workflow module instead of the whole adapter (vanillabp.workflow-modules.<module>.adapters.<id>.tenant-id). This is how a single module gets a scope of its own. If no level uses by-adapter, setting it fails the boot, because the deployment would ignore the name
table-prefix — prefix of the engine's database tables (engine setting databaseTablePrefix). With it, two adapter ids can share ONE datasource and still run separate engines. The prefixed tables have to exist and database-schema-update has to be false, because Camunda does not create prefixed tables. See One datasource, two engines
sleep-until-something-is-due false the job executor waits until the next job is due instead of polling every 5 to 60 seconds. A transaction which writes a job wakes it. It needs one database index and it changes the order in which jobs are picked up, see Letting an idle engine go quiet
db-metrics-reporting follows the key above whether the engine's metrics reporter writes its counters to the database every 900 seconds. If unset, it stays on while the executor polls and goes off once the executor may wait. So a waiting engine is not woken by its own metrics
allow-listeners false whether a @WorkflowTask method serves a camunda:executionListener of your model. If off, a model with such a listener does not boot, and the message says what to do. If on, each listener gets one method, with all the costs the switch brings. You can set it per workflow module and per workflow as well, see Listeners somebody modelled

Retries are up to the engine, not the adapter. By default the job executor retries a failed task three times. camunda:failedJobRetryTimeCycle in the model sets this per task. The adapter has no keys at task level. VanillaBP has a four-level configuration pattern, and this adapter reads only the three levels above the task. A value written at task level has no effect. serialization-format and allow-listeners can be set down to a single workflow, and tenant-id down to a workflow module. sleep-until-something-is-due exists only at adapter level, because there is one job executor per engine.

This also answers what happens if the aggregate cannot be saved because another branch of the workflow changed it in the meantime (two writers on one aggregate). The job fails, the executor retries it as configured, and an incident is created once all attempts are used up. The embedded engine owns the transaction while it delivers a task, whichever datasource the adapter id runs on. So the conflict fails THAT transaction and VanillaBP never sees it. The platform logs a guiding message for a conflict in a transaction of its own, but that message does not appear here. Cockpit shows the incident instead.

What a handler may assume about its thread

Camunda 7 runs inside your application, and it delivers work in two ways. A workflow which a caller starts or continues is moved on after the caller's commit, through the phase-two outbox, not in the caller's transaction (see Transactional behavior). The @WorkflowTask methods behind an asynchronous continuation are different: the engine runs them, not the caller. Its job executor picks them up. That is a thread pool of its own, with three threads by default. Here is what you should know about it:

  • a @WorkflowTask method holds one job executor thread for as long as it runs. If it blocks there, it delays the timers, the asynchronous continuations and the retries of every workflow of that engine;
  • there is one such pool per adapter id, because each id runs its own embedded engine;
  • jobs of ONE workflow instance do not run at the same time, as long as you keep the engine's default. Camunda 7 marks such a job as exclusive (camunda:exclusive, true unless a model says otherwise). So a second branch of the same instance waits and does not race the first. This makes two writers on one workflow aggregate rarer here than on a remote BPMS. It does not make them impossible, because your own API calls write the aggregate outside the engine's threads anyway;
  • the handler shares the engine's transaction, so it holds the database connection of that transaction while it runs. Size the pool to match the connection pool, not the CPU.

The adapter does not offer the pool size as a property today. If you need to change it, tell us and it becomes one. Until then the engine's defaults apply.

Camunda 8 works differently here, because its cluster hands work to polling workers outside your application. If you move between the two, read the same section of the Camunda 8 wiki. Do not assume VanillaBP behaves the same everywhere.

Letting an idle engine go quiet

The job executor from the section above does not wait between two cycles. It polls: every 5 seconds, growing to 60 while nothing happens, and each cycle is a database command. An application which spends most of its life waiting for a timer pays for that. On a database billed by active use, it pays for doing nothing.

One key per adapter id changes this:

vanillabp:
  adapters:
    c7:
      sleep-until-something-is-due: true

When a cycle finds nothing, it asks the engine for the one job with the earliest due date still ahead. Then it waits until exactly that moment. An engine with no job at all waits until somebody wakes it. Every commit of this application wakes it: starting a workflow, correlating a message, completing a task, sending a signal, a continuation the engine wrote for itself, and a request to Cockpit or to the REST API all shorten the wait. So a timer is not served later than without the key. It is served at its due date, and the cycles in between are gone.

Be careful about what this is. It is not an immediate wake-up for new jobs. The engine has always done that by itself for a job which is due now. And it is not a longer polling interval. That would make a timer late and would still not stop the polling.

The one index you have to add

With the key on, jobs are picked up in order of their due date. The engine does not do that by default. Waking for the earliest job but picking jobs in some other order would pick the wrong job as soon as more jobs are due than one cycle takes. This order needs a database index on the due date of ACT_RU_JOB. Only you can create it in your database:

CREATE INDEX ACT_IDX_JOB_DUEDATE ON ACT_RU_JOB(DUEDATE_);

Camunda explains the background under the job order of job acquisition. The startup log asks you for this index as well, so nobody has to find this page first. The adapter leaves the engine's jobExecutorPreferTimerJobs as it is. Preferring one kind of job over another is a different question from when to wake up.

What a wake-up can cost at worst

Say a wake-up arrives after a cycle has read the database. It is kept, and it ends that cycle's wait. So the longest a job can wait unserved because of this key is the one acquisition cycle already running. This matters because the wait here is not a fixed interval. It lasts until the next due date, or a year if the engine holds no job at all. Camunda's own loop drops a wake-up which arrives between reading its flag and going to sleep. It loses one idle interval for that. This adapter keeps the wake-up instead, because here the same loss would be the whole wait.

One case is outside that promise, and the section below covers it. A job written by another application against the same engine does not wake anything here. So it waits for the next due date this node already knew about.

The metrics reporter goes quiet as well

The engine's metrics reporter has a timer of its own. It writes its counters through a database command every 900 seconds. That would wake this engine four times an hour and lose most of the saving. With the key on, this writing stops. The counters are still kept in memory, so Cockpit still shows the same about the running engine. Only the ACT_RU_METER_LOG rows stop. If your application needs them written, say so:

vanillabp:
  adapters:
    c7:
      sleep-until-something-is-due: true
      db-metrics-reporting: true

VanillaBP keeps polling, though

A quiet engine alone does not make a quiet database. Two parts of VanillaBP itself still query the database on a schedule. The startup log names them, so you do not go looking for them in the engine:

  • the phase-two outbox queries its store every vanillabp.outbox.poll-interval, 10 seconds by default;
  • the retention cleanup of the task delivery log runs once an hour.

A developer who turns the key on and watches the database sees those two. Until they sleep as well, the database still sees VanillaBP, though no longer this engine.

A second application on the same engine

The key does not cover this case. Another application commits its job in another process, and that does not reach this node. Say a node waits for a due date it computed before that write. It learns about the new job only when it asks the engine again. This adapter does not support two applications on one engine anyway. Several nodes of ONE application are fine: the node which writes a job is awake anyway, and it serves the job itself.

Creating the engine tables yourself

If your application applies its schema with Liquibase or Flyway, it sets database-schema-update to false. Then it needs the statements. They come with the engine, not from VanillaBP, because the engine version decides what is correct:

  • Liquibase: camunda-engine-<version>.jar contains org/camunda/bpm/engine/db/liquibase/camunda-changelog.xml. It has a baseline changeset plus one changeset per engine upgrade, and a dbms property chooses the database. Include it from the classpath instead of copying it. Then the next engine version brings its own upgrade changesets along.
  • Flyway: the plain scripts are next to it under org/camunda/bpm/engine/db/create/, one per database and engine component. Apply them with a Flyway instance of their own (with its own history table), the same way VanillaBP's own tables are applied.

This does not cover two things. The engine keeps track of its own versions in ACT_GE_SCHEMA_LOG, and that stays the engine's business. And with table-prefix neither file fits as it is. Camunda's statements contain fixed table names, so you have to transform them to get a prefixed schema. See One datasource, two engines.

The tables VanillaBP itself needs are a separate topic. They are documented in the platform wiki.

The BPMN model for Camunda 7

Model with the Camunda Modeler (Camunda 7 / Platform diagrams). The Camunda 7 BPMN 2.0 reference documents the BPMN elements and attributes Camunda 7 supports. This is what VanillaBP expects of the model:

Element What names the task definition Notes
service, send, business-rule and script task the Expression ${myTaskDefinition} or the Delegate expression the expression text is the task definition of the @WorkflowTask method. By default that is the method name
a task whose method declares @TaskId the Delegate expression ${myAsyncTask} an Expression task completes when the expression returns, so it can never stay open. The application does not start, and the message names the task, the process, the module and this fix
user task the camunda:formKey a @WorkflowTask method is its notification handler. A user task of a claimed process needs one, as in VanillaBP 1, or implemented-externally: true where a task list handles it (a task something else serves)
message catch event, message boundary event, receive task the message name nothing else has to be modelled: the adapter correlates by business key
message start event the message name startWorkflowByMessage starts only the process of its own ProcessService, see A message starts its own process
business rule task calling a decision nothing it names a camunda:decisionRef instead, and the engine serves it. No @WorkflowTask method is expected, see Decision tables
an execution listener you modelled the Expression or the Delegate expression served only where allow-listeners allows it, one method per listener, see Listeners somebody modelled

A method may name the BPMN ELEMENT instead of the task definition. @WorkflowTask(id = "IW_Task") serves the element with that id, whatever the model wrote on it. Use this wiring where the task definition is not stable, above all for an expression. It is also the only wiring for a user task with a form key. There the form key IS the task definition, and a form key names the form, not the method. Both wirings are accepted when your application boots, and both are served while a workflow runs.

Apart from that, the model needs less than a Camunda application usually carries. The reason is that the workflow aggregate is the source of truth, not the process variables:

  • Expressions in gateway conditions, timer definitions and multi-instance collections (${riskAcceptable}, ${items}) are resolved against the workflow aggregate the business key identifies (getter first, then boolean getter, then field). Spring beans can still be resolved on Spring Boot. You do not have to set process variables for them.

  • Input/output mappings are only needed where a @WorkflowTask method declares a @TaskParam.

  • Correlation ids (correlateMessage(aggregate, messageName, correlationId)) are matched the way version 1 did it, by a local variable. The scope waiting for the message carries a LOCAL variable named <primary bpmnProcessId>-<messageName> with the expected id. You usually set it at the multi-instance body which creates one catch event per element. Only executions whose variable matches receive the message.
    Read the word primary twice. The name comes from the primary process of the workflow service, the one its bpmnProcess names. It never comes from the process the catch event happens to sit in. So a message caught inside a called process carries the id of the calling process:

    @WorkflowService(
        workflowAggregateClass = Order.class,
        bpmnProcess = @BpmnProcess(bpmnProcessId = "OrderProcess"),
        secondaryBpmnProcesses = @BpmnProcess(bpmnProcessId = "ShipmentProcess"))

    An event waiting in ShipmentProcess for the message ParcelScanned expects the local variable OrderProcess-ParcelScanned. If a modeller writes ShipmentProcess-ParcelScanned, nobody reads that variable, and the correlation silently matches nothing. Changing the convention would mean migrating every running workflow. So the name stays as version 1 made it, and this paragraph is the remedy.

  • Correlating without an id names only the business key. So every occurrence of that message name waiting in the workflow can receive it, and the engine decides which one does. Where more than one can wait at the same time, pass a correlation id instead of relying on the engine's choice.

  • Versions need no preparation. The engine counts them up per process id and reports the version with every delivery. A boundary of @WorkflowTask(version = ...) may also name the model's camunda:versionTag.

A message starts its own process

startWorkflowByMessage starts the process of the ProcessService you call, and no other. VanillaBP checks the name before anything is saved. To make that possible, this adapter tells VanillaBP at boot which messages start each process. A message which does not start that process fails with an IllegalArgumentException, which names the messages which do.

The adapter also keeps this rule by itself. The correlation names the newest version of the process, the same version startWorkflow starts. So even a message VanillaBP could not check starts no other process. If somebody deploys a new version of the process while your application runs, the next start by message takes that new version.

Version 1 did it differently. It handed the message to the engine, and every process with a start event for that message started, whichever ProcessService you called. If your code starts a process through the ProcessService of another process, call the ProcessService of the process the message is meant for.

Give a message start event a plain name. Camunda 7 refuses to deploy a model whose message start event has an expression as its name, such as ${orderMessage}.

The iteration a called process runs in

A @MultiInstanceElement, @MultiInstanceIndex or @MultiInstanceTotal parameter names a BPMN element which repeats (iterates). Camunda 7 keeps the item, the index and the total of an iteration in the execution below that element. So the adapter reads them from the execution your task runs on, and from every execution above it. You do not have to model anything for that or pass any variable.

Every kind of task reads them, user tasks too. Your method hears about a user task from a task listener. The task the engine hands that listener names the execution the task waits on. So a user task inside a multi-instance element learns the same as a service task in its place.

The reading does not stop at the process your task is drawn in. If a call activity started that process, the adapter goes on into the calling process. It reports every level it finds on the way, the outermost first. So a task of a called process can ask for the multi-instance element of its caller, through as many call activities as your models nest. A task which is itself multi-instance inside a called process learns both its own iteration and the one of its caller.

The adapter goes on only if the called process works on the same workflow aggregate as the calling one. The business key follows the same rule. A process with a workflow aggregate of its own runs a business case of its own. What its caller iterates over says nothing about that case, so the adapter reports nothing of the caller to it. Its own multi-instance elements work as usual. The values are not lost from the workflow either. Camunda 7 keeps them in the executions of the calling process. A model which wants them in the called process passes them like any other variable. Only the adapter's report ends at that boundary. It ends there because this is where the two business cases can be told apart.

The adapter also reads a call activity which names the process to call in an expression. The process behind it is chosen while your workflow runs, so the adapter asks about the aggregate at that time, not when the model is deployed. Your model needs nothing for that, and it does not matter which of the two ways names the called process. One thing does stay with the deployment: the answer for a call activity which names the called process directly. A workflow in an older version of a process keeps the answer that version was deployed with. That is the answer its workflows were started under.

There is one level the adapter cannot answer, and your application learns about it at boot. The item of an iteration is the variable named by camunda:elementVariable. So a multi-instance element whose model names none has no item to hand over. The usual case is an element based on cardinality. It repeats a number of times but over nothing, so there is no item. @MultiInstanceIndex and @MultiInstanceTotal work for such an element as for any other.

If a @WorkflowTask method asks for the item of such an element, the deployment stops the boot. The message names the task, the element and the attribute your model would have to carry. Version 1 passed null to the parameter and said nothing; this replaces that. You have two ways out. Give the element a collection to walk through, and name the variable that receives its item:

<bpmn:multiInstanceLoopCharacteristics camunda:collection="${items}" camunda:elementVariable="item"/>

Or drop the parameter and read @MultiInstanceIndex and @MultiInstanceTotal, which every multi-instance element answers.

Nothing else is refused. An element which counts rounds deploys as before, as long as no handler asks for its item. The same goes for a collection whose handler only wants to know how far the iteration got. An element id your model does not know is also left alone. The chain crosses call activities, so a task of a called process may ask for an element of its caller. That element is checked when the caller is deployed.

Listeners somebody modelled

An execution listener is a place where Camunda 7 lets your code run while the engine moves a token. Your code there is what makes a model no longer portable, because another BPMS has no listener at that element. VanillaBP 1 served such a listener with a @WorkflowTask method without telling you, and only on an end event and an intermediate throw event. This version serves one on any element, but only if you ask for it:

vanillabp:
  adapters:
    c7:
      allow-listeners: true
  workflow-modules:
    loan-approval:
      adapters:
        c7:
          # a workflow module may deviate
          allow-listeners: false
      workflows:
        LoanApproval:
          adapters:
            c7:
              # and so may a single workflow
              allow-listeners: true

The most specific value wins, in both directions. So the module above serves no listener, but the one workflow does. There is no level per task. That level is keyed by a task definition, and this key decides whether a listener becomes a task at all. A value written there is reported and changes nothing.

A listener is served if its implementation is an Expression or a Delegate expression. The expression text is the task definition, exactly as for a service task:

<bpmn:endEvent id="orderArchived">
  <bpmn:extensionElements>
    <camunda:executionListener event="end" delegateExpression="${archiveOrder}" />
  </bpmn:extensionElements>
</bpmn:endEvent>
@WorkflowTask(taskDefinition = "archiveOrder")
public void archiveOrder(
    final Order order) {

  order.setArchived(true);

}

A listener is served only if a method names its task definition. That is why the switch takes nothing away from you. With Spring or CDI (Contexts and Dependency Injection), a delegate expression is resolved against your application context. So ${auditTheOrder} may name one of your own beans which implements Camunda's ExecutionListener. That is an ordinary Camunda 7 model, and VanillaBP leaves it exactly as it is. Name the task definition, not the element. @WorkflowTask(id = "<element id>") names the element, and one element may carry a task and a listener at the same time.

One method serves one event of one element. The event is set in the model, not in the method. A @TaskEvent parameter receives CREATED whenever the modelled listener fires, whatever event you picked for it. It receives CANCELED when the element the listener sits on is canceled. A listener knows only these two events. If a method has to tell two of your own listeners apart, model two listeners with one expression each, and write one method per expression. A @TaskId parameter is refused when your application starts. A listener is notified and then done, so its task can never stay open. TaskException has nothing to route either, because the engine is in the middle of a transition while the listener runs. A method which throws it gets a message saying so, instead of an incident which says nothing.

You are told when the element is canceled, and you need no listener of your own for that. An interrupting boundary event or a terminating end event removes the element before the moment your listener waits for. So VanillaBP delivers CANCELED to the method serving that listener. A listener you modelled on end gets no CANCELED, because this engine fires an END execution listener on a cancellation too. Your method already hears that moment. A listener on a sequence flow gets none either, because a sequence flow is never canceled. A method WITHOUT a @TaskEvent parameter subscribes to CREATED only and hears no cancellation. So add the parameter where the cancellation matters.

Here everything your aggregate shares is written in the engine's own transaction. So a listener method may change the workflow aggregate just like a task method, whatever the listener and whatever its event. On the Camunda 8 adapter only an execution listener on end writes this way. A start execution listener and a task listener leave the process instance as it was. What such a method changed reaches that cluster at the next sync point of the workflow. Read that page too before you model a listener into a workflow meant to run on both.

Three cases stop your application from starting, each with a message naming the element and the way out:

  • a model with a served listener while the switch is off;
  • two listeners of one element under ONE expression;
  • a method with a @TaskId parameter.

If two listeners of one element have different expressions, both are served, with one method each.

Two kinds of listener are not part of any of this, and neither costs you anything. A listener implemented as a Java class or Script is run by the engine itself, and there is no method to match it to. A task listener of a user task is not read at all, because the @WorkflowTask method of a user task is already notified on creation and on cancellation. Version 1 refused a class or script listener on an end event and an intermediate throw event. This version accepts it.

Every boot tells you what the switch costs. It logs one framed warning per workflow module. The warning names each served listener, the key which switched it on and the way back: move what the listener does into a task of the model with a @WorkflowTask method behind it, or set the key to false. No key silences this warning, because what it says stays true as long as the listener is in your model.

Decision tables

Put the .dmn files of your workflow module next to its BPMN files, below <module>/processes/<adapter id>. The boot deploys them together with the processes: one Camunda deployment, one tenant, one version step for process and decision together. You do not have to deploy anything by hand, and no property has to switch it on.

A business rule task then names the decision it calls:

<bpmn:businessRuleTask id="rate"
    camunda:decisionRef="creditRating"
    camunda:decisionRefBinding="deployment"
    camunda:mapDecisionResult="singleEntry"
    camunda:resultVariable="rating"/>

decisionRefBinding="deployment" is worth setting. It ties the task to the decision of the same deployment, which is the one your module just brought. The default latest works too, and picks whatever the engine holds as newest.

The result is a process variable like any other. So reading it in Java needs no DMN (Decision Model and Notation) knowledge at all. You need an input mapping on the next task and a @TaskParam parameter:

<bpmn:serviceTask id="read" camunda:expression="${readTheRating}">
  <bpmn:extensionElements>
    <camunda:inputOutput>
      <camunda:inputParameter name="rating">${rating}</camunda:inputParameter>
    </camunda:inputOutput>
  </bpmn:extensionElements>
</bpmn:serviceTask>
@WorkflowTask(taskDefinition = "readTheRating")
public void readTheRating(
    final Order order,
    @TaskParam("rating") final String rating) {

  order.setRating(rating);

}

If the decision only steers the model, a gateway condition reads the variable and no Java code is involved.

Two things to know about the task itself. A business rule task with a decisionRef expects NO @WorkflowTask method. The engine evaluates the decision, so the wiring check leaves that task alone. A business rule task wired by an expression is an ordinary VanillaBP task and is checked like a service task.

And under name-clash-avoidance: use-prefix the decision ids of your files are renamed like the process ids. The camunda:decisionRef of the tasks calling them is renamed with them, so both sides still name the same decision. If you write a decisionRef as an expression, it gets the prefix in front of it. So ${whichDecision} reaches the engine as my-module__${whichDecision}, and your expression still picks a decision of your module. Camunda 7 reads the attribute as one expression and builds the id from both parts. A camunda:calledElement written as an expression works the same way. A task pointing at a decision your module does NOT deploy breaks in this mode. The reference is renamed here, but the decision in the engine is not. Deploy the decision with the module, or stay with the tenant isolation of by-adapter.

What reaches the engine

Everything, unless you exclude it. Since VanillaBP 2.0 this adapter shares values like every other BPMS. The values of your workflow aggregate are written as Camunda process variables, and the engine evaluates its expressions against those variables. Being embedded is no reason to do it differently. A model which reads something else works here but breaks on every remote BPMS, and @SyncWithBPMS exists to prevent exactly that. (why)

What is shared follows the platform's rules (see workflow aggregates). An aggregate without annotations shares every readable attribute. An application which wants to share less adds annotations. Readable means it has a getter: getX() with any return type, or isX() returning boolean. A field without a getter is never read. VanillaBP never reads the variables back; the aggregate stays the source of truth.

The values are written every time the adapter talks to the engine for you:

  • starting a workflow (also by message);
  • completing a @WorkflowTask method, including the path a TaskException takes through an error boundary event;
  • completing or cancelling an asynchronous task;
  • completing or cancelling a user task;
  • correlating a message;
  • aggregateChanged.

Completing a task matters most, because a gateway right after a service task decides on what that task just computed. A broadcast signal is the exception and carries nothing, because it reaches workflows of other aggregates.

ProcessService#aggregateChanged(aggregate) writes the shared values at the process instance, and aggregateChanged(aggregate, taskId) at the scope the task runs in. This is what makes conditional events usable on Camunda 7. The engine evaluates a waiting condition when a variable of its scope, or of a parent scope, changes. A called process is a process instance of its own here, with variables of its own. So aggregateChanged(aggregate) also writes into every called process which works on the same aggregate. A condition waiting in a called process sees the change just like one in the caller. If your aggregate shares nothing at all, VanillaBP writes a technical variable vanillabpAggregateChanged with the time of the push. Its only purpose is to make a change happen.

Some values have no variable type in this engine, such as a nested value or a number it cannot store as itself. What becomes of them is described in What becomes of a shared value. That page also covers the serialization-format property and the dataformat it asks you to put on the classpath.

Where the name of a workflow is kept

Camunda 7 has a business key, and this adapter keeps the name of a workflow there. The name is the id of your workflow aggregate. Your application assigns it, and the engine only holds it. How VanillaBP reads the name it finds in a key is a platform rule, written down here: Starting workflows.

So in Cockpit one search is enough. An instance found by its business key is the workflow of the aggregate with that id. The value is the same on every version of your models. It is also the same for a workflow you took over from VanillaBP 1, which has its id in the business key and in no variable at all.

The key is written wherever a workflow starts. Your own start writes the aggregate's id into it. A workflow the engine started by itself arrives without a key. Your @WorkflowStartedByBpms method names it, and the adapter writes that name into the key, in the same transaction which created the instance. This gets an INFO line, once per workflow. Otherwise the application would hear about the workflow only when its first task arrives.

A called process also arrives without a key if a call activity named it in an expression. Nothing could be written onto such a call activity when the model was deployed. So while your workflow runs, the adapter asks whether the called process works on the aggregate of its caller. If it does, the instance takes the caller's name. No @WorkflowStartedByBpms method is asked and no INFO line is written, because this is the same business case and not a start of its own.

A key somebody else chose is refused. The instance then carries a name that no workflow aggregate of yours has. The start fails with a message naming that key and both ways out. The engine handles the failure like the failure of any other listener.

Migrating from an adapter which read the aggregate live

Until VanillaBP 2.0 this adapter resolved expressions by reading the workflow aggregate directly. That is why workflows started with an older version have no variables. Those workflows keep running. Where the engine has no variable of that name, the adapter still reads the aggregate. It logs this once, naming the attribute and the way out. It also covers two cases the sharing rules do not: an attribute readable only as a field, and an isX() method returning something other than boolean.

The fallback will be removed, and no version is named for that. Ask the VanillaBP team if you need a date. Until then, you can watch your own log. The adapter names every attribute it read live. Once an hour it says how many workflows used the fallback since the application started. That number goes down by itself. A workflow stops needing the fallback as soon as it reaches the next point where the adapter writes the shared values. If the number keeps growing, new workflows run into the same gap. Then a model of yours reads something the aggregate does not share.

When the application starts, the adapter reports each expression of your models which reads a path the engine holds nothing at, with one WARN each. The WARN names the element, the expression, the segment where the path stops, what this engine will do with the resulting null, and the fix. The adapter reads the whole path. So ${order.customer.address.city} is checked segment by segment, not only by its first name. A model which reads only shared values produces no WARN, and the check never fails a deployment.

The sentence about the outcome is the part worth reading, because the same null has very different effects here:

Where the expression sits What this engine does with a null
a sequence flow condition, gateway with a default flow the workflow quietly continues along the default flow
a sequence flow condition, gateway without one an incident, because no outgoing flow can be selected
the condition of a conditional event the condition counts as false and the event waits forever, with no incident and no log line
the completion condition of a multi-instance element every instance runs and the element ends as if it had done its work
a timer, a multi-instance cardinality or collection an incident naming what was expected at that place instead

The conditional event is the reason this check reads a path at all. Camunda 7 evaluates such a condition with tryEvaluate, which answers false instead of failing. So a broken condition looks exactly like one which is not met yet, and the boot log is the only place that mentions it.

The check has limits. It reads the DECLARED types of a path and says nothing where they cannot decide. This is the case for a Map, an interface, an abstract type or a collection without an element type. It does not check method calls either. So ${order.getTotal()} and ${order.status.name()} are missing from the report, although both stop working on 2.0. Those two fail LOUDLY at runtime, with an incident naming the expression and the class it looked at. That is why they are left to the engine. And the fallback covers only what it always covered. A top-level name is still answered from the live aggregate. An expression reading past the first dot never was.

The one place where variables ARE read is @TaskParam. Such a parameter is taken from the task's input mapping, where the model hands something over on purpose.

Keeping workflow modules apart

The name-clash-avoidance mode decides how the modules are kept apart (why):

Mode Deployment
by-adapter (default) the workflow module ID becomes the Camunda tenant ID, as in version 1. tenant-id overrides the name, for the whole adapter or for one workflow module
use-prefix deployed without a tenant. Instead the adapter puts the workflow module id in front of process ids, call-activity references (camunda:calledElement), decision ids and the camunda:decisionRef of a business rule task, message names, signal names, escalation codes and error codes. Task definitions get NO prefix. They are resolved within the process, so nothing subscribes to them across the whole engine
none deployed without a tenant and without prefixes, so your identifiers have to be unique across modules

by-adapter is the default because version 1 deployed every workflow module into a tenant named after it. So an application which upgrades without touching its configuration finds its running workflows again. It costs nothing on this engine. A tenant id is an attribute of the deployment, so any name is accepted and nothing has to exist. There are two other ways: prefixed identifiers, or a separate engine per module (data-source-name, table-prefix). While none applies, the adapter logs a WARN per workflow module that names exactly these options.

If the identifiers really are unique, which is usual for a single workflow module, say so once. The warning is then gone (a DEBUG line remains):

vanillabp:
  adapters:
    camunda7:
      accept-unscoped-identifiers: true   # yes: unique across all workflow modules

This is a confirmation, not a log level setting. If you confirm wrongly, two workflow modules use the very same process definitions and tasks, and nothing warns you any more.

A tenant does not have to exist in Camunda 7. It is an attribute of the deployment and of everything below it. But your application may register tenants (IdentityService#newTenant). If the tenant VanillaBP deploys into is not among them, the adapter logs a WARN. The deployment works, but nobody is authorized for those workflows.

Two workflow modules using the same BPMN process id

This is the one name clash which loses a model, so it stops the boot instead of only warning. Whether two modules with one process id clash at all depends on where they end up in the engine. VanillaBP asks the adapter about that. The adapter works out the tenant of each of the two modules, the same way it works out the tenant it deploys into, and compares the two names.

The default is clear. Every workflow module has a tenant named after it, the two names differ, and the same process id exists once per module without any problem. The mode use-prefix is just as clear. The prefix contains the module id, so the two keys are different from the start.

One tenant-id for the whole adapter is the case to watch, because then every workflow module is deployed into that one tenant. Two modules declaring the same process id really reach one process definition key. Camunda 7 keeps one definition under it and loses the other. So the boot of the SECOND module stops, and the message names both modules, both process ids and the shared key. Give one of them a tenant of its own:

vanillabp:
  adapters:
    c7:
      tenant-id: shared-tenant
  workflow-modules:
    loan-approval:
      adapters:
        c7:
          tenant-id: loan-approval   # this module alone gets a scope of its own

Having no tenant counts as a scope here too. A workflow module under use-prefix or none reaches the engine without a tenant. So the engine does NOT keep two such modules apart, but it does keep one of them apart from a module in a tenant. You get this case by mixing modes per workflow module. The refusal then names the mode of each side and the property which set it.

What the start asks the engine about your names

A mode keeps the workflow modules of YOUR application apart. It says nothing about a name somebody else deployed into the same engine before you. From then on, the engine alone decides which side a start or a message reaches. So every start asks, once per workflow module and right after the deployment. If it finds something, it logs a WARN naming both sides and the change which frees the name. (why)

The engine can answer for two of your identifiers, the two it keeps in its repository:

Identifier What is asked What it costs
BPMN process id every process of the module in one statement, including suspended definitions and every version the engine holds one query per workflow module
DMN decision id one statement per decision, because a decision query does not accept a list of keys one query per decision of the module

A message name, a signal name, an error code and an escalation code live in your BPMN models, and the engine keeps no index of them. Reading one model per held version would make your start slower every year you run, so the adapter does not ask this. Where the models are read anyway, the question costs nothing, so it is asked there. The names your current models declare are compared between your own workflow modules. The names of a version the engine still holds come from the model the check of older versions reads anyway. Task definitions are no question on Camunda 7 at all, because they are resolved inside the process and nothing subscribes to them across the whole engine.

The Camunda deployment tells whether a finding really belongs to somebody else, but only partly. Every VanillaBP version deploys a workflow module with the module's own id as the deployment name, version 1 included. So:

  • a definition from a deployment named after your workflow module is your own history, including what version 1 left behind. You are not told about it;
  • a definition deployed under another name is reported, and the deployment source says how sure VanillaBP is. A source of camunda7:<adapter id> means another adapter id or another workflow module. This may be your own application during a BPMS migration, and the warning says that it cannot tell. Any other source was written by something else, a modeller or an application deploying its own models, and the finding is certainly foreign;
  • a second application which deploys a workflow module with the same id stays invisible. No column of the engine's deployment table names an application.

None of this fails a deployment, and there is no key to turn it on or off. A finding is only a warning, because whoever holds the name may be an application which runs correctly, and stopping your start would not help it. If the engine does not answer a question, the start goes on and says so at DEBUG level.

Choosing the datasource

The engine runs on the application's default datasource. This is what an embedded engine promises: engine state commits and rolls back together with your business data, without any configuration.

As soon as the application provides more than one datasource, VanillaBP does not guess. Not even a @Primary bean decides it, because an embedded engine writes its ACT_* tables into whatever database it gets. The boot fails with a guiding message listing the available datasources, and data-source-name names the one to use.

The application's default datasource has no name of its own, so you name it with the reserved value default:

vanillabp:
  adapters:
    c7:
      data-source-name: default   # the application's default datasource
    c7-legacy:
      type: camunda7
      data-source-name: legacy    # a second, application-provided datasource

With default the engine shares the application's datasource. This is what makes task delivery run in one transaction with your business data. Any other name is a separate datasource, and an engine there cannot join your transaction at all. Its commands commit on a transaction manager inside the adapter. For operations which move a workflow forward, the difference no longer matters. Every adapter id runs them after the commit, through the phase-two outbox. They run idempotently, with the small at-least-once window every operation through the outbox has (see Transactional behavior). What a separate datasource still changes is the INBOUND direction: task delivery no longer shares the transaction with your business data. The section below is about that. Every Camunda 7 application needs a phase-two outbox.

Deliveries are deduplicated on an own datasource, and need not be on the default one

VanillaBP remembers task deliveries, so a repeated one does not run the @WorkflowTask method again (a repeated delivery). On the default datasource Camunda 7 needs none of this. The handler runs INSIDE the engine's own transaction. So aggregate changes and engine state commit or roll back together. If the engine delivers a job again, that proves nothing was committed. There is no result to remember.

On an engine with its own datasource, the two commits are no longer one. Your aggregate is committed before the engine's job is. If the job fails afterwards, it is handed out again while your work is already saved. There the adapter names each delivery by the id of the job, and the core answers the repetition from its record. The platform does the same for every remote BPMS. Two things stay your job in that mode: a user task notification, and anything a handler does outside its transaction. Both are described in Transactional behavior.

You can turn the deduplication off per adapter id, workflow module, workflow or task with vanillabp.adapters.<id>.deduplicate-deliveries: false. This says that your handlers cope with a repetition themselves. An application without a delivery log is told so once at boot, and behaves as it did before.

Those records live in your application's database, in VANILLABP_TASK_DELIVERY next to the workflow aggregates. This matters on the day you replace that engine's schema but keep the adapter id. Job ids start again from the beginning, so a record of the old schema can answer a delivery of the new one. The fix is the same step every rebuilt backend needs, and it is described once on the platform wiki (replacing the backend of a BPMS). On the default datasource no job id is ever written down, so none of this applies there. A record IS written there, one per delivery, but it answers no repetition. It holds the adapter id and the kind of id the task id is, which a later call about that task reads. Such a record is not renewed. So it lives for vanillabp.delivery.retention, counted from the moment your handler ran. The same holds for a user task notification on either datasource.

There is no fetch-variables here

The Camunda 8 and Process-Engine-API adapters have a fetch-variables property. It exists because a delivery from those engines TRAVELS over the network. Unless the adapter names the variables it needs, every job (Camunda 8) or every task subscription (Process-Engine-API) carries the whole variable scope of the process instance across the wire. Camunda warns about this in plain words. VanillaBP names the variables: the one holding the workflow aggregate's id, and whatever your @TaskParam parameters ask for. The property is the way out for the rare case where that list is not enough.

Camunda 7 has nobody to name them to. The engine runs in your JVM (Java Virtual Machine), on your database. It hands the adapter a DelegateExecution, and a @TaskParam is answered from it with getVariableLocal(name) while your method runs. The engine loads a variable only when it is used. So nothing is copied, nothing crosses a wire, and a shorter list would save nothing. That is why the property does not exist here at all, rather than having some default.

Several engines side by side

You may configure the same type under several ids, for example an old and a new Camunda 7 engine during a migration. Each id gets its OWN embedded engine, named vanillabp-camunda7-<id>. Two embedded engines must never share one database and table prefix, because they would then be the same engine state. So the ids have to be distinguishable:

  • a separate datasource per id (data-source-name), or
  • one datasource and a separate table-prefix per id, which needs a prepared schema (One datasource, two engines).

If the ids cannot be told apart, the boot fails with a guiding message naming both fixes.

The same rule holds across adapter types. The forks of Camunda 7, CIB seven and Operaton, use the same tables (ACT_*). So a camunda7 id and an id of a fork adapter must not share one datasource and table prefix either. Otherwise each job executor runs the jobs of the other engine, and nothing fails until then. This adapter tells VanillaBP the datasource and the prefix of each id, and VanillaBP compares them across all types. Two ids on the same tables stop the boot, with a message which names both ids with their types, the datasource, the prefix and the same two fixes. Ids on different tables are the normal setup for a migration to a fork. (why)

data-source-name: default names the application's datasource, the same as leaving the key out. Both count as the same datasource in these checks.

Setting up datasources is the application's job, on purpose: VanillaBP never builds its own connection pool. The key data-source-name is the same on both platforms. Only what it refers to differs.

Spring Boot

data-source-name is the name of a DataSource bean. Declare it with defaultCandidate = false. Then it is not injected by type, and Spring Boot's auto-configuration of the default datasource stays active. This is the standard pattern for additional application datasources:

@Bean(defaultCandidate = false)
public DataSource legacy() {
    return DataSourceBuilder.create()...build();
}

Quarkus

data-source-name is the name of a named Agroal datasource declared under quarkus.datasource.<name>.*:

quarkus:
  datasource:            # the application's default datasource (aggregates + engine)
    db-kind: postgresql
    ...
    legacy:              # a second, named datasource for the OLD engine
      db-kind: postgresql
      ...
vanillabp:
  adapters:
    c7:
      type: camunda7     # runs on the default datasource (tasks delivered in your transaction)
    c7-legacy:
      type: camunda7
      data-source-name: legacy   # runs on its own schema

An unknown data-source-name fails the boot with a guiding message. So do two adapter ids sharing one datasource.

One datasource, two engines

table-prefix is the other way to tell two ids apart. It sets Camunda's databaseTablePrefix, and the engine then reads and writes NEW_ACT_* instead of ACT_* in the very same database. Everything the engine does at runtime respects the prefix. Creating the tables does not, and Camunda says so itself. With a prefix configured, database-schema-update creates the tables under their DEFAULT names, because the statements shipped with the engine contain those names in every SQL dialect. (why)

So an id with a prefix runs on a schema you created:

vanillabp:
  adapters:
    c7:
      type: camunda7             # the engine of today, on ACT_*
    c7-new:
      type: camunda7
      table-prefix: NEW_         # a second engine in the same database, on NEW_ACT_*
      database-schema-update: false

The adapter checks this at boot, before the engine exists. A prefix together with a database-schema-update which creates tables stops the boot. So does a prefix whose tables are missing. The message names the prefix, the datasource, the tables it looked for and the two ways forward. Nothing is written to the shared database in the meantime.

To create the tables, take Camunda's own statements (Creating the engine tables yourself) and rename the objects in them. Be careful which ACT_ names are objects. The tables are ACT_GE_*, ACT_HI_*, ACT_RE_*, ACT_RU_* and ACT_ID_*. The indexes and constraints are ACT_IDX_*, ACT_FK_* and ACT_UNIQ_*. But ACT_ID_, ACT_INST_ID_, ACT_INST_STATE_, ACT_NAME_ and ACT_TYPE_ are COLUMNS and keep their names. Renaming a column breaks the first query. Leaving an index name as it is makes it clash with the unprefixed engine next to it. Both mistakes show up only once something runs. That is why this is the job of whoever owns the schema. It is also why an id with a datasource of its own is the simpler setup wherever possible, because there the engine does all of it.

The versions this engine still holds

The startup check of older versions needs a few answers, and Camunda 7 can give all of them with the engine at hand and no extra configuration: which versions of a process it holds, the model of each of them (through the repository service), and how many workflows still run on each (through the runtime service). So when you fade a version out with vanillabp.adapters.<id>.outfaded-versions, it is always known here what that leaves behind. (why)

Reading those models also lets the adapter warn you about an older version whose parallel branches write one workflow aggregate. Say your newest model dropped a parallel gateway. The gateway still forks the workflows started before, and those workflows run longest of all. You get one warning per BPMN process, and it names the version the elements are in.

The same models tell which message names, signal names, error codes and escalation codes a held version declares. A name a workflow module deployed years ago is compared with what your other modules deploy today. That is the clash described under what the start asks the engine about your names. Nothing extra is fetched for it, because the model of that version is read anyway.

One effect of the embedded engine is worth knowing. If your models did not change, the engine deploys nothing, but it still reports the version it runs on. So the check runs on every start, not only on a start which changed a model.

Which methods a workflow of an older version reaches

Camunda 7 runs a workflow on the model it was started with, even after you deployed a newer one. Say version 1 of a process has the expression ${checkTheOldWay} at a task, and version 2 has ${checkTheNewWay} at the same task. A workflow started on version 1 still asks for checkTheOldWay, and it reaches the @WorkflowTask method of that name. It never reaches the method version 2 names at the same element. This holds for a task wired by camunda:expression or camunda:delegateExpression, for a user task's form key and for a listener you modelled.

To keep serving such a workflow, keep its method and say which version it is for:

@WorkflowTask(taskDefinition = "checkTheOldWay", version = "1")
public void checkTheOldWay(final Order order) {
    ...
}

Without the version attribute the start refuses the method, because it matches no task of the model you deploy now.

If you remove the method, the start tells you which task definitions of version 1 nothing serves any more, and how many workflows still run on that version. A workflow of version 1 which reaches that task ends in an incident. The incident names the task definition, the element and the version. A listener without its method is evaluated by the engine itself, and its incident names the expression the engine could not resolve. Add the method again, or fade the version out with outfaded-versions once its workflows are done. (why)

VanillaBP 1 looked up a task by its element alone. If two versions had different expressions at one element, it was a matter of chance, and a workflow could run the method of the other version. Version 2.0 decides by the version a workflow runs on.

A process id you declare without a model

When you rename a BPMN process, its old id stays in the engine. Every version ever deployed under it stays too, together with the workflows still running on them. With @WorkflowService(secondaryBpmnProcesses = @BpmnProcess(bpmnProcessId = "<old id>")) your application says that it answers for both names. On Camunda 7 that declaration is enough. When a workflow module starts processing, the adapter reads the model of every version the engine still holds under the declared id and wires its tasks. So the workflows on it reach the @WorkflowTask methods of your current application. The log of every start says which id was wired and how many versions it found.

Reading the models is what makes this exact and not a guess. A task wired by camunda:expression completes when your handler returns. One wired by camunda:delegateExpression can stay open for a @TaskId method. Only the model of that version says which of the two a task is. (why)

The engine also keeps STARTING workflows under the old id by itself. The timer of its latest version keeps firing, and its signal subscription keeps matching a broadcast. Those starts are still full VanillaBP workflows. @WorkflowStartedByBpms builds the aggregate (a signal start gets the plain signal name, read from the engine's own copy of the model). The task after the start event runs, and the end reaches @WorkflowEnded. If you do not want these starts after the rename, remove the timer start or signal start from the model and deploy it once under the old id. The recipe below uses the same step to finish the rename. (why)

Because the engine keeps starting them, VanillaBP also checks the @WorkflowStartedByBpms methods you keep for the declared id. The adapter reads the start events of every version the engine still holds under it. You are told if a method names a start event that none of those versions has. Nothing is deployed under that id, so nothing else could tell you. And the method would never run while the old timer fires every night. This is only a warning and never stops the start, because nobody can change those models any more. A @WorkflowEnded method you kept for the old id is checked in the same place. So if an adapter cannot deliver the end of those workflows, you learn about it when the id is wired.

If such a version contains something VanillaBP cannot read, you get a warning, but your application still starts. An old version wired by camunda:topic, or by an expression VanillaBP does not understand, is named together with what a workflow on it will run into. The other versions are wired anyway. You still get the refusal you know from deploying such a model, because that one checks what you can still change.

Two things stay your job while the declaration exists. First, the check above reports a version of the old id whose tasks your application no longer serves, with the number of workflows it affects. A method you keep for such a version needs a version range which covers it. Second, the engine must still hold those definitions. It does so as long as workflows run on them, but an operator who deletes them deletes the models too. You do not have to configure anything for this, and it costs no query beyond the one the check runs anyway. The recipe for the whole rename, including how you learn that the last workflow of the old id has ended, is Renaming a BPMN process.

A suspended definition is still checked

Deleting a process definition really removes it here, so the check stops reporting it by itself. Suspending it does not. A suspended definition comes back the moment somebody resumes it. Its workflows never went away, and a @WorkflowTask method missing for one of their tasks is still missing afterwards. So the check counts a suspended version like every other one, and no configuration key changes that. (why)

The emergency exit vanillabp.ignore-suspended-process-definitions

There is one situation the rule above does not fit. Your application has to be up now, and nobody can clean up the old version in its way at this minute. Start it with

-Dvanillabp.ignore-suspended-process-definitions=true

or, with the same effect, with the environment variable

VANILLABP_IGNORE_SUSPENDED_PROCESS_DEFINITIONS=true

Then every suspended process definition drops out of the check. If both are set, the system property wins. Only the value true counts, in upper or lower case. Any other value is reported and changes nothing.

Why two ways? A Quarkus native image does read -D arguments on its command line. But it reads neither JAVA_OPTS nor JAVA_TOOL_OPTIONS, which is where a container usually puts them. Without the variable, the switch would do nothing at all in a native image, and say nothing about it either. Silence is the worst thing an emergency exit can do. In Kubernetes the variable also saves you a change to the start command.

It is not a configuration property, and that is on purpose. A key in your application.yaml gets committed, and then it stays set forever without anybody noticing it again. This switch is meant to be the decision of one start, typed where everybody can see it. The adapter reads it from neither Spring's environment nor Quarkus' config, and it is not part of the configuration documented above.

Every start with the switch set writes two warnings. One says that the exit was taken and what follows from that. The other names the versions it hid, so the list is still there for whoever cleans up later. Neither is remembered anywhere, so both come back until the switch is gone. And the risk you buy time with is real. If somebody resumes such a definition, its workflows run into a task nobody serves, and you find out from an incident on a live workflow. Remove the switch once your application is up.

The name has no adapter part and no adapter id in it, because it is meant to be found in a hurry, not configured. An application with two Camunda 7 adapter ids cannot take the exit for just one of them.

⚠️ **GitHub.com Fallback** ⚠️