Skip to content

Getting Started

Prerequisites

Tool Version Purpose
Go 1.22+ Build the engine and Go SDK
Docker + Docker Compose 24+ Run PostgreSQL
protoc 3.21+ Regenerate gRPC/proto code
protoc-gen-go latest Go proto plugin
protoc-gen-go-grpc latest Go gRPC plugin
Python 3.10+ Python SDK
Node.js 20+ Node/TypeScript SDK
Java 21+ Java SDK
Gradle 8+ (wrapper included) Java SDK build

Install proto plugins

# macOS
brew install protobuf

# Ubuntu / Debian
sudo apt install -y protobuf-compiler

# Go plugins
go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest

Quick Start — Docker Compose (no clone required)

The fastest way to get the engine running locally:

curl -O https://raw.githubusercontent.com/batnam/rochallor-engine/main/deploy/docker-compose.quickstart.yml
docker compose -f docker-compose.quickstart.yml up -d

Services started:

Service URL
Workflow Modeller http://localhost:3000
Engine REST API http://localhost:8080
gRPC localhost:9090
Prometheus metrics http://localhost:9091/metrics

PostgreSQL data persists in a named Docker volume.

Then install an SDK and jump to Step 5 — Upload a definition.


Quick Start (from source)

Steps 1–3 are the same regardless of which SDK you use.

1. Clone and start PostgreSQL

git clone https://github.com/batnam/rochallor-engine.git
cd rochallor-engine

# Start PostgreSQL (workflow/workflow on port 5432)
docker compose -f workflow-engine/docker-compose.yml up -d

2. Generate proto code

cd workflow-engine
make proto-gen

3. Start the engine

export WE_POSTGRES_DSN="postgres://workflow:workflow@localhost:5432/workflow?sslmode=disable"
make run
# Engine listens on :8080 (REST), :9090 (gRPC), :9091 (metrics)

4. Install the SDK

Python

pip install rochallor-sdk

Python SDK supports REST only.

Go

go get github.com/batnam/rochallor-engine/workflow-sdk-go@latest

Node / TypeScript

npm install rochallor-workflow-sdk-node

Java (Maven Central)

dependencies {
    implementation("io.github.batnam:workflow-sdk-java:1.0.0")
}

5. Upload a workflow definition

Python

# upload_greet.py
from workflow_sdk.client.rest import RestEngineClient

client = RestEngineClient("http://localhost:8080")
definition = {
    "id": "greet-workflow",
    "name": "Greet Workflow",
    "steps": [
        {
            "id": "say-hello",
            "name": "Say Hello",
            "type": "SERVICE_TASK",
            "jobType": "greet",
            "nextStep": "end"
        },
        {"id": "end", "name": "End", "type": "END"}
    ]
}
result = client.upload_definition(definition)
print(f"Uploaded: {result['id']} v{result['version']}")

Go

// upload_greet.go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    definition := map[string]any{
        "id":   "greet-workflow",
        "name": "Greet Workflow",
        "steps": []any{
            map[string]any{
                "id": "say-hello", "name": "Say Hello",
                "type": "SERVICE_TASK", "jobType": "greet", "nextStep": "end",
            },
            map[string]any{"id": "end", "name": "End", "type": "END"},
        },
    }
    body, _ := json.Marshal(definition)
    resp, err := http.Post("http://localhost:8080/v1/definitions", "application/json", bytes.NewReader(body))
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    var result map[string]any
    json.NewDecoder(resp.Body).Decode(&result)
    fmt.Printf("Uploaded: %s v%.0f\n", result["id"], result["version"])
}

Node / TypeScript

// upload_greet.ts
const resp = await fetch('http://localhost:8080/v1/definitions', {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
        id:    'greet-workflow',
        name:  'Greet Workflow',
        steps: [
            { id: 'say-hello', name: 'Say Hello', type: 'SERVICE_TASK',
              jobType: 'greet', nextStep: 'end' },
            { id: 'end', name: 'End', type: 'END' },
        ],
    }),
})
const result = await resp.json()
console.log(`Uploaded: ${result.id} v${result.version}`)

Java

// UploadGreet.java
import com.batnam.workflow.sdk.client.RestEngineClient;
import java.util.List;
import java.util.Map;

public class UploadGreet {
    public static void main(String[] args) throws Exception {
        var client = new RestEngineClient("http://localhost:8080");

        var definition = Map.of(
            "id",   "greet-workflow",
            "name", "Greet Workflow",
            "steps", List.of(
                Map.of("id", "say-hello", "name", "Say Hello",
                       "type", "SERVICE_TASK", "jobType", "greet", "nextStep", "end"),
                Map.of("id", "end", "name", "End", "type", "END")
            )
        );
        var result = client.uploadDefinition(definition);
        System.out.println("Uploaded: " + result.get("id") + " v" + result.get("version"));
    }
}

6. Run a worker

Python

# worker.py
import signal, threading
from workflow_sdk.client.rest import RestEngineClient
from workflow_sdk.handler.registry import HandlerRegistry
from workflow_sdk.runner.runner import Runner

client   = RestEngineClient("http://localhost:8080")
registry = HandlerRegistry()

@registry.register("greet")
def handle_greet(ctx):
    name = ctx["variables"].get("name", "World")
    print(f"Hello, {name}!")
    return {"greeting": f"Hello, {name}!"}

stop = threading.Event()
signal.signal(signal.SIGINT, lambda *_: stop.set())

runner = Runner(client=client, registry=registry, worker_id="py-worker-1")
runner.run(stop_event=stop)

Go

// worker.go
package main

import (
    "context"
    "fmt"
    "os/signal"
    "syscall"

    "github.com/batnam/rochallor-engine/workflow-sdk-go/client"
    "github.com/batnam/rochallor-engine/workflow-sdk-go/handler"
    "github.com/batnam/rochallor-engine/workflow-sdk-go/runner"
)

func main() {
    engine   := client.NewRest("http://localhost:8080", "go-worker-1")
    registry := handler.New()

    registry.Register("greet", func(ctx context.Context, job handler.JobContext) (handler.Result, error) {
        name, _ := job.Variables["name"].(string)
        if name == "" {
            name = "World"
        }
        fmt.Printf("Hello, %s!\n", name)
        return handler.Result{VariablesToSet: map[string]any{"greeting": "Hello, " + name + "!"}}, nil
    })

    r := runner.New(runner.Config{WorkerID: "go-worker-1"}, engine, registry)

    ctx, stop := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
    defer stop()

    r.Run(ctx) // blocks until SIGINT / SIGTERM
}

Swap client.NewRest for client.NewGrpc("localhost:9090", "go-worker-1") to use gRPC transport.

Node / TypeScript

// worker.ts
import { RestEngineClient } from './src/client/rest.js'
import { HandlerRegistry }   from './src/handler/registry.js'
import { Runner }            from './src/runner/runner.js'

const engine   = new RestEngineClient('http://localhost:8080')
const registry = new HandlerRegistry()

registry.register('greet', async ctx => {
    const name = (ctx.variables['name'] as string) ?? 'World'
    console.log(`Hello, ${name}!`)
    return { variablesToSet: { greeting: `Hello, ${name}!` } }
})

const controller = new AbortController()
process.on('SIGINT',  () => controller.abort())
process.on('SIGTERM', () => controller.abort())

const runner = new Runner(engine, registry, { workerId: 'node-worker-1' })
await runner.run(controller.signal)

Swap RestEngineClient for GrpcEngineClient('localhost:9090', grpc.credentials.createInsecure()) to use gRPC transport.

Java

// Worker.java
import com.batnam.workflow.sdk.client.RestEngineClient;
import com.batnam.workflow.sdk.handler.HandlerRegistry;
import com.batnam.workflow.sdk.runner.Runner;
import java.util.Map;

public class Worker {
    public static void main(String[] args) throws InterruptedException {
        var client   = new RestEngineClient("http://localhost:8080");
        var registry = new HandlerRegistry();

        registry.register("greet", ctx -> {
            String name = (String) ctx.variables().getOrDefault("name", "World");
            System.out.println("Hello, " + name + "!");
            return Map.of("greeting", "Hello, " + name + "!");
        });

        // parallelism=64, pollIntervalMs=500
        var runner = new Runner("java-worker-1", 64, 500, client, registry);
        runner.start();

        // Block main thread — JVM shutdown hook handles graceful drain
        Thread.currentThread().join();
    }
}

Swap RestEngineClient for GrpcEngineClient("localhost:9090") to use gRPC transport.


7. Start a workflow instance

Python

# start_instance.py
from workflow_sdk.client.rest import RestEngineClient

client   = RestEngineClient("http://localhost:8080")
instance = client.start_instance("greet-workflow", variables={"name": "Alice"})
print(f"Started instance: {instance['id']}")

Go

// start_instance.go
package main

import (
    "bytes"
    "encoding/json"
    "fmt"
    "net/http"
)

func main() {
    body, _ := json.Marshal(map[string]any{
        "variables": map[string]any{"name": "Alice"},
    })
    resp, err := http.Post(
        "http://localhost:8080/v1/definitions/greet-workflow/instances",
        "application/json",
        bytes.NewReader(body),
    )
    if err != nil {
        panic(err)
    }
    defer resp.Body.Close()
    var instance map[string]any
    json.NewDecoder(resp.Body).Decode(&instance)
    fmt.Println("Started instance:", instance["id"])
}

Node / TypeScript

// start_instance.ts
const resp = await fetch('http://localhost:8080/v1/definitions/greet-workflow/instances', {
    method:  'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ variables: { name: 'Alice' } }),
})
const instance = await resp.json()
console.log('Started instance:', instance.id)

Java

// StartInstance.java
import com.batnam.workflow.sdk.client.RestEngineClient;
import java.util.Map;

public class StartInstance {
    public static void main(String[] args) throws Exception {
        var client   = new RestEngineClient("http://localhost:8080");
        var instance = client.startInstance("greet-workflow", Map.of("name", "Alice"), null, null);
        System.out.println("Started instance: " + instance.get("id"));
    }
}

Next steps

Topic Doc
Full SDK reference — Python docs/sdk/python.md
Full SDK reference — Go docs/sdk/go.md
Full SDK reference — Node/TypeScript docs/sdk/node.md
Full SDK reference — Java docs/sdk/java.md
Workflow definition format docs/workflow-format.md
Architecture overview docs/architecture.md
Local development guide docs/development.md