Java SDK
Java 21+ SDK. Lives at workers/java/. Pure JDK + Jackson — no Spring, no servlet container.
Install
<!-- pom.xml -->
<dependency>
<groupId>io.conduit</groupId>
<artifactId>conduit-worker</artifactId>
<version>0.1.0</version>
</dependency>
(Until v0.1 ships to Maven Central, depend on the source tree directly with <systemPath> or vendor the jar.)
Required runtime: Java 21 for the sealed-interface switch on HandlerResult.
Quick start with @TaskHandler
package com.example.workers;
import io.conduit.worker.*;
@TaskHandler(topic = "http.call")
public class HttpCallHandler implements Handler {
@Override
public HandlerResult handle(ExternalTask task) {
String url = (String) task.variableMap().get("url");
// ... do work ...
return HandlerResult.complete(Variable.string("status", "ok"));
}
public static void main(String[] args) throws Exception {
Client client = new Client(new Client.Config("http://localhost:8080"));
Runner runner = new Runner(client, new Runner.Config("http-worker-1"));
runner.discover(new HttpCallHandler());
runner.run();
}
}
runner.discover(Object...) reads each instance’s @TaskHandler annotation and registers it under the annotation’s topic. There is no classpath scanning — pass the instances explicitly so dependency wiring stays your responsibility.
Direct registration
For handlers that don’t carry an annotation (functional, lambda, configured per-instance), use register:
runner.register("policy.check", task ->
HandlerResult.bpmnError("POLICY_VIOLATION", "not allowed"));
Handler is a @FunctionalInterface, so a method reference or lambda is fine.
RunnerConfig
Runner.Config cfg = new Runner.Config("http-worker-1");
cfg.maxJobs = 10; // 1..100
cfg.lockDurationSecs = 30;
cfg.pollInterval = Duration.ofSeconds(1); // back-off when idle
The runner’s loop is single-threaded — for parallelism, run one Runner per worker process and scale horizontally, or fan tasks out from inside handle() onto your own executor.
Reporting outcomes
return HandlerResult.complete( // /complete
Variable.string("status", "ok"),
Variable.longVar("amount", 1500L));
return HandlerResult.bpmnError("PAYMENT_DECLINED", // /bpmn-error
"card declined");
throw new RuntimeException("upstream 502"); // /failure
The runner catches any thrown exception, calls /failure with getMessage(), and lets the engine decrement retries.
Variables
Variable.string("name", "alice");
Variable.longVar("count", 42L);
Variable.doubleVar("ratio", 0.75);
Variable.bool("approved", true);
Variable.json("payload", mapper.createObjectNode().put("x", 1));
Variable.nullVar("tombstone");
Read variables off the incoming task:
String orderId = (String) task.variable("order_id"); // Object → cast
Map<String, Object> all = task.variableMap();
Building and running
cd workers/java
mvn -q test
mvn -q package
java -cp target/conduit-worker-0.1.0.jar com.example.workers.HttpCallHandler
mvn -q test runs the embedded JDK HttpServer integration tests — no real engine needed for SDK CI.
Sealed HandlerResult
HandlerResult is a sealed interface with two permitted records, so a switch over it is exhaustive:
switch (result) {
case HandlerResult.Complete c -> client.complete(taskId, workerId, c.variables());
case HandlerResult.BpmnError be -> client.bpmnError(taskId, workerId, be.code(), be.message(), be.variables());
}
You’ll only see this if you bypass the runner and drive Client yourself.
Idempotency
Same contract as every other SDK — see Idempotency.