diff --git a/docs/explanation/bpm-concepts.md b/docs/explanation/bpm-concepts.md
index f14283408..eeea27017 100644
--- a/docs/explanation/bpm-concepts.md
+++ b/docs/explanation/bpm-concepts.md
@@ -6,7 +6,7 @@ description: The core concepts behind ZenBPM - processes, instances, tokens, tas
# BPM Concepts
-This page explains the ideas behind ZenBPM. You don't need to read it before the [Getting Started tutorial](/tutorials/getting-started) - but whenever a term there feels unclear, this is the place to look it up.
+This page explains the ideas behind ZenBPM. You don't need to read it before the [Getting Started tutorial](/tutorials/getting-started/first-bpmn-process) - but whenever a term there feels unclear, this is the place to look it up.
If you already know Camunda, Zeebe, or another BPMN engine, skip to [How ZenBPM maps to other engines](#terminology-mapping).
@@ -28,8 +28,6 @@ A **process definition** is the blueprint - the BPMN diagram you deploy to the e
A **process instance** is one running execution of that blueprint. *"Order #4711 from Alice, currently being handled."* There can be thousands, each with its own state and data, each at a different point in the flow.
-{/* Excalidraw source: definition-vs-instance.excalidraw — export as SVG (embed scene, transparent
- background) and check dark mode; use ThemedImage with a dark variant if needed. */}

The same relationship as a class and its objects, or a recipe and the meals cooked from it.
@@ -111,7 +109,7 @@ This decoupling is intentional. Workers can be written in any language, scaled i
Putting it all together, working with ZenBPM follows one loop: **model** the diagram, **deploy** it to the engine, **start instances** with their variables, let workers and people **execute** the work, and **observe** state and history - then improve the diagram and go around again.
-The [Getting Started tutorial](/tutorials/getting-started) walks this exact loop once, end to end.
+The [Getting Started tutorial](/tutorials/getting-started/first-bpmn-process) walks this exact loop once, end to end.
## How ZenBPM maps to other engines {#terminology-mapping}
@@ -132,6 +130,6 @@ ZenBPM follows the Zeebe-style architecture: an external-worker model over gRPC,
## Where to go next
-- Run the loop yourself: [Getting Started tutorial](/tutorials/getting-started)
+- Run the loop yourself: [Getting Started tutorial](/tutorials/getting-started/first-bpmn-process)
- See working processes and workers: [zenbpm-examples](https://github.com/pbinitiative/zenbpm-examples)
- Engine internals: [Architecture](/category/architecture)
diff --git a/docs/static/client-libraries.md b/docs/static/client-libraries.md
index de6c584bb..041f760de 100644
--- a/docs/static/client-libraries.md
+++ b/docs/static/client-libraries.md
@@ -3,40 +3,67 @@ sidebar_position: 110
---
# Client Libraries
-ZenBPM provides officially supported client libraries in multiple programming languages to help developers integrate with the engine more easily.
+ZenBPM provides officially supported client libraries that wrap its REST and gRPC APIs so you don't have to hand-roll HTTP calls or gRPC streams.
-The versions of the libraries are aligned with the ZenBPM engine versions.
+Every client does two things:
+
+- **REST** — deploy process/decision resources, start instances, query state.
+- **gRPC** — run *job workers*: subscribe to a job type, complete or fail jobs over a bidirectional stream.
+
+## Versioning
+
+The Go client ships as part of the engine module, so it tracks the engine version. The **Java** client is published to **Maven Central** under group `org.pbinitiative.zenbpm`, and its version tracks the engine version too (e.g. `1.4.0`).
+
+## Choosing a client
+
+| Language | Artifact | Use when |
+|---|---|---|
+| Go | `github.com/pbinitiative/zenbpm/pkg/zenclient` | Any Go application. Ships as part of the engine module. |
+| Java | `org.pbinitiative.zenbpm:zenbpm-client-core` | Java apps **not** using Spring Boot. Works on older Java versions. |
+| Java | `org.pbinitiative.zenbpm:zenbpm-spring-boot-starter` | Spring Boot apps. Adds auto-configuration and the `@JobWorker` annotation; pulls in `zenbpm-client-core`. |
+
+> The examples below omit error handling for brevity. Handle returned errors/exceptions in real code.
+
+---
## Go Client
-The Go client is part of the ZenBPM engine and is available as package `github.com/pbinitiative/zenbpm/pkg/zenclient`.
+The Go client lives in package `github.com/pbinitiative/zenbpm/pkg/zenclient` and provides both a REST client and a gRPC worker client.
+
+### Install
+
+```bash
+go get github.com/pbinitiative/zenbpm@latest
+```
-The `zenclient` package provides two clients:
-- REST (HTTP) client for managing process/decision resources, starting instances, etc.
-- gRPC worker client for subscribing to job types and completing/failing jobs via a bidirectional stream.
+The client is part of the engine module, so its version follows the engine version.
+
+### Deploy and start an instance (REST)
-### Usage examples
-#### Deploy a BPMN process definition and start a process instance
-Simplified example:
```go
-restClient, _ := zenclient.NewClient("http://localhost:8080/v1")
+// The "WithResponses" client returns typed, parsed responses (with JSON201 etc.).
+restClient, _ := zenclient.NewClientWithResponses("http://localhost:8080/v1")
+// Deploy is a multipart upload of the .bpmn file.
var bodyBuf bytes.Buffer
mw := multipart.NewWriter(&bodyBuf)
-...
-resp1, _ := restClient.CreateProcessDefinitionWithBody(
- ctx,
- mw.FormDataContentType(),
- &bodyBuf,
+// ... write the .bpmn file into the "resource" form field ...
+mw.Close()
+
+defResp, _ := restClient.CreateProcessDefinitionWithBodyWithResponse(
+ ctx, mw.FormDataContentType(), &bodyBuf,
)
+key := defResp.JSON201.ProcessDefinitionKey
-startBody := zenclient.CreateProcessInstanceJSONRequestBody{
- ProcessDefinitionKey: key,
-}
-resp2, _ := restClient.CreateProcessInstance(ctx, startBody)
+// Start an instance from the returned key.
+instResp, _ := restClient.CreateProcessInstanceWithResponse(ctx,
+ zenclient.CreateProcessInstanceJSONRequestBody{ProcessDefinitionKey: &key},
+)
+_ = instResp.JSON201 // the started ProcessInstance
```
-#### Register a worker
-Simplified example:
+
+### Register a worker (gRPC)
+
```go
conn, _ := grpc.NewClient("127.0.0.1:9090", grpc.WithTransportCredentials(insecure.NewCredentials()))
defer conn.Close()
@@ -44,92 +71,141 @@ defer conn.Close()
zen := zenclient.NewGrpc(conn)
jobWorker := func(ctx context.Context, job *proto.WaitingJob) (map[string]any, *zenclient.WorkerError) {
-// ...
+ vars := job.GetVariables()
+
+ // ... do the work ...
+
+ // Success: return output variables (or an empty map) and nil.
+ return map[string]any{"result": "done"}, nil
+
+ // To fail the job (and trigger a retry) return a WorkerError instead:
+ // return nil, &zenclient.WorkerError{ErrorCode: "BUSINESS_ERROR"}
}
zen.RegisterWorker(context.Background(), "my-client-id", jobWorker, "my-job-type")
```
-## Java Client
-The Java client is a lightweight library that wraps the REST and gRPC APIs, providing a type-safe interface for Java applications.
+---
-The client is available on GitHub: [zenbpm-java-client](https://github.com/pbinitiative/zenbpm-java-client)
+## Java Client
-There are 2 artefacts available:
-- **zenbpm-client-code:** the core library, can be used independently of Spring Boot and supports old java versions
-- **zenbpm-spring-boot-starter:** a Spring Boot starter that provides auto-configuration for the core library and `@JobWorker("jobName")` method annotation.
+The Java client wraps the REST and gRPC APIs with a type-safe interface (DTOs are generated from the OpenAPI spec). Source: [zenbpm-java-client](https://github.com/pbinitiative/zenbpm-java-client).
-### Features
+### Install
-* Spring Boot auto-configuration (drop-in starter)
-* REST client (`ApiClient` + typed APIs generated from OpenAPI)
-* gRPC job workers via `@JobWorker` and ZenbpmJobWorkerManager
-* OpenTelemetry interceptors for REST and spans for gRPC
-* Configurable HTTP/gRPC logging
+The client is on **Maven Central** (group `org.pbinitiative.zenbpm`), so no extra repository is needed. Set `zenbpm.version` to the version matching your engine.
-### Build Java Client
-``mvn clean package``
+**Spring Boot** — a complete, copy-pasteable `pom.xml` for a worker application:
-### Getting started
+```xml
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.4
+
+
+
+ com.example
+ my-worker
+ 1.0.0
+
+
+ 17
+ 1.4.0
+ 1.58.0
+
+
+
+
+
+
+ io.opentelemetry
+ opentelemetry-bom
+ ${opentelemetry.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.pbinitiative.zenbpm
+ zenbpm-spring-boot-starter
+ ${zenbpm.version}
+
+
+
+ io.grpc
+ grpc-netty-shaded
+ 1.80.0
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+```
-Add the starter to your application and the core client as needed.
+**Non-Spring (core only)** — if you're not using Spring Boot, depend on the core client instead of the starter (no OpenTelemetry BOM or `spring-boot-starter` needed):
-Maven:
```xml
- org.zenbpm
- zenbpm-spring-boot-starter
- ${zenbpm.version}
-
-
- org.zenbpm
+ org.pbinitiative.zenbpmzenbpm-client-core${zenbpm.version}
```
-Configure connection settings in application.yml
+### Configure
-values shown in `zenbpm` section are defaults.
+The Spring Boot starter is configured through `application.yml`. Minimal configuration to connect to a local engine:
-`logging` section configures logging for rest and grpc clients separately.
- - `DEBUG` levels expose headers of calls and responses.
- - `TRACE` level exposes full request and response bodies. **Never use this in production!**
```yaml
zenbpm:
restUrl: http://localhost:8080/v1
- restLoggingEnabled: true
grpcHost: localhost
grpcPort: 9090
- grpcPlaintext: true
- grpcLoggingEnabled: true
- jobWorkerEnabled: true
- otelEnabled: true
-
-logging:
- level:
- root: INFO
- org.zenbpm.rest: TRACE
- org.zenbpm.grpc: DEBUG
-
+ grpcPlaintext: true # local/dev only; use TLS in production
+ jobWorkerEnabled: true # connect job workers on startup
```
-### Working examples
+See [Configuration reference](#configuration-reference) for all options, including logging.
+
+> Using `zenbpm-client-core` without Spring Boot? You configure the `ApiClient` and worker manager programmatically instead of via `application.yml` — see the [client repository](https://github.com/pbinitiative/zenbpm-java-client) for a plain-Java example.
-#### 1) Use REST APIs
-Inject the provided ZenbpmClientService to obtain the ApiClient, then create a typed API.
+### Deploy and start an instance (REST)
+
+Inject `ZenbpmClientService` to obtain the `ApiClient`, then use the typed APIs.
```java
import org.springframework.stereotype.Service;
import org.springframework.beans.factory.annotation.Autowired;
-import org.zenbpm.rest.ZenbpmClientService;
-import org.zenbpm.client.ApiException;
-import org.zenbpm.client.ApiClient;
-import org.zenbpm.client.api.ProcessDefinitionApi;
-import org.zenbpm.client.api.ProcessInstanceApi;
-import org.zenbpm.client.api.dto.CreateProcessInstanceRequest;
-
-import java.util.HashMap;
+import org.pbinitiative.zenbpm.rest.ZenbpmClientService;
+import org.pbinitiative.zenbpm.client.ApiException;
+import org.pbinitiative.zenbpm.client.ApiClient;
+import org.pbinitiative.zenbpm.client.api.ProcessDefinitionApi;
+import org.pbinitiative.zenbpm.client.api.ProcessInstanceApi;
+import org.pbinitiative.zenbpm.client.api.dto.CreateProcessInstanceRequest;
+
import java.util.Map;
@Service
@@ -137,72 +213,104 @@ public class MyService {
@Autowired
private ZenbpmClientService zenbpm;
- public Long deployExampleProcess() throws ApiException {
+ public Long deployProcess(String bpmnXml) throws ApiException {
ApiClient apiClient = zenbpm.getApiClient();
ProcessDefinitionApi defApi = new ProcessDefinitionApi(apiClient);
- // Example: create a process definition from a BPMN string (adjust to your endpoint contract)
- String bpmnXml = "...";
- Long definitionKey = defApi.createProcessDefinition(bpmnXml).getProcessDefinitionKey();
- return definitionKey;
+ // Deploy the BPMN definition; the generated client returns the 201 response body.
+ return defApi.createProcessDefinition(bpmnXml).getProcessDefinitionKey();
}
- public void startMyProcess() throws ApiException {
+ public void startProcess(Long definitionKey) throws ApiException {
ApiClient apiClient = zenbpm.getApiClient();
ProcessInstanceApi piApi = new ProcessInstanceApi(apiClient);
- Map vars = new HashMap<>();
- vars.put("orderId", 12345L);
-
CreateProcessInstanceRequest req = new CreateProcessInstanceRequest()
- .processDefinitionKey(123456L)
- .variables(vars);
+ .processDefinitionKey(definitionKey)
+ .variables(Map.of("orderId", 12345L));
piApi.createProcessInstance(req);
}
}
```
-Notes:
-- Available typed APIs include ProcessDefinitionApi, ProcessInstanceApi, JobApi, MessageApi, etc. Construct them with the provided ApiClient.
-- Methods and DTOs come from the generated package `org.zenbpm.client.api` and `org.zenbpm.client.api.dto`.
+Available typed APIs include `ProcessDefinitionApi`, `ProcessInstanceApi`, `JobApi`, `MessageApi`, and others. Methods and DTOs come from the generated packages `org.pbinitiative.zenbpm.client.api` and `org.pbinitiative.zenbpm.client.api.dto`.
-#### 2) Register a gRPC job worker
-Create a Spring bean with a method annotated by `@JobWorker`. Accepted method signatures:
-- no parameters
-- one parameter of type `org.zenbpm.proto.Zenbpm.WaitingJob`
-- one parameter of type `org.zenbpm.grpc.JobContext`
-- one parameter of type `Map`
+### Register a worker (gRPC)
-Return value can be any object and will be serialized as variables for job completion. Throwing an exception fails the job.
+Annotate a Spring bean method with `@JobWorker("")`. The gRPC worker manager connects on application startup when `zenbpm.jobWorkerEnabled` is `true`.
```java
import org.springframework.stereotype.Component;
-import org.zenbpm.grpc.JobWorker;
-import org.zenbpm.grpc.JobContext;
+import org.pbinitiative.zenbpm.grpc.JobWorker;
+import org.pbinitiative.zenbpm.grpc.JobContext;
import java.util.Map;
-import java.util.HashMap;
@Component
public class EmailWorker {
@JobWorker("send-email")
public Map handleJob(JobContext ctx) {
- Map vars = ctx.getVariables();
+ Map vars = ctx.getVariables();
String to = (String) vars.get("email");
- // send email ...
+ // ... send the email ...
- Map result = new HashMap<>();
- result.put("success", true);
- result.put("message", "Email to " + to + " mocked successfully");
- return result;
+ // Return value is serialized as output variables on job completion.
+ return Map.of("emailSent", true);
+ // Throw an exception to fail the job (triggering a retry).
}
}
```
-The gRPC worker manager connects on application start if `zenbpm.jobWorkerEnabled` is true.
+Accepted `@JobWorker` method parameters (pick one):
+
+- no parameters
+- `org.pbinitiative.zenbpm.proto.Zenbpm.WaitingJob`
+- `org.pbinitiative.zenbpm.grpc.JobContext`
+- `Map` (the job variables)
+
+---
+
+## Configuration reference
+
+Full set of `application.yml` options for the Java Spring Boot starter.
+
+| Key | Purpose |
+|---|---|
+| `zenbpm.restUrl` | Base URL of the engine REST API (include `/v1`). |
+| `zenbpm.grpcHost` | Engine gRPC host. |
+| `zenbpm.grpcPort` | Engine gRPC port. |
+| `zenbpm.grpcPlaintext` | Use plaintext gRPC (no TLS). Local/dev only. |
+| `zenbpm.jobWorkerEnabled` | Connect registered job workers on startup. |
+| `zenbpm.otelEnabled` | Enable OpenTelemetry interceptors (REST) and spans (gRPC). |
+| `zenbpm.restLoggingEnabled` | Enable request/response logging for the REST client. |
+| `zenbpm.grpcLoggingEnabled` | Enable logging for the gRPC client. |
+
+> See the [client repository](https://github.com/pbinitiative/zenbpm-java-client) for the authoritative list of options and their defaults.
+
+Logging verbosity is controlled through standard Spring logging levels, configured **per client** (`org.pbinitiative.zenbpm.rest`, `org.pbinitiative.zenbpm.grpc`):
+
+- `DEBUG` — exposes request/response headers.
+- `TRACE` — exposes full request and response **bodies**. **Never use `TRACE` in production**; it can leak sensitive data.
+
+```yaml
+logging:
+ level:
+ org.pbinitiative.zenbpm.rest: DEBUG
+ org.pbinitiative.zenbpm.grpc: DEBUG
+```
+
+## Building the Java client from source
+
+Most users consume the published Maven Central artifacts and don't need this. To build the Java client from source:
+
+```bash
+mvn clean package
+```
## Future Clients
+Officially supported clients are planned for:
+
- Python
- JavaScript / TypeScript
diff --git a/docs/tutorials/assets/images/approval-process.svg b/docs/tutorials/assets/images/approval-process.svg
new file mode 100644
index 000000000..034378beb
--- /dev/null
+++ b/docs/tutorials/assets/images/approval-process.svg
@@ -0,0 +1,31 @@
+
diff --git a/docs/tutorials/assets/images/first-bpmn-process.svg b/docs/tutorials/assets/images/first-bpmn-process.svg
new file mode 100644
index 000000000..fad3d3dcb
--- /dev/null
+++ b/docs/tutorials/assets/images/first-bpmn-process.svg
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/docs/tutorials/getting-started.mdx b/docs/tutorials/getting-started.mdx
deleted file mode 100644
index 2330e3e2a..000000000
--- a/docs/tutorials/getting-started.mdx
+++ /dev/null
@@ -1,165 +0,0 @@
-import ApiOperation from "@theme/ApiOperation";
-
-# Getting Started with ZenBPM
-
-This tutorial will guide you through the basics of ZenBPM, helping you set up your first BPMN process and execute it.
-
-## What You'll Learn
-
-- How to install and run ZenBPM
-- How to create a simple BPMN process
-- How to deploy the process to ZenBPM
-- How to start a process instance
-- How to monitor the process execution
-
-## Prerequisites
-
-- Basic understanding of what BPMN is
-- Docker installed on your machine
-
-## Step 1: Install and Run ZenBPM
-
-The easiest way to get started with ZenBPM is to use Docker.
-
-1. Pull the ZenBPM Docker image:
-
-```bash
-docker pull ghcr.io/pbinitiative/zenbpm:latest
-```
-
-:::
-
-2. Run the ZenBPM container:
-
-```bash
-docker run -d -p 8080:8080 -p 9090:9090 --name zenbpm ghcr.io/pbinitiative/zenbpm:latest
-```
-
-:::
-
-This will start ZenBPM with:
-
-- REST API available at localhost:8080
-- gRPC API available at localhost:9090
-
-## Step 2: Create a Simple BPMN Process
-
-For this tutorial, we'll create a simple "Hello World" process with a start event, a message throw event, and an end event.
-
-1. Create a file named `hello-world.bpmn` with the following content:
-
-```xml
-
-
-
-
- Flow_1l01xu9
-
-
-
- Flow_02ezsew
-
-
-
- Flow_1l01xu9
- Flow_02ezsew
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-```
-
-This BPMN file defines a simple process with:
-
-- A start event
-- A Message intermediate throw event
-- An end event
-
-## Step 3: Deploy the Process to ZenBPM
-
-Now that we have our BPMN process, let's deploy it to ZenBPM:
-
-
-
-You should receive a response with details about the deployed process definition, including a `processDefinitionKey` that uniquely identifies it.
-
-## Step 4: Start a Process Instance
-
-With the process definition deployed, we can now start a process instance:
-
-
-
-This will start a new instance of our "Hello World" process. The response will include a `processInstanceKey` that uniquely identifies this instance.
-
-## Step 5: Monitor the Process Execution
-
-Let's check the status of our process instance:
-
-
-
-Replace `{processInstanceKey}` with the actual key from the previous step.
-
-You should see details about the process instance, including its current state. Since our process is very simple, it might have already completed by the time you check.
-
-## Step 6: View Process history
-
-:::warning
-this feature is not implemented yet
-:::
-To see what activities were executed in our process:
-
-
-
-This will show you all the activities that were executed as part of the process instance, including the message throw event.
-
-## Conclusion
-
-Congratulations! You've successfully:
-
-- Set up ZenBPM
-- Created a simple BPMN process
-- Deployed the process to ZenBPM
-- Started a process instance
-- Monitored the process execution
-
-## Next Steps
-
-[//]: # "TODO: Link documents after creation"
-
-Now that you've completed this basic tutorial, you might want to:
-
-- Learn how to [create more complex BPMN processes](/future-feature) _`TODO`_
-- Explore [user tasks and forms](/future-feature) _`TODO`_
-- Understand [message events and correlation](/future-feature) _`TODO`_
-- Set up [process monitoring and observability](/future-feature) _`TODO`_
diff --git a/docs/tutorials/getting-started/_category_.json b/docs/tutorials/getting-started/_category_.json
new file mode 100644
index 000000000..fb126c888
--- /dev/null
+++ b/docs/tutorials/getting-started/_category_.json
@@ -0,0 +1,8 @@
+{
+ "label": "Getting Started",
+ "position": 1,
+ "link": {
+ "type": "generated-index",
+ "description": "Get up and running with the ZenBPM engine: start it, deploy and run your first BPMN process, then orchestrate a human task."
+ }
+}
diff --git a/docs/tutorials/getting-started/first-bpmn-process.mdx b/docs/tutorials/getting-started/first-bpmn-process.mdx
new file mode 100644
index 000000000..602340241
--- /dev/null
+++ b/docs/tutorials/getting-started/first-bpmn-process.mdx
@@ -0,0 +1,308 @@
+---
+id: first-bpmn-process
+title: First BPMN process
+sidebar_position: 2
+description: Deploy a BPMN process over REST, write a worker that connects over gRPC, and watch a process instance execute end to end.
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# First BPMN process
+
+With the engine running from the [previous chapter](run-the-engine), you'll now put it to work — talking to it over its REST and gRPC APIs, exactly as your own code will. By the end you'll have deployed a process, written a worker that does real work, and watched an instance run to completion.
+
+{/* TODO: restore once the UI/platform docs land.
+If you'd rather deploy and run processes visually, start with the [ZenBPM UI documentation](/ui) or the [Getting Started platform basics](/platform) instead — this page uses only the terminal and code.
+*/}
+
+:::info New to BPM?
+The engine executes business processes described as **BPMN** diagrams. A **process definition** is the blueprint; a running copy of it is a **process instance**; a **worker** is a program *you* write that carries out a step. That's enough for this tutorial — for the full picture, see [BPM Concepts](/explanation/bpm-concepts).
+
+Coming from Camunda or Zeebe? This is the familiar external-worker model over gRPC; skim [the terminology mapping](/explanation/bpm-concepts#terminology-mapping) and read straight on.
+:::
+
+## What you'll build {#what-youll-build}
+
+You'll run a minimal process (`first-bpmn-process.bpmn`): a single service task, *Log Greeting*. When an instance reaches that task, the engine creates a **job** and waits. Nothing happens until a **worker** — which you'll write — connects, picks the job up, reads a variable, and prints a greeting.
+
+{/* Rendered from getting-started/02-first-bpmn-process/first-bpmn-process.bpmn via bpmn-to-image.
+ Keep the .bpmn source next to the SVG so the figure always matches the real process. */}
+
+
+That "the engine creates a job and waits for your worker" mechanic is the heart of ZenBPM — this tutorial exists mainly to make it concrete.
+
+## Prerequisites {#prerequisites}
+
+- The **engine running locally** and the examples repo cloned — see [Run the engine](run-the-engine)
+- `curl` — or any HTTP client
+- For the worker (Step 3), **either** [Java](https://adoptium.net/) 17+ with [Maven](https://maven.apache.org/) **or** [Go](https://go.dev/dl/) 1.22+
+
+## Step 1: Deploy a process {#step-1-deploy-a-process}
+
+A **process definition** is a `.bpmn` file. This chapter's process is `first-bpmn-process.bpmn` in the repo you cloned. From `getting-started/`, move into the chapter folder:
+
+```bash
+cd 02-first-bpmn-process
+```
+
+Deploy it with a multipart upload:
+
+```bash
+curl -X POST http://localhost:8080/v1/process-definitions \
+ -F "resource=@first-bpmn-process.bpmn"
+```
+
+The response contains a `processDefinitionKey` — a generated number that uniquely identifies this deployed definition. Note it; you'll start an instance from it next.
+
+```json
+{ "processDefinitionKey": 4503599627370498 }
+```
+
+## Step 2: Start an instance (and watch it wait) {#step-2-start-an-instance}
+
+Starting an instance asks the engine to execute one copy of the definition. Use the key from Step 1:
+
+```bash
+curl -X POST http://localhost:8080/v1/process-instances \
+ -H "Content-Type: application/json" \
+ -d '{"processDefinitionKey": 4503599627370498, "variables": {}}'
+```
+
+You get back a `processInstanceKey` and a state of `active`:
+
+```json
+{
+ "key": 4503599627370501,
+ "processDefinitionKey": 4503599627370498,
+ "state": "active"
+}
+```
+
+Query the instance by its key:
+
+```bash
+curl http://localhost:8080/v1/process-instances/4503599627370501
+```
+
+It's **still active** — the token has reached the *Log Greeting* service task, the engine has created a job for it, and there it sits. The engine will never run your business logic itself; it's waiting for a worker to take the job. That's what you'll build now.
+
+## Step 3: Write a worker {#step-3-write-a-worker}
+
+A **worker** connects to the engine over gRPC, subscribes to a **job type**, and for each job: reads the variables, does the work, and reports completion. This process's service task creates jobs of type `log-worker`; the work is simply to print the `log` variable.
+
+ZenBPM ships officially supported clients for Java and Go, and this chapter includes a ready-to-run worker project in each (the `java/` and `go/` folders of [`getting-started/02-first-bpmn-process`](https://github.com/pbinitiative/zenbpm-examples/tree/main/getting-started/02-first-bpmn-process)). Pick your language:
+
+
+
+
+The Java client is a Spring Boot starter: you declare a worker as a bean method annotated with `@JobWorker`, and the starter connects to the engine on startup.
+
+The client is on **Maven Central** under `org.pbinitiative.zenbpm`, versioned in step with the engine (`1.4.0`). Here is a complete, copy-pasteable `pom.xml`:
+
+```xml
+
+
+ 4.0.0
+
+
+ org.springframework.boot
+ spring-boot-starter-parent
+ 3.3.4
+
+
+
+ org.pbinitiative.examples
+ first-bpmn-process-worker
+ 1.0.0
+
+
+ 17
+ 1.4.0
+ 1.58.0
+
+
+
+
+
+
+ io.opentelemetry
+ opentelemetry-bom
+ ${opentelemetry.version}
+ pom
+ import
+
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-starter
+
+
+
+ org.pbinitiative.zenbpm
+ zenbpm-spring-boot-starter
+ ${zenbpm.version}
+
+
+
+ io.grpc
+ grpc-netty-shaded
+ 1.80.0
+
+
+
+
+
+
+ org.springframework.boot
+ spring-boot-maven-plugin
+
+
+
+
+```
+
+Point the starter at your engine in `application.yml`:
+
+```yaml
+zenbpm:
+ grpcHost: localhost
+ grpcPort: 9090
+ grpcPlaintext: true
+ jobWorkerEnabled: true
+```
+
+Declare the worker. The method subscribes to the `log-worker` job type, reads the `log` variable, and prints it:
+
+```java
+import org.springframework.stereotype.Component;
+import org.pbinitiative.zenbpm.grpc.JobWorker;
+import org.pbinitiative.zenbpm.grpc.JobContext;
+import java.util.Map;
+
+@Component
+public class LogWorker {
+ @JobWorker("log-worker")
+ public Map handleJob(JobContext ctx) {
+ System.out.println("[log-worker] " + ctx.getVariables().get("log"));
+ return Map.of(); // no output variables; job complete
+ }
+}
+```
+
+This worker is ready in the chapter's `java/` folder. From `02-first-bpmn-process/`, start it:
+
+```bash
+cd java
+mvn spring-boot:run
+```
+
+
+
+
+The Go client is part of the engine module (`github.com/pbinitiative/zenbpm/pkg/zenclient`). Register a worker with a handler function:
+
+```go
+package main
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/pbinitiative/zenbpm/pkg/zenclient"
+ "github.com/pbinitiative/zenbpm/pkg/zenclient/proto"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/credentials/insecure"
+)
+
+func main() {
+ conn, err := grpc.NewClient(
+ "127.0.0.1:9090",
+ grpc.WithTransportCredentials(insecure.NewCredentials()),
+ )
+ if err != nil {
+ panic(err)
+ }
+ defer conn.Close()
+
+ zen := zenclient.NewGrpc(conn)
+
+ // Subscribe to "log-worker" jobs.
+ zen.RegisterWorker(context.Background(), "first-bpmn-process-worker",
+ func(ctx context.Context, job *proto.WaitingJob) (map[string]any, *zenclient.WorkerError) {
+ vars := job.GetVariables()
+ fmt.Printf("[log-worker] %v\n", vars["log"])
+ return map[string]any{}, nil // no output variables; job complete
+ },
+ "log-worker",
+ )
+
+ select {} // keep the worker running
+}
+```
+
+This worker is ready in the chapter's `go/` folder. From `02-first-bpmn-process/`, start it:
+
+```bash
+cd go
+go mod tidy
+go run .
+```
+
+
+
+
+The worker connects, finds the waiting job from Step 2, and prints:
+
+```
+[log-worker] Hello, World!
+```
+
+The moment it reports the job complete, the engine advances the token past the service task to the end event.
+
+:::note Client reference
+Full dependency coordinates, configuration options, and REST/gRPC usage for both languages are in the [Client Libraries](/static/client-libraries) reference.
+:::
+
+## Step 4: Observe completion {#step-4-observe-completion}
+
+Query the same instance again:
+
+```bash
+curl http://localhost:8080/v1/process-instances/4503599627370501
+```
+
+Its state is now `completed`. You just watched the full engine loop: a token entered the process, waited at a service task, your worker did the work, and the instance finished — driven entirely through the API, exactly as your own application would drive it.
+
+## Step 5: Make it yours {#step-5-make-it-yours}
+
+Change the greeting and redeploy to see versioning in action. In `first-bpmn-process.bpmn`, find the input mapping that sets `log` to `Hello, World!` and change the text. Redeploy the same file:
+
+```bash
+curl -X POST http://localhost:8080/v1/process-definitions \
+ -F "resource=@first-bpmn-process.bpmn"
+```
+
+Because the process id is unchanged, the engine stores this as a **new version** and returns a new `processDefinitionKey`. Start an instance against the new key (Step 2) with your worker still running — your new greeting prints. Older instances keep running on the version they started with.
+
+## Clean up {#clean-up}
+
+Stop the worker (Ctrl+C), then stop the engine from the `getting-started/` folder:
+
+```bash
+docker compose down
+```
+
+## Next steps {#next-steps}
+
+- **Understand the model.** [BPM Concepts](/explanation/bpm-concepts) — definitions, instances, tokens, and the job/worker mechanic in one short read.
+- **A realistic process.** The [Commission Payout example](https://github.com/pbinitiative/zenbpm-examples/tree/main/examples/processes/02-commission-payout) adds user tasks and an exclusive gateway.
+- **More workers.** [`examples/workers/`](https://github.com/pbinitiative/zenbpm-examples/tree/main/examples/workers) shows several workers in one Go project.
+- **The APIs in full.** [REST (OpenAPI)](/static/openapi) and the [gRPC proto](/zenbpm.proto).
+- **Engine internals.** [Architecture](/category/architecture).
diff --git a/docs/tutorials/getting-started/orchestrate-human-tasks.mdx b/docs/tutorials/getting-started/orchestrate-human-tasks.mdx
new file mode 100644
index 000000000..7ac6b0ed9
--- /dev/null
+++ b/docs/tutorials/getting-started/orchestrate-human-tasks.mdx
@@ -0,0 +1,200 @@
+---
+id: orchestrate-human-tasks
+title: Orchestrate human tasks
+sidebar_position: 3
+description: Model a BPMN user task, run it on the ZenBPM engine, and drive the human step over REST — list the pending task, assign it, and complete it with a decision.
+---
+
+import Tabs from '@theme/Tabs';
+import TabItem from '@theme/TabItem';
+
+# Orchestrate human tasks
+
+In the [previous chapter](first-bpmn-process), a *service task* handed work to a worker you wrote. Many processes also need a step that waits for a **person** — an approval, a review, a form to fill in. In BPMN that's a **user task**, and this chapter shows how to drive one with the engine.
+
+The key difference: a service task is completed by a worker that *polls* the engine; a user task is completed by *your application* (or a person through the UI) calling the engine when the human is done. At the engine level both are **jobs** — you just interact with a user-task job through the REST `/jobs` API instead of a gRPC worker.
+
+:::info Prerequisites
+The engine running locally and the examples repo cloned, as set up in [Run the engine](run-the-engine). No worker is needed this time — you'll play the human.
+:::
+
+## What you'll build {#what-youll-build}
+
+A minimal **approval** process: a request comes in, a person approves or rejects it, done.
+
+
+
+This chapter's process is `approval.bpmn`. From `getting-started/`, move into the chapter folder:
+
+```bash
+cd 03-orchestrate-human-tasks
+```
+
+In the BPMN, the human step is a `userTask` marked with a `` extension. That's what tells the engine to create a **user-task job** and wait, rather than dispatching to a worker.
+
+## Step 1: Deploy and start {#step-1-deploy-and-start}
+
+Deploy the process and start an instance, exactly as in the previous tutorial:
+
+```bash
+# Deploy
+curl -X POST http://localhost:8080/v1/process-definitions \
+ -F "resource=@approval.bpmn"
+# -> note the returned processDefinitionKey
+
+# Start an instance
+curl -X POST http://localhost:8080/v1/process-instances \
+ -H "Content-Type: application/json" \
+ -d '{"processDefinitionKey": , "variables": {}}'
+# -> note the returned processInstanceKey
+```
+
+Query the instance — it's `active`, because the token has reached the user task and is waiting for a person:
+
+```bash
+curl http://localhost:8080/v1/process-instances/
+```
+
+## Step 2: Find the pending task {#step-2-find-the-task}
+
+User tasks are jobs of type `user-task`. List the ones waiting to be worked on:
+
+
+
+
+```bash
+curl "http://localhost:8080/v1/jobs?jobType=user-task&state=active"
+```
+
+
+
+
+```java
+// JobApi is generated from the OpenAPI spec; construct it with the ApiClient.
+// verify: exact method/param names against the generated JobApi.
+JobApi jobApi = new JobApi(zenbpm.getApiClient());
+JobPartitionPage jobs = jobApi.getJobs(
+ /* processInstanceKey */ null,
+ /* jobType */ "user-task",
+ /* assignee */ null,
+ /* state */ "active",
+ /* page */ 1,
+ /* size */ 10,
+ /* sortBy */ null,
+ /* sortOrder */ null
+);
+```
+
+
+
+
+```go
+// verify: exact params/struct names against the generated zenclient.
+restClient, _ := zenclient.NewClient("http://localhost:8080/v1")
+jobType := "user-task"
+state := zenclient.JobStateActive
+resp, _ := restClient.GetJobs(ctx, &zenclient.GetJobsParams{
+ JobType: &jobType,
+ State: &state,
+})
+```
+
+
+
+
+The response lists each waiting task with its `key`, the `elementId` (`Task_Approve`), the `processInstanceKey`, and its `inputVariables` — here the `instruction` the process passed in for the person to act on. Note the job `key`; you'll use it next.
+
+## Step 3: Assign the task {#step-3-assign-the-task}
+
+Claiming a task records who is responsible for it. In a real app this happens when a user opens the task in their inbox.
+
+
+
+
+```bash
+curl -X POST http://localhost:8080/v1/jobs//assign \
+ -H "Content-Type: application/json" \
+ -d '{"assignee": "john.doe"}'
+```
+
+
+
+
+```java
+// verify: DTO name (AssignJobRequest) against the generated client.
+jobApi.assignJob(jobKey, new AssignJobRequest().assignee("john.doe"));
+```
+
+
+
+
+```go
+// verify: request body type name against the generated zenclient.
+restClient.AssignJob(ctx, jobKey, zenclient.AssignJobJSONRequestBody{
+ Assignee: "john.doe",
+})
+```
+
+
+
+
+A successful assign returns `204 No Content`.
+
+## Step 4: Complete the task with a decision {#step-4-complete-the-task}
+
+When the person is done, complete the job. The variables you send become part of the process instance — this is how the human's decision flows into the rest of the process (for example, a later gateway could branch on `approved`).
+
+
+
+
+```bash
+curl -X POST http://localhost:8080/v1/jobs//complete \
+ -H "Content-Type: application/json" \
+ -d '{"variables": {"approved": true}}'
+```
+
+
+
+
+```java
+// verify: DTO name (CompleteJobRequest) against the generated client.
+jobApi.completeJob(jobKey, new CompleteJobRequest()
+ .variables(Map.of("approved", true)));
+```
+
+
+
+
+```go
+// verify: request body type name against the generated zenclient.
+restClient.CompleteJob(ctx, jobKey, zenclient.CompleteJobJSONRequestBody{
+ Variables: &map[string]any{"approved": true},
+})
+```
+
+
+
+
+The token advances to the end event. Query the instance again — it's now `completed`, and its variables include the `approved` decision you submitted:
+
+```bash
+curl http://localhost:8080/v1/process-instances/
+```
+
+## Rejecting, and where your app fits {#rejecting-and-where-your-app-fits}
+
+Completing with `{"approved": false}` is a perfectly normal outcome — "reject" is a decision, not an error, so you still **complete** the job (with a different variable) rather than failing it. Reserve `POST /jobs/{jobKey}/fail` for genuine failures (a downstream system was unreachable, invalid data), which trigger a retry.
+
+In a real system you don't call these endpoints by hand. Your application backend:
+
+1. lists user-task jobs (Step 2) to build each person's task inbox,
+2. assigns a task when someone opens it (Step 3),
+3. shows a form, and on submit completes the task with the entered variables (Step 4).
+
+The engine stays the source of truth for what's pending and what was decided; your app is just the human-facing surface — and the ZenBPM UI is one ready-made such surface.
+
+## Next steps {#next-steps}
+
+- **A realistic process.** The [Commission Payout example](https://github.com/pbinitiative/zenbpm-examples/tree/main/examples/processes/02-commission-payout) combines user tasks with an exclusive gateway (branching on the human decision) and an AI service task.
+- **The concepts.** [BPM Concepts → tasks](/explanation/bpm-concepts#tasks) explains user vs. service tasks and the job model.
+- **The full API.** The `/jobs` endpoints in the [REST reference](/static/openapi).
diff --git a/docs/tutorials/getting-started/run-the-engine.mdx b/docs/tutorials/getting-started/run-the-engine.mdx
new file mode 100644
index 000000000..f540185e8
--- /dev/null
+++ b/docs/tutorials/getting-started/run-the-engine.mdx
@@ -0,0 +1,98 @@
+---
+id: run-the-engine
+title: Run the engine
+sidebar_position: 1
+description: Start the ZenBPM engine with Docker Compose and verify it is healthy — the foundation for every other Getting Started chapter.
+---
+
+# Run the engine
+
+Everything in Getting Started builds on a running engine. This short chapter gets one up on your machine and confirms it's healthy — no BPMN yet. Once you're done here, move on to [First BPMN process](first-bpmn-process).
+
+:::info New to BPM?
+ZenBPM is a **process engine**: you give it business processes described as BPMN diagrams and it executes them. You don't need to know BPMN to start the engine. For background, see [BPM Concepts](/explanation/bpm-concepts).
+:::
+
+## Prerequisites {#prerequisites}
+
+- [Docker](https://docs.docker.com/get-docker/) with Compose
+- [Git](https://git-scm.com/downloads)
+- `curl` — or any HTTP client
+
+:::note Windows
+Run the commands in this tutorial from **Git Bash** (installed with Git) or **WSL**. They use bash syntax — line continuations (`\`), quoting, and the real `curl`. In PowerShell, `curl` is an alias for a different command and won't behave the same.
+:::
+
+## Get the examples {#get-the-examples}
+
+The companion projects for this tutorial live in the [zenbpm-examples](https://github.com/pbinitiative/zenbpm-examples) repository. Clone it and move into the Getting Started track:
+
+```bash
+git clone https://github.com/pbinitiative/zenbpm-examples.git
+cd zenbpm-examples/getting-started
+```
+
+Every chapter below is a folder here, and the engine is defined once in `compose.yaml` at this level.
+
+## Start the engine {#start-the-engine}
+
+From the `getting-started/` folder:
+
+```bash
+docker compose up -d
+```
+
+The first run pulls the image and takes a moment. This starts **only** the engine (the container is named `zenbpm`); the workers are something you run yourself in later chapters. It exposes two APIs:
+
+| Address | Protocol | What you use it for |
+|---|---|---|
+| `http://localhost:8080` | REST | Deploy processes, start instances, query state, drive user tasks |
+| `localhost:9090` | gRPC | Connect workers that carry out service tasks |
+
+## Verify it's running {#verify}
+
+Ask the engine for its deployed processes:
+
+```bash
+curl http://localhost:8080/v1/process-definitions
+```
+
+An empty list (`[]` or an empty `items` array) is exactly what you want: the engine is up and answering, with nothing deployed yet. **You're ready to deploy your first process.**
+
+If the request is refused, give the engine a few seconds to finish starting and try again, or see [Troubleshooting](#troubleshooting).
+
+## Optional: web UI {#optional-ui}
+
+ZenBPM has a web UI. It's off by default; start it alongside the engine with:
+
+```bash
+docker compose --profile ui up -d
+```
+
+Then open http://localhost:9000. This tutorial uses only the terminal, but the UI is handy for watching instances.
+
+## Stop {#stop}
+
+Stop the engine but keep everything you deployed:
+
+```bash
+docker compose down
+```
+
+Engine state is kept in a named volume, so your deployed processes and instances survive a restart. To wipe it and start completely fresh:
+
+```bash
+docker compose down -v
+```
+
+## Troubleshooting {#troubleshooting}
+
+**Port already in use** (`bind: address already in use`) — another process holds `8080` or `9090`. Stop it, or change the host ports in `compose.yaml` (e.g. `18080:8080`) and use those ports in the following chapters.
+
+:::note Beyond local
+The compose file mounts a small single-node config (`conf/zenbpm/conf.yaml`) that bootstraps the engine's cluster with one partition — enough to run everything in this tutorial. That file is where cluster, partition, and script-engine settings live. Durable persistence (rqlite), multi-node clustering, and observability are operational topics covered in the [How-to guides](/category/how-to).
+:::
+
+## Next {#next}
+
+The engine is running. Next, [First BPMN process](first-bpmn-process) — deploy a process, write a worker, and watch an instance execute.