Skip to content

Standalone Workers

Workers are TypeScript programs that poll Zeebe for jobs and execute business logic. They depend only on @bpmnkit/worker-client — no BPMNKit SDK required at runtime. Run them anywhere: terminal, Docker, serverless, or via casen worker start.

The fastest way to get a worker is via the /implement skill, which scaffolds workers for every service task in a generated process. You can also scaffold manually using the MCP tool:

Call worker_scaffold with jobType: "com.example:send-invoice:1"

Or generate one directly from Claude Code:

/implement a worker that sends invoices via SendGrid

Either way, the result is a directory in ./workers/:

workers/
send-invoice/
index.ts ← implement handle() here
package.json
tsconfig.json
README.md
workers/send-invoice/index.ts
import { createWorkerClient } from "@bpmnkit/worker-client"
const JOB_TYPE = "com.example:send-invoice:1"
const WORKER_NAME = "send-invoice"
const client = createWorkerClient({ workerName: WORKER_NAME })
interface Inputs {
invoiceId: unknown // Invoice ID to send
recipientEmail: unknown // Recipient email address
}
interface Outputs {
// (no outputs defined)
}
async function handle(variables: Inputs): Promise<Outputs> {
// TODO: implement send invoice logic
throw new Error("Not implemented")
}
console.log(`[${WORKER_NAME}] polling ${JOB_TYPE}`)
for await (const job of client.poll(JOB_TYPE)) {
try {
const outputs = await handle(job.variables as Inputs)
await job.complete(outputs)
console.log(`[${WORKER_NAME}] completed ${job.key}`)
} catch (err) {
const msg = err instanceof Error ? err.message : String(err)
await job.fail(msg, job.retries - 1)
console.error(`[${WORKER_NAME}] failed ${job.key}: ${msg}`)
}
}
VariableRequiredDescription
ZEEBE_ADDRESSnoZeebe REST base URL (default: http://localhost:26500)
ZEEBE_CLIENT_IDCamunda SaaS onlyOAuth2 client ID
ZEEBE_CLIENT_SECRETCamunda SaaS onlyOAuth2 client secret
ZEEBE_TOKEN_URLnoOAuth2 token URL (default: Camunda SaaS endpoint)
ZEEBE_TOKEN_AUDIENCEnoOAuth2 audience (default: zeebe.camunda.io)

For local development with reebe, only ZEEBE_ADDRESS matters (or leave it at the default).

Terminal window
cd workers/send-invoice
npm install
npm start # runs: tsx index.ts
Terminal window
cd workers/send-invoice
npm install
npm run build # runs: tsc
npm run start:prod # runs: node dist/index.js
Terminal window
casen worker start

To start a specific worker:

Terminal window
casen worker start send-invoice

Each scaffolded worker includes a multi-stage Dockerfile recipe in its README:

FROM node:22-alpine AS build
WORKDIR /app
COPY package*.json ./
RUN npm install
COPY index.ts tsconfig.json ./
RUN npm run build
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm install --omit=dev
COPY --from=build /app/dist ./dist
CMD ["node", "dist/index.js"]

Build and run:

Terminal window
docker build -t my-org/send-invoice .
docker run -e ZEEBE_ADDRESS=http://reebe:26500 my-org/send-invoice

For Camunda 8 cloud, pass OAuth2 credentials as environment variables:

Terminal window
ZEEBE_ADDRESS=https://your-cluster.bru-2.zeebe.camunda.io:443 \
ZEEBE_CLIENT_ID=... \
ZEEBE_CLIENT_SECRET=... \
npm run start:prod

@bpmnkit/worker-client handles OAuth2 token fetching and caching automatically.

Terminal window
casen worker start --help

Or from Claude Code, call worker_list to see all built-in and scaffolded workers:

worker_list()

Built-in workers (provided by the proxy):

Job typeDescription
bpmnkit:llm:1Call an LLM and return the response text
bpmnkit:cli:1Run a shell command
bpmnkit:http:scrape:1Fetch and extract text from a URL
bpmnkit:fs:read:1Read a file from the local filesystem
bpmnkit:fs:write:1Write content to a file
bpmnkit:fs:append:1Append content to a file
bpmnkit:fs:list:1List files in a directory
bpmnkit:js:1Evaluate a JavaScript expression
bpmnkit:email:fetch:1Fetch email from an IMAP mailbox
bpmnkit:email:send:1Send email via SMTP