Camunda 8 Service Task with Java

 In Camunda 8, a Service Task is executed by a Job Worker (external worker). In Spring Boot, the easiest way to implement this is with @JobWorker, where the job type (a.k.a. task type) in the BPMN matches the worker’s type.

This blog shows a complete working example: BPMN service task → Java worker → update variables → handle failures.


1) BPMN: Configure the Service Task (Job Type)

In Camunda Modeler:

  1. Add a Service Task

  2. Set Implementation to Job Worker (or “External” depending on Modeler version)

  3. Set Type to: payment-worker (example)

  4. (Optional) Set Retries: 3

Important: The Type must match the Java worker annotation:

@JobWorker(type = "payment-worker")

2) Spring Boot Dependencies (Maven)

Use Camunda 8 Spring Boot starter (Zeebe):

<dependency> <groupId>io.camunda</groupId> <artifactId>spring-boot-starter-camunda</artifactId> </dependency>

If you’re using an older setup, you may see io.camunda.spring:zeebe-spring-boot-starter. Use whichever matches your project, but the concept remains the same.


3) Configuration (application.yaml)

If you run Camunda 8 locally (Self-Managed)

You’ll typically configure the Zeebe gateway connection:

camunda: client: mode: self-managed zeebe: gateway-address: 127.0.0.1:26500 auth: enabled: false

If you use Camunda 8 SaaS

You configure clusterId / clientId / clientSecret etc. (different block). Share your mode if you want the exact config.


4) Java Worker Example (@JobWorker) — Read + Update Variables

Here’s a clean worker that:

  • reads process variables (orderId, amount)

  • does some business logic

  • returns variables (txnId, paymentStatus)

package com.realtech.workflow.workers; import io.camunda.zeebe.client.api.response.ActivatedJob; import io.camunda.zeebe.spring.client.annotation.JobWorker; import org.springframework.stereotype.Component; import java.util.HashMap; import java.util.Map; @Component public class PaymentWorker { @JobWorker(type = "payment-worker", autoComplete = true) public Map<String, Object> handle(final ActivatedJob job) { Map<String, Object> vars = job.getVariablesAsMap(); String orderId = (String) vars.get("orderId"); Double amount = vars.get("amount") == null ? null : ((Number) vars.get("amount")).doubleValue(); // --- Business logic (example) --- // Call payment gateway, validate, etc. String txnId = "TXN-" + System.currentTimeMillis(); Map<String, Object> out = new HashMap<>(); out.put("txnId", txnId); out.put("paymentStatus", "SUCCESS"); out.put("processedOrderId", orderId); out.put("chargedAmount", amount); return out; // variables updated in the workflow } }

Why autoComplete=true is nice
It automatically completes the job after the method returns successfully.


5) Failure Handling (Retries + Incidents)

In real projects, jobs fail. You should:

  • throw an exception to trigger retries

  • or use a BPMN Error pattern if you want a modeled error path

Option A: Throw Exception → Zeebe retries

@JobWorker(type = "payment-worker", autoComplete = true) public Map<String, Object> handle(ActivatedJob job) { Map<String, Object> vars = job.getVariablesAsMap(); if (vars.get("amount") == null) { throw new RuntimeException("amount is missing"); } // ... }

If retries become 0, Camunda will create an Incident visible in Operate.

Option B: Manual control (autoComplete=false)

Useful when you want full control over complete/fail:

@JobWorker(type = "payment-worker", autoComplete = false) public void handle(final ActivatedJob job) { try { // do work... Map<String, Object> out = Map.of("paymentStatus", "SUCCESS"); // complete job with variables job.getClient().newCompleteCommand(job.getKey()) .variables(out) .send() .join(); } catch (Exception ex) { job.getClient().newFailCommand(job.getKey()) .retries(job.getRetries() - 1) .errorMessage(ex.getMessage()) .send() .join(); } }

6) Variable Mapping Tip (Avoid “null” surprises)

If your DMN/service tasks depend on variables, prefer:

  • consistent variable names (orderId, amount)

  • JSON-friendly structures (Camunda 8 is JSON-first)

  • input/output mapping in BPMN if needed

Example: map amount into a nested object:

  • payment.amount = amount


7) How to Test Quickly (Local)

Minimum steps:

  1. Start Camunda 8 (Docker Compose)

  2. Deploy BPMN from Modeler

  3. Start a process instance (with variables)

  4. Run your Spring Boot app

  5. Watch the service task complete in Operate

Sample start variables:

{ "orderId": "ORD-1001", "amount": 1499 }

8) Common Mistakes (That Cause “Worker Not Picking Job”)

  • Job type mismatch: BPMN type ≠ @JobWorker(type="...")

  • Worker not running / not connected to gateway

  • Wrong gateway address/port

  • Auth enabled but not configured (SaaS/self-managed mismatch)

  • Variables are missing or wrong type (e.g., amount is string not number)


Conclusion

In Camunda 8, a Service Task is executed by a Job Worker. With Spring Boot, @JobWorker is the simplest way to:

  • subscribe to a job type

  • implement business logic

  • update process variables

  • handle failures and retries cleanly


💼 Professional Support Available

If you are facing issues in real projects related to enterprise backend development or workflow automation, I provide paid consulting, production debugging, project support, and focused trainings.

Technologies covered include Java, Spring Boot, PL/SQL, CMS, Azure, and workflow automation (jBPM, Camunda BPM, RHPAM).

Comments

Popular posts from this blog

Scopes of Signal in jBPM

OOPs Concepts in Java | English | Object Oriented Programming Explained

jBPM Installation Guide: Step by Step Setup