diff --git a/getting-started/01-run-the-engine/README.md b/getting-started/01-run-the-engine/README.md new file mode 100644 index 0000000..5b47abb --- /dev/null +++ b/getting-started/01-run-the-engine/README.md @@ -0,0 +1,43 @@ +# 01 · Run the engine + +Everything else in Getting Started builds on a running engine. This chapter starts one — no BPMN, no code. + +The engine is defined in the shared [`compose.yaml`](../compose.yaml) at the root of the Getting Started track. Run these commands **from the `getting-started/` folder**. + +## Start + +```bash +cd .. # into getting-started/, where compose.yaml lives +docker compose up -d +``` + +The engine exposes two APIs: + +| Address | Protocol | Used for | +|---|---|---| +| `http://localhost:8080` | REST | Deploy processes, start instances, query state | +| `localhost:9090` | gRPC | Connect workers | + +## Verify + +```bash +curl http://localhost:8080/v1/process-definitions +``` + +An empty list is the healthy response: the engine is up with nothing deployed yet. You're ready for [02-first-bpmn-process](../02-first-bpmn-process/). + +## Optional: web UI + +```bash +docker compose --profile ui up -d +``` + +Then open http://localhost:9000. + +## Stop + +```bash +docker compose down +``` + +Engine state is kept in a named volume, so it survives a restart. Use `docker compose down -v` to wipe it. diff --git a/getting-started/02-first-bpmn-process/README.md b/getting-started/02-first-bpmn-process/README.md new file mode 100644 index 0000000..7b21292 --- /dev/null +++ b/getting-started/02-first-bpmn-process/README.md @@ -0,0 +1,80 @@ +# 02 · First BPMN process + +Deploy the **first-bpmn-process**, run a **worker** (Java or Go) that handles its one service task, and watch an instance execute end to end. + +``` +[Start] --> [Log Greeting] --> [Done] + | + log-worker <- the worker (java/ or go/) in this folder +``` + +Deploying is done **manually** with `curl` (the tutorial teaches it). The worker is provided in **both Java and Go** — pick one. + +## Files + +| Path | What it is | +|---|---| +| `first-bpmn-process.bpmn` | The process definition you deploy (shared by both workers) | +| `java/` | A Java (Spring Boot) worker that handles the `log-worker` job | +| `go/` | A Go worker that handles the `log-worker` job | + +## Prerequisites + +- The engine running — from the [`getting-started/`](../) folder run `docker compose up -d` (see [01-run-the-engine](../01-run-the-engine/)) +- For the Java worker: Java 17+ and Maven — **or** for the Go worker: Go 1.22+ + +## 1. Deploy the process (manual) + +```bash +curl -X POST http://localhost:8080/v1/process-definitions \ + -F "resource=@first-bpmn-process.bpmn" +``` + +Note the returned `processDefinitionKey`. + +## 2. Run the worker + +Pick your language and leave it running. + +**Java** + +The ZenBPM Java client is on Maven Central, so no extra setup is needed: + +```bash +cd java +mvn spring-boot:run +``` + +> `java/pom.xml` uses `org.pbinitiative.zenbpm:zenbpm-spring-boot-starter:1.4.0` (the client version tracks the engine version) plus `grpc-netty-shaded` for the worker transport. + +**Go** + +```bash +cd go +go mod tidy +go run . +``` + +Either one connects to the engine and registers the `log-worker` handler. + +## 3. Start an instance + +In another terminal, using the key from step 1: + +```bash +curl -X POST http://localhost:8080/v1/process-instances \ + -H "Content-Type: application/json" \ + -d '{"processDefinitionKey": , "variables": {}}' +``` + +The worker logs: + +``` +[log-worker] Hello, World! +``` + +That greeting came out of your instance. Try starting an instance **before** the worker is running to see it park at the service task and wait — then start the worker and watch it complete. + +## Clean up + +Stop the worker (Ctrl+C), then stop the engine from the [`getting-started/`](../) folder with `docker compose down`. diff --git a/getting-started/02-first-bpmn-process/first-bpmn-process.bpmn b/getting-started/02-first-bpmn-process/first-bpmn-process.bpmn new file mode 100644 index 0000000..dc09183 --- /dev/null +++ b/getting-started/02-first-bpmn-process/first-bpmn-process.bpmn @@ -0,0 +1,58 @@ + + + + + Flow_1 + + + + + + + + + Flow_1 + Flow_2 + + + Flow_2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/getting-started/02-first-bpmn-process/go/go.mod b/getting-started/02-first-bpmn-process/go/go.mod new file mode 100644 index 0000000..81eff3b --- /dev/null +++ b/getting-started/02-first-bpmn-process/go/go.mod @@ -0,0 +1,7 @@ +module first-bpmn-process-worker + +go 1.22 + +// Dependencies are intentionally omitted here. Run `go mod tidy` to resolve them +// from the imports in main.go (pin github.com/pbinitiative/zenbpm to the version +// matching your engine). diff --git a/getting-started/02-first-bpmn-process/go/main.go b/getting-started/02-first-bpmn-process/go/main.go new file mode 100644 index 0000000..d724982 --- /dev/null +++ b/getting-started/02-first-bpmn-process/go/main.go @@ -0,0 +1,37 @@ +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" +) + +// Handles the "log-worker" service task of the first-bpmn-process: reads the +// "log" variable, prints it, and completes the job so the instance can finish. +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) { + fmt.Printf("[log-worker] %v\n", job.GetVariables()["log"]) + return map[string]any{}, nil // no output variables; job complete + }, + "log-worker", + ) + + select {} // keep the worker running +} diff --git a/getting-started/02-first-bpmn-process/java/pom.xml b/getting-started/02-first-bpmn-process/java/pom.xml new file mode 100644 index 0000000..bcd7186 --- /dev/null +++ b/getting-started/02-first-bpmn-process/java/pom.xml @@ -0,0 +1,69 @@ + + + 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 + + + + diff --git a/getting-started/02-first-bpmn-process/java/src/main/java/org/pbinitiative/examples/gettingstarted/FirstBpmnProcessWorkerApplication.java b/getting-started/02-first-bpmn-process/java/src/main/java/org/pbinitiative/examples/gettingstarted/FirstBpmnProcessWorkerApplication.java new file mode 100644 index 0000000..d7f5ffd --- /dev/null +++ b/getting-started/02-first-bpmn-process/java/src/main/java/org/pbinitiative/examples/gettingstarted/FirstBpmnProcessWorkerApplication.java @@ -0,0 +1,15 @@ +package org.pbinitiative.examples.gettingstarted; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** + * Boots the worker. The ZenBPM Spring Boot starter connects to the engine over + * gRPC on startup (see application.yml) and registers every @JobWorker bean. + */ +@SpringBootApplication +public class FirstBpmnProcessWorkerApplication { + public static void main(String[] args) { + SpringApplication.run(FirstBpmnProcessWorkerApplication.class, args); + } +} diff --git a/getting-started/02-first-bpmn-process/java/src/main/java/org/pbinitiative/examples/gettingstarted/LogWorker.java b/getting-started/02-first-bpmn-process/java/src/main/java/org/pbinitiative/examples/gettingstarted/LogWorker.java new file mode 100644 index 0000000..cd5bede --- /dev/null +++ b/getting-started/02-first-bpmn-process/java/src/main/java/org/pbinitiative/examples/gettingstarted/LogWorker.java @@ -0,0 +1,24 @@ +package org.pbinitiative.examples.gettingstarted; + +import org.springframework.stereotype.Component; +import org.pbinitiative.zenbpm.grpc.JobContext; +import org.pbinitiative.zenbpm.grpc.JobWorker; + +import java.util.Map; + +/** + * Handles the "log-worker" service task of the first-bpmn-process. + * + * When a process instance reaches the "Log Greeting" task, the engine creates a + * job of type "log-worker" and waits. This method picks it up, reads the "log" + * variable, prints it, and completes the job so the instance can finish. + */ +@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 + } +} diff --git a/getting-started/02-first-bpmn-process/java/src/main/resources/application.yml b/getting-started/02-first-bpmn-process/java/src/main/resources/application.yml new file mode 100644 index 0000000..a6b8f06 --- /dev/null +++ b/getting-started/02-first-bpmn-process/java/src/main/resources/application.yml @@ -0,0 +1,6 @@ +# Where the worker finds the engine. These match the ports from 01-run-the-engine. +zenbpm: + grpcHost: localhost + grpcPort: 9090 + grpcPlaintext: true # local/dev only; use TLS in production + jobWorkerEnabled: true # connect and register @JobWorker beans on startup diff --git a/getting-started/03-orchestrate-human-tasks/README.md b/getting-started/03-orchestrate-human-tasks/README.md new file mode 100644 index 0000000..33eb5ee --- /dev/null +++ b/getting-started/03-orchestrate-human-tasks/README.md @@ -0,0 +1,68 @@ +# 03 · Orchestrate human tasks + +Some steps wait for a **person**, not a worker. This chapter runs a one-step approval process and drives the human task over REST — no worker to write, because *you* are the one completing it. + +``` +[Request received] --> (Approve request) --> [Done] + user task +``` + +A **user task** surfaces at the engine as a job of type `user-task`. You list it, assign it, and complete it with a decision. In a real system your application (or the UI) makes these same calls on a person's behalf. + +## Files + +| Path | What it is | +|---|---| +| `approval.bpmn` | The process definition you deploy | + +There is no `java/` or `go/` folder here: a user task has no worker. The Java and Go clients *can* drive user tasks (see the docs), but the core flow is the REST calls below. + +## Prerequisites + +- The engine running — from the [`getting-started/`](../) folder run `docker compose up -d` (see [01-run-the-engine](../01-run-the-engine/)) +- `curl` + +## 1. Deploy the process (manual) + +```bash +curl -X POST http://localhost:8080/v1/process-definitions \ + -F "resource=@approval.bpmn" +``` + +Note the returned `processDefinitionKey`. + +## 2. Start an instance + +```bash +curl -X POST http://localhost:8080/v1/process-instances \ + -H "Content-Type: application/json" \ + -d '{"processDefinitionKey": , "variables": {}}' +``` + +The instance starts and **parks at the user task** — it will wait for a person indefinitely. + +## 3. Find the pending task + +```bash +curl "http://localhost:8080/v1/jobs?jobType=user-task&state=active" +``` + +Note the job `key`. + +## 4. Assign and complete it + +```bash +curl -X POST http://localhost:8080/v1/jobs//assign \ + -H "Content-Type: application/json" \ + -d '{"assignee": "john.doe"}' + +curl -X POST http://localhost:8080/v1/jobs//complete \ + -H "Content-Type: application/json" \ + -d '{"variables": {"approved": true}}' +``` + +The token advances to the end event and the instance completes. Completing with `{"approved": false}` is also a valid decision — "reject" is still a `complete`, not a `fail`. + +## Clean up + +Stop the engine from the [`getting-started/`](../) folder with `docker compose down`. diff --git a/getting-started/03-orchestrate-human-tasks/approval.bpmn b/getting-started/03-orchestrate-human-tasks/approval.bpmn new file mode 100644 index 0000000..78e7d60 --- /dev/null +++ b/getting-started/03-orchestrate-human-tasks/approval.bpmn @@ -0,0 +1,58 @@ + + + + + Flow_1 + + + + + + + + + Flow_1 + Flow_2 + + + Flow_2 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/getting-started/README.md b/getting-started/README.md new file mode 100644 index 0000000..3504a41 --- /dev/null +++ b/getting-started/README.md @@ -0,0 +1,33 @@ +# Getting Started + +Runnable companion projects for the [Getting Started tutorial](https://zenbpm.pbinitiative.org/tutorials/getting-started). Each folder maps 1:1 to a chapter of the docs. + +The engine is defined once in [`compose.yaml`](compose.yaml) at the root of this track and is shared by every chapter — start it once, from this folder. + +| Folder | Docs chapter | What it contains | +|---|---|---| +| [`01-run-the-engine/`](01-run-the-engine/) | Run the engine | How to start the shared engine (`compose.yaml`). No code. | +| [`02-first-bpmn-process/`](02-first-bpmn-process/) | First BPMN process | The `first-bpmn-process.bpmn` process and a **worker** in both Java and Go. You deploy the process manually and run the worker. | +| [`03-orchestrate-human-tasks/`](03-orchestrate-human-tasks/) | Orchestrate human tasks | The `approval.bpmn` process with a user task. You deploy it and complete the task over REST — no worker. | + +## How the pieces fit + +- **The engine** runs from `01-run-the-engine/` and is shared by every chapter. Start it once. +- **Processes** (`.bpmn`) are deployed **manually** with `curl` in each chapter — deploying is a step the tutorial teaches, so it is never automated here. +- **Workers** are small programs *you* run. They connect to the engine over gRPC and carry out service tasks. Chapter 02 ships one in both Java and Go. + +## Prerequisites + +- [Docker](https://docs.docker.com/get-docker/) with Compose +- `curl` +- For the worker: [Java](https://adoptium.net/) 17+ and [Maven](https://maven.apache.org/), **or** [Go](https://go.dev/dl/) 1.22+ + +## Quick path + +```bash +# 1. Start the engine (from this getting-started/ folder) +docker compose up -d + +# 2. Go to chapter 02 and follow its README +cd 02-first-bpmn-process +``` diff --git a/getting-started/compose.yaml b/getting-started/compose.yaml new file mode 100644 index 0000000..908c672 --- /dev/null +++ b/getting-started/compose.yaml @@ -0,0 +1,52 @@ +# ZenBPM engine — shared by every chapter of the Getting Started track. +# Run these commands from this (getting-started/) folder. +# +# Start: docker compose up -d +# Verify: curl http://localhost:8080/v1/process-definitions (empty list = healthy) +# Stop: docker compose down +# +# This runs ONLY the engine. Workers are something you write and run yourself +# (see 02-first-bpmn-process) — that is the whole point of the tutorial. The +# optional UI is off by default; enable it with: docker compose --profile ui up -d + +# Project name shown by `docker compose ls` / `docker ps` groupings. +name: zenbpm-getting-started + +services: + zenbpm: + # Pinned: :latest currently resolves to a 1.0.0 dev build whose key encoding is + # incompatible with this config (process-instance lookups fail with + # "partition not found"). v1.4.0 matches the docs and the OpenAPI examples. + image: ghcr.io/pbinitiative/zenbpm:v1.4.0 + container_name: zenbpm + environment: + # Required: without a config the engine has no partitions and process-instance + # calls fail with "partition not found". This config bootstraps a single node. + - CONFIG_FILE=/var/zenbpm/zen-conf.yaml + volumes: + - ./conf/zenbpm/conf.yaml:/var/zenbpm/zen-conf.yaml + # Persist engine state (Raft + rqlite data) so instances survive restarts. + # `docker compose down` keeps this volume; use `down -v` to wipe it. + - zenbpm-data:/node-1 + ports: + - "8080:8080" # REST API + - "9090:9090" # gRPC (workers) + healthcheck: + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/8080'"] + interval: 3s + timeout: 3s + retries: 10 + start_period: 10s + + zenbpm-ui: + image: ghcr.io/pbinitiative/zenbpm-ui:v1.4.0 + container_name: zenbpm-ui + profiles: ["ui"] # optional: docker compose --profile ui up -d + ports: + - "9000:80" # http://localhost:9000 + depends_on: + zenbpm: + condition: service_healthy + +volumes: + zenbpm-data: diff --git a/getting-started/conf/zenbpm/conf.yaml b/getting-started/conf/zenbpm/conf.yaml new file mode 100644 index 0000000..9af0432 --- /dev/null +++ b/getting-started/conf/zenbpm/conf.yaml @@ -0,0 +1,24 @@ +name: zenbpm +httpServer: + context: / + addr: :8080 +grpcServer: + addr: :9090 +cluster: + addr: localhost:8090 + adv: localhost:8090 + raft: + dir: node-1 + bootstrapExpect: 1 + bootstrapExpectTimeout: 30m + joinAttempts: 5 + joinAddresses: + - localhost:8090 + nodeId: node-1 + script: + feel: + maxVmPoolSize: 10 + minVmPoolSize: 2 + js: + maxVmPoolSize: 10 + minVmPoolSize: 2