
Camunda
CertifiedInteract with a Camunda 8 cluster from Kestra flows.
Camunda 8 is a process orchestrator built around BPMN. This plugin talks to a Camunda 8 cluster
with the official camunda-client-java client, so Kestra flows can drive Camunda process
instances and Camunda service tasks can be implemented as Kestra flows.
It covers deploying BPMN, DMN and form resources, starting and cancelling process instances,
publishing correlation messages, holding a job worker open as a realtime trigger, and completing
the activated job from the flow.
Authentication supports no credentials (development clusters), Basic auth, OAuth2 against a
self-managed identity provider, and Camunda SaaS client credentials.
Camunda
Interact with a Camunda 8 cluster from Kestra flows.
Camunda 8 is a process orchestrator built around BPMN. This plugin talks to a Camunda 8 cluster
with the official camunda-client-java client, so Kestra flows can drive Camunda process
instances and Camunda service tasks can be implemented as Kestra flows.
It covers deploying BPMN, DMN and form resources, starting and cancelling process instances, publishing correlation messages, holding a job worker open as a realtime trigger, and completing the activated job from the flow.
Authentication supports no credentials (development clusters), Basic auth, OAuth2 against a self-managed identity provider, and Camunda SaaS client credentials.
tasks
Talk to a Camunda 8 cluster from Kestra flows: deploy BPMN, DMN and form resources, start and cancel process instances, publish correlation messages, and implement a BPMN service task as a Kestra flow.
Built on the official io.camunda: camunda-client-java client, so it works against a self-managed
orchestration cluster and against Camunda SaaS.
Connecting
Every task and the trigger take the same connection properties.
restAddress: REST API base URL, for examplehttp://localhost: 8080. Commands use it unless onlygrpcAddressis set. One ofrestAddress,grpcAddressorclusterIdis required.grpcAddress: gRPC gateway address, for examplehttp://localhost: 26500. Setting it withoutrestAddresssends every command over gRPC. Required for the trigger'sstreamEnabledmode, which has no REST equivalent.transport:RESTorGRPC. Defaults to gRPC on SaaS, and on a self-managed cluster to the API implied by whichever address is set, falling back to REST when both or neither are.tenantId: Camunda's own tenant, unrelated to the Kestra tenant the flow runs in. Always applied to commands, defaulting to<default>. On the trigger it additionally selects which tenants' jobs the worker activates, which is a separate SDK setting.
Four authentication modes, picked from what is set:
| Mode | Properties |
|---|---|
| None | nothing, works only on a cluster with API protection disabled |
| Basic | username, password |
| OAuth2 self-managed | clientId, clientSecret, authorizationServerUrl, audience |
| Camunda SaaS | clusterId, clientId, clientSecret, optional region |
audience is required for the self-managed OAuth2 mode, commonly zeebe-api. The client validates it
and there is no fallback, because this plugin disables environment overrides (below). Camunda SaaS
derives its own audience, so leave it unset there.
Camunda's client normally reads CAMUNDA_* and ZEEBE_* environment variables. This plugin turns
that off, so a flow always connects with what it declares and never with what the worker happens to
have in its environment.
Camunda SaaS uses gRPC by default here, which is a deliberate divergence from the client's own REST
preference. A default free-tier cluster answers Failed with code 404: 'Not Found' on the REST base
the client derives, https://<region>.zeebe.camunda.io: 443/<clusterId>, while gRPC succeeds with the
same credentials in the same execution. gRPC is served by every supported cluster, so defaulting to
it works everywhere REST does and also where REST does not.
To use REST on SaaS, take the Orchestration Cluster REST Address from the Camunda Console
(cluster, then Connection information) and set it as restAddress alongside clusterId. This is what
Camunda's own client documentation instructs, because the address the cloud builder derives is the
pre-8.8 Zeebe gateway routing and does not match a unified Orchestration Cluster. The derivation is
identical in 8.9 and 8.10, so it is not something a client upgrade fixes.
clusterId: "{{ secret('CAMUNDA_CLUSTER_ID') }}"
region: "{{ secret('CAMUNDA_REGION') }}"
clientId: "{{ secret('CAMUNDA_CLIENT_ID') }}"
clientSecret: "{{ secret('CAMUNDA_CLIENT_SECRET') }}"
restAddress: "{{ secret('CAMUNDA_REST_ADDRESS') }}"
Setting restAddress is enough, REST is inferred from it. Without it a SaaS client can only use
gRPC, which is why that is the default there.
Setting an address alongside clusterId builds the client directly rather than through the cloud
builder, whose build() overwrites any address it is given, and keeps the SaaS OAuth endpoint and
audience.
Deploying resources
Deploy sends one command holding every resource, so either all of them are deployed or none is.
Resource names carry the type: .bpmn or .xml for a process, .dmn for a decision, .form for a
form. A value is either the content itself or a kestra:// internal storage URI produced by an
earlier task.
id: deploy_camunda_resources
namespace: company.team
tasks:
- id: deploy
type: io.kestra.plugin.camunda.Deploy
restAddress: http://localhost: 8080
resources:
order-fulfillment.bpmn: "{{ read('order-fulfillment.bpmn') }}"
discount.dmn: "{{ read('discount.dmn') }}"
Driving process instances
CreateProcessInstance starts a process by processId (the BPMN process ID) or by
processDefinitionKey. Set awaitCompletion to block until the instance ends and read its variables
back, and raise requestTimeout above the expected process duration when you do, the client default
is 10 seconds.
id: run_camunda_process
namespace: company.team
tasks:
- id: run_process
type: io.kestra.plugin.camunda.CreateProcessInstance
restAddress: http://localhost: 8080
processId: order-fulfillment
variables:
orderId: ORD-123
awaitCompletion: true
requestTimeout: PT2M
CancelProcessInstance terminates a running instance by key.
Implementing a service task as a flow
Trigger holds a Camunda job worker open and starts one execution per activated job. The trigger does
not decide the outcome: the flow reports it with CompleteJob on success and FailJob on error, both
keyed on {{ trigger.jobKey }}.
Always report an outcome. Camunda re-offers a job whose lock expired without decrementing its retries,
so a flow that reports neither is activated again every timeout, fails again, and repeats for as long
as the process instance lives. Retries never reach zero, so no incident is raised and nothing surfaces
in Operate. FailJob with the default retries: 0 raises the incident immediately.
Delivery is at-least-once. A lock expiry, a worker restart mid-flow, or a flow slower than timeout
each produce a second execution for the same job. Keep timeout above the expected flow duration, and
make the work idempotent or guard it on the job key if a repeat would be harmful.
maxJobsActive bounds how many jobs the worker activates, not how many executions run at once: the
trigger releases each job as soon as its execution is created. Use the flow's concurrency block to
limit parallelism, and remember that time queued behind that limit counts against the job lock.
id: handle_camunda_job
namespace: company.team
triggers:
- id: on_camunda_job
type: io.kestra.plugin.camunda.Trigger
restAddress: http://localhost: 8080
jobType: send-notification
timeout: PT5M
tasks:
- id: send_notification
type: io.kestra.plugin.core.log.Log
message: "Notifying about order {{ trigger.variables.orderId }}"
- id: complete_job
type: io.kestra.plugin.camunda.CompleteJob
restAddress: http://localhost: 8080
jobKey: "{{ trigger.jobKey }}"
variables:
notified: true
errors:
- id: fail_job
type: io.kestra.plugin.camunda.FailJob
restAddress: http://localhost: 8080
jobKey: "{{ trigger.jobKey }}"
errorMessage: "Kestra execution {{ execution.id }} failed"
Jobs are activated over the same transport as the tasks, which is the REST API when restAddress is
set. Set grpcAddress and streamEnabled: true for push-based streaming instead of long polling.
Messages
PublishMessage publishes a correlation message. Leave correlationKey unset for a message that
starts a process instance through a message start event. timeToLive controls how long Camunda
buffers the message when nothing is waiting for it yet, and messageId deduplicates it over that
window.
Local development
docker-compose.yml in this repository starts a single-node Camunda 8 cluster without secondary
storage, which is enough for every task here. Operate and Tasklist are not part of it.
Camunda is published on its own default ports, http://localhost: 8080 for REST and
localhost: 26500 for gRPC, so the examples above work as written. Kestra's own dev server in that
same file is on http://localhost: 8090 to keep 8080 free.