Rust SDK

The Rust SDK covers both sides of the engine: the typed process builder for embedding the engine and defining processes in Rust, and the Rust Worker SDK for implementing external-task workers that poll the engine, lock tasks, run your logic, and complete or fail tasks.


Typed Process Builder (Rust developer API)

For embedded Rust developers, ProcessBuilder is the ergonomic entry point: you build a process as typed Rust code instead of hand-writing BPMN 2.0 XML or DSL JSON strings.

use bpm_engine::bpm_engine_bpmn::builder::ProcessBuilder;

let def = ProcessBuilder::new("order")
    .start_node("start")
    .external_task("payment", "payment-topic")
        .retries(5)
        .timeout_secs(120)
    .exclusive_gateway("decide")
    .end_node("end-paid")
    .end_node("end-skipped")
    .edge("start", "payment")
    .edge("payment", "decide")
    .edge("decide", "end-paid").when_variable_eq("paid", "true")
    .edge("decide", "end-skipped").is_default()
    .build()?;

Runnable example: cargo run --example builder_process (builds the flow above, deploys it to memory stores, runs it to completion and prints the outcome).

One compiler, three entries

The builder follows ADR-006: it assembles the same intermediate model the BPMN XML parser produces and hands it to the shared bpm_engine_bpmn::compile verbatim. There is no separate validation path.

Entry Audience Format
bpm_engine_bpmn::builder::ProcessBuilder Rust developers Typed Rust code
bpm_engine_dsl::compile(&str) AI agents / generators JSON DSL (schema: bpm_engine_dsl::schema_json())
bpm_engine_bpmn::parse_and_compile(&str) BPMN interop / existing models BPMN 2.0 XML

All three compile to the same ProcessDefinition (enforced by the three-way parity test tests/builder_parity.rs) and execute identically (tests/builder_execution.rs).

Node type coverage

Builder method Compiles to Options (chained)
.start_node(id) NodeType::Start .name()
.end_node(id) NodeType::End .name()
.external_task(id, task_type) NodeType::ExternalTask .retries(n), .timeout_secs(n), .name()
.user_task(id) NodeType::UserTask .form_key(k), .form_field(FormFieldSpec), .name()
.timer_catch(id, duration) NodeType::TimerIntermediateCatch .name()
.exclusive_gateway(id) NodeType::ExclusiveGateway .name()
.parallel_gateway(id) NodeType::ParallelFork / ParallelJoin .name()
.edge(source, target) sequence flow .when_variable_eq(var, value), .when_expression(expr), .is_default()

Out of scope (use BPMN XML for these): boundary events, subprocesses, call activities, message/signal events, terminate ends.

Defaults (identical to the BPMN parser and the DSL)

Error behavior

.build() performs no validation of its own: it returns the shared compiler’s CompileErrors verbatim with their stable ErrorCodes — NoStartEvent, MultipleStartEvents, NoEndEvent, OrphanNode, DeadEnd, ExclusiveGatewayNoDefault, ParallelGatewayInvalidShape, SequenceFlowSourceNotFound, SequenceFlowTargetNotFound. For example, an edge referencing an unknown node id fails at build() time with SequenceFlowTargetNotFound, exactly like a dangling sourceRef/targetRef in XML.

Option setters apply to the most recently added node/edge (the classic BPMN fluent-builder convention). Misuse — .retries() on a user task, a condition before any .edge(), a duplicate node id, or a when_variable_eq variable containing quotes — panics immediately with a precise message: it is static programmer error, never process data, so it never reaches build().

Boundary: inline ServiceTask closures

The core NodeType also has a ServiceTask(fn(&mut ProcessInstance)) variant for synchronous in-process closures. Function pointers cannot round-trip through BPMN/DSL, so the builder deliberately does not cover them — build such definitions by constructing the ProcessDefinition by hand. The builder supports exactly the BPMN-expressible subset, the same set as the JSON DSL.

Semver and the API surface test

The facade’s public surface (builder entry, DSL compile, core model types, runtime engine types, storage traits, memory adapter, worker SDK) is pinned by tests/api_surface.rs: every documented item is referenced through its bpm_engine:: facade path and the load-bearing signatures are checked by fn-pointer coercion. Removing, renaming or re-signaturing any of them fails that test — you cannot make a breaking facade change without updating the snapshot. Additive changes update the test’s list and the changelog; removals/signature changes require a major version bump.

Features

The facade follows the same pattern for all opt-in surfaces:


Rust Worker SDK

The Rust Worker SDK lets you implement external-task workers that poll the engine, lock tasks, run your logic, and complete or fail tasks. No BPM knowledge is required in worker code.


Crates and example


Quick start

  1. Start the engine: cargo run -p bpm-engine-server-rest
  2. Deploy or use a process that has an ExternalTask node (e.g. task_type = "payment").
  3. Run the worker: cargo run -p bpm-engine-worker-sdk --example payment

Delivery and idempotency


Worker Responsibility Contract


Core types


Implementing a handler

Handler must be idempotent; the same task may be delivered more than once.

use async_trait::async_trait;
use bpm_worker_sdk::{
    EngineClient, ExternalTask, TaskContext, TaskHandler, TaskResult, Worker, WorkerConfig,
};
use std::collections::HashMap;

struct PaymentHandler;

#[async_trait]
impl TaskHandler for PaymentHandler {
    fn task_type(&self) -> &str {
        "payment"
    }

    async fn handle(&self, task: ExternalTask, _ctx: TaskContext) -> TaskResult {
        let amount = task.variables.get("amount").cloned().unwrap_or_else(|| "0".to_string());
        // ... your logic ...
        let mut variables = HashMap::new();
        variables.insert("status".to_string(), "PAID".to_string());
        TaskResult::Complete { variables }
    }
}

Running the worker

let client = EngineClient::new("http://127.0.0.1:3000");
let config = WorkerConfig::new("payment-worker-1")
    .max_tasks(5)
    .lock_duration(Duration::from_secs(30))
    .poll_interval(Duration::from_secs(1));

let worker = Worker::builder()
    .client(client)
    .handler(PaymentHandler)
    .config(config)
    .build();

worker.start().await;

Task result

Long-running or fault-tolerant handlers should also look at the execution-context helpers — cancellation, auto lease renewal (run_with_renewal), the with_timeout helper and the IdempotencyGuard — documented in docs/worker-context.md.


Dependencies

Add to your Cargo.toml:

bpm-engine-worker-sdk = { path = "../crates/worker-sdk" }
# or from the workspace root

You need tokio and async-trait for the async handler.

Through the bpm-engine facade the worker SDK is available behind the default-on worker-sdk feature (bpm_engine::bpm_engine_worker_sdk) — default-features = false embedders opt out of it (and its HTTP stack), mirroring the api/observability features.


See also