Skip to content
GitHub

Migrating Camunda 7 External Tasks

Camunda 7 external tasks let workers pull work over REST: a worker calls fetchAndLock for a topic, does the job, then calls complete, failure or bpmnError. The Camunda external-task plugin from the fleans-plugins repository serves the same REST API on top of Fleans. You can move your processes to Fleans and keep your workers. In most cases you only change their base URL.

The engine has no Camunda-specific code. The plugin is an ordinary custom-task worker that connects to the API over gRPC. It subscribes to camunda:*, stores waiting tasks in its own table, defers them, and reports workers’ results back through the REST API.

Plugin 0.11.0 requires Fleans 0.11.0 or later.

Upgrading from 0.9.x: the engine now evaluates the input/output mappings, and the plugin’s camunda_external_tasks table changed shape. Drain the waiting tasks, drop the table (the plugin recreates it at startup), then upgrade the engine and the plugin together.

Replace Camunda’s camunda:type="external" camunda:topic="…" attribute pair with a Fleans task type that starts with camunda::

<!-- Camunda 7 -->
<bpmn:serviceTask id="charge" camunda:type="external" camunda:topic="charge-card" />
<!-- Fleans -->
<bpmn:serviceTask id="charge" type="camunda:charge-card" />

Workers keep subscribing to the topic (charge-card). Camunda’s <camunda:inputOutput> is not read. Use Fleans input/output mappings instead; the plugin README has a conversion table.

The task’s businessKey is the process instance’s business id, set when you start the instance ("BusinessId": "order-123" on POST /Execution/start or POST /Execution/message). You don’t need a mapping for it.

Other Camunda task metadata comes from reserved input targets, evaluated when the task is queued (except extension properties, evaluated at fetch time) and never sent as variables:

<zeebe:ioMapping>
<zeebe:input source="=order.priority" target="taskPriority" /> <!-- camunda:taskPriority -->
<zeebe:input source="acme" target="tenantId" />
<zeebe:input source="eu-west" target="extensionProperty_region" /> <!-- camunda:properties -->
</zeebe:ioMapping>

A businessKey input replaces the business id, even when it evaluates to null. Use it where the instance has no business id, such as a call activity’s child instance: <zeebe:input source="=order.id" target="businessKey" />.

The plugin repository ships one ready-to-run worker image with every plugin in it, ghcr.io/nightbaker/fleans-plugins. It needs the API’s URL and a database for its table — the single-PostgreSQL and scalable installs show it in their compose files. With the Docker Compose bundle:

Start the engine first. The bundle creates the fleans_default network that the plugin joins, and the network only gets that name when the compose project is called fleans:

Terminal window
cd <unzipped-engine-compose-bundle>
echo "COMPOSE_PROJECT_NAME=fleans" >> .env # names the network fleans_default
docker compose up -d # engine first: creates the network

Then add a docker-compose.override.yaml next to the bundle’s docker-compose.yaml:

services:
fleans-plugins:
image: ghcr.io/nightbaker/fleans-plugins:0.11.0
networks: [fleans_default]
ports: ["8090:8080"]
environment:
Fleans__ExternalTaskWorker__EngineUrl: "http://fleans-core:8080"
ConnectionStrings__camunda-external-tasks: "Host=postgres;Database=camunda_external_tasks;Username=postgres;Password=${POSTGRES_PASSWORD}"
depends_on: [fleans-core, postgres]
networks:
fleans_default:
external: true
name: fleans_default

Then run docker compose up -d again from the bundle directory. Compose merges the override and starts the plugin on the existing network. If you skip the engine-first step or the project name, it fails with network fleans_default declared as external, but could not be found.

The database and the camunda_external_tasks table are created at startup.

To run the plugin inside a worker you already have, add the Fleans.Plugins.CamundaExternalTask NuGet package and register it with builder.AddCamundaExternalTaskPlugin() — one line that reads its settings and maps the API. See the plugin guide for details and every configuration setting.

ExternalTaskClient.create().baseUrl("http://camunda-external-task:8080/engine-rest").build();
new Client({ baseUrl: "http://camunda-external-task:8080/engine-rest" });

Every endpoint the official Java and JS clients call is supported: fetchAndLock (including long polling, usePriority, sorting, and the businessKey / tenant filters), complete, failure, bpmnError, lock, extendLock, unlock, and the file-variable download. The operator endpoints are there too: task queries and counts, topic-names, PUT /retries and PUT /priority. Errors use Camunda’s body and status codes. Set CamundaExternalTasks__BasicAuth__Username and __Password on the plugin to require HTTP basic authentication (the clients’ BasicAuthProvider / BasicAuthInterceptor).

  • Incidents are opt-in. By default, failure with retries = 0 fails the activity with error code 500, and an error boundary event can catch it. With CamundaExternalTasks__Incidents=true the task stays as an incident, as in Camunda, until PUT /external-task/{id}/retries resolves it. Incidents are not shown in the Fleans UI.
  • bpmnError fails the activity with the worker’s errorCode, so an error boundary event with that code catches it. Its variables are visible to the boundary or error event sub-process that catches it, but not to an error boundary on a parent workflow’s call activity.
  • Variables: primitives, Date (kept as a string), Json, Bytes, File, and Object with application/json are supported. Binary values live in the workflow state, so keep them small. Java-serialized objects are rejected.
  • Missing fields: processDefinitionVersionTag is always null, because Fleans has no equivalent.
  • Scope: the /external-task resource plus the file-variable download. Start processes and correlate messages through the Fleans REST API.

The plugin guide lists every difference.