Worker Examples
End-to-end pairs: a tiny BPMN with <conduit:taskTopic> alongside the worker that handles it. Each example shows the same task — fetching a URL and reporting the status code — written in each language.
The BPMN (shared by every example)
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions
xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL"
xmlns:conduit="http://conduit.io/ext"
targetNamespace="http://example.com/workers-demo">
<bpmn:process id="health_check" isExecutable="true">
<bpmn:startEvent id="start" />
<bpmn:sequenceFlow id="f1" sourceRef="start" targetRef="ping" />
<bpmn:serviceTask id="ping" name="Ping URL">
<bpmn:extensionElements>
<conduit:taskTopic>http.call</conduit:taskTopic>
</bpmn:extensionElements>
</bpmn:serviceTask>
<bpmn:sequenceFlow id="f2" sourceRef="ping" targetRef="end" />
<bpmn:endEvent id="end" />
</bpmn:process>
</bpmn:definitions>
Deploy it once, start an instance with {"url": "https://example.com"}, and any of the workers below will pick the task up.
# 1. Deploy
curl -X POST http://localhost:8080/api/v1/deployments \
-F "files=@health-check.bpmn"
# 2. Start an instance
curl -X POST http://localhost:8080/api/v1/process-instances \
-H 'Content-Type: application/json' \
-d '{
"process_key": "health_check",
"variables": {"url": "https://example.com"}
}'
Rust
use std::sync::Arc;
use conduit_worker::{
handler, Client, ClientConfig, ExternalTask, HandlerError, HandlerResult,
Runner, RunnerConfig, Variable,
};
#[handler(topic = "http.call")]
async fn http_call(task: &ExternalTask) -> Result<HandlerResult, HandlerError> {
let url = task
.variable("url")
.and_then(|v| v.as_str())
.ok_or("missing variable: url")?;
let resp = reqwest::get(url).await.map_err(|e| e.to_string())?;
Ok(HandlerResult::complete(vec![
Variable::long("status", resp.status().as_u16() as i64),
]))
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let client = Client::new(ClientConfig::new("http://localhost:8080"))?;
Runner::new(client, Arc::new(HttpCallHandler), RunnerConfig::new("rust-1"))
.run().await;
Ok(())
}
Java
package com.example;
import io.conduit.worker.*;
import java.net.URI;
import java.net.http.*;
@TaskHandler(topic = "http.call")
public class HttpCallHandler implements Handler {
private static final HttpClient HTTP = HttpClient.newHttpClient();
@Override
public HandlerResult handle(ExternalTask task) {
String url = (String) task.variable("url");
try {
var resp = HTTP.send(
HttpRequest.newBuilder(URI.create(url)).build(),
HttpResponse.BodyHandlers.discarding());
return HandlerResult.complete(Variable.longVar("status", resp.statusCode()));
} catch (Exception e) {
throw new RuntimeException(e); // → /failure
}
}
public static void main(String[] args) throws Exception {
Client c = new Client(new Client.Config("http://localhost:8080"));
Runner r = new Runner(c, new Runner.Config("java-1"));
r.discover(new HttpCallHandler());
r.run();
}
}
Python
import asyncio, httpx
from conduit_worker import (
Client, ClientConfig, Complete, ExternalTask, Runner, RunnerConfig,
Variable, handler,
)
@handler(topic="http.call")
async def http_call(task: ExternalTask) -> Complete:
url = task.variable("url")
async with httpx.AsyncClient() as h:
resp = await h.get(url)
return Complete([Variable.long("status", resp.status_code)])
async def main() -> None:
client = Client(ClientConfig(base_url="http://localhost:8080"))
runner = Runner(client, RunnerConfig(worker_id="python-1"))
runner.discover(http_call)
await runner.run()
asyncio.run(main())
Node (TypeScript)
import {
Client, defineHandler, ExternalTask, HandlerResult,
Runner, Variable,
} from "@conduit/worker";
export const httpCall = defineHandler({
topic: "http.call",
async handle(task: ExternalTask): Promise<HandlerResult> {
const url = task.variable("url") as string;
const resp = await fetch(url);
return HandlerResult.complete([Variable.long("status", resp.status)]);
},
});
const client = new Client({ baseUrl: "http://localhost:8080" });
const runner = new Runner(client, { workerId: "node-1" });
runner.register(httpCall);
await runner.run();
Go
package main
import (
"context"
"encoding/json"
"log"
"net/http"
"os"
"os/signal"
"syscall"
cw "github.com/kinarix/conduit/workers/go/conduitworker"
)
func main() {
client := cw.NewClient(cw.ClientConfig{BaseURL: "http://localhost:8080"})
runner := cw.NewRunner(client, "go-1")
runner.Register("http.call", func(ctx context.Context, t *cw.ExternalTask) (*cw.Result, error) {
var url string
if raw, ok := t.Variable("url"); ok {
_ = json.Unmarshal(raw, &url)
}
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil)
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return cw.Complete(cw.VarLong("status", int64(resp.StatusCode))), nil
})
ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
defer stop()
if err := runner.Run(ctx); err != nil && err != context.Canceled {
log.Fatal(err)
}
}
What’s the same in every example
- The same
<conduit:taskTopic>http.call</conduit:taskTopic>routes the task. The engine doesn’t know about HTTP — it just hands you the task withurlin the variables. - Every SDK reads variables off
taskwith the same shape (task.variable("url")). - Every SDK has factory constructors for typed variables (
Variable.long(...)). - Returning
Completecalls/completewith the variables; throwing or returning an error calls/failure; returning aBpmnErrorcalls/bpmn-error.
The contract is the protocol, not the SDK — see Protocol for the wire-level details.