Home / Docs / Packages / @bpmnkit/worker-client

@bpmnkit/worker-client

Thin Zeebe REST client for standalone workers — no BPMNKit SDK required at runtime.

Tools Maintained, on 0.x: a minor release can break, so pin a version.

@bpmnkit/worker-client is a thin TypeScript wrapper around the Zeebe REST API. It is the only runtime dependency for scaffolded BPMNKit workers. It works with both local reebe and Camunda 8 cloud.

Installation

npm install @bpmnkit/worker-client

Quick start

import { createWorkerClient } from "@bpmnkit/worker-client"

const client = createWorkerClient()

for await (const job of client.poll("com.example:send-email:1")) {
  try {
    await sendEmail(job.variables)
    await job.complete({ sent: true })
  } catch (err) {
    await job.fail(err instanceof Error ? err.message : String(err))
  }
}

createWorkerClient(options?)

Factory function. Reads connection details from environment variables by default.

const client = createWorkerClient({
  address:      "http://localhost:26500",  // or ZEEBE_ADDRESS
  clientId:     "...",                     // or ZEEBE_CLIENT_ID
  clientSecret: "...",                     // or ZEEBE_CLIENT_SECRET
  tokenUrl:     "...",                     // or ZEEBE_TOKEN_URL
  audience:     "zeebe.camunda.io",        // or ZEEBE_TOKEN_AUDIENCE
  workerName:   "my-worker",              // sent during job activation
})

Options

OptionTypeDefaultDescription
addressstringZEEBE_ADDRESS or http://localhost:26500Zeebe REST base URL
clientIdstringZEEBE_CLIENT_IDOAuth2 client ID (Camunda SaaS)
clientSecretstringZEEBE_CLIENT_SECRETOAuth2 client secret (Camunda SaaS)
tokenUrlstringZEEBE_TOKEN_URL or Camunda SaaS endpointOAuth2 token URL
audiencestringZEEBE_TOKEN_AUDIENCE or zeebe.camunda.ioOAuth2 audience
workerNamestring"bpmnkit-worker"Worker name sent during activation

Returns

A WorkerClient object with a single poll() method.

client.poll(jobType, options?)

Async generator. Continuously polls Zeebe for jobs of the given type. Each activation request long-polls: the engine holds it open for up to requestTimeout until a job is available. When a poll comes back empty, the next one starts at least 5 seconds after it began.

Transient failures — a network error, 408, 429, a 5xx, or a token endpoint that is down — are passed to onError and retried. Anything retrying cannot fix, such as credentials the token endpoint rejects or a 401/403/400 from the engine, ends the loop: the generator throws, so a worker with a wrong secret stops with a message instead of idling forever.

for await (const job of client.poll("my-job-type", { maxJobs: 10, timeout: 60_000 })) {
  // handle job
}

Options

OptionTypeDefaultDescription
maxJobsnumber5Maximum jobs to activate per poll request
timeoutnumber300_000Activation lock timeout in milliseconds
requestTimeoutnumber20_000How long the engine may hold an activation request open (long polling), in ms; 0 uses the engine default
onError(error: Error) => voidwarning on stderrCalled with each transient error before the poll is retried
signalAbortSignal—Stops the poll: the generator returns once it aborts, even mid-request or mid-pause

Yields ActivatedJob

Each iteration yields a job with these fields and methods:

interface ActivatedJob {
  key: string                            // unique job key
  jobType: string                        // job type from BPMN task definition
  processInstanceKey: string
  bpmnProcessId: string
  elementId: string
  retries: number                        // remaining retries — use to pass to fail()
  variables: Record<string, unknown>     // process variables from the instance

  complete(variables?: Record<string, unknown>): Promise<void>
  fail(message: string, retries?: number): Promise<void>
  throwError(errorCode: string, message: string, variables?: Record<string, unknown>): Promise<void>
}

job.complete(variables?)

Completes the job, optionally returning output variables to the process instance.

await job.complete({ approved: true, reviewedAt: new Date().toISOString() })

job.fail(message, retries?)

Marks the job as failed. retries is how many retries the job has left afterwards. It defaults to job.retries - 1 (never below 0), so Zeebe retries until the task’s retries are used up and then raises an incident. Pass 0 to raise the incident at once.

await job.fail("External API returned 503")        // one retry fewer
await job.fail("Invalid customer record", 0)       // incident now: retrying will not help

job.throwError(errorCode, message, variables?)

Throws a BPMN error that can be caught by an error boundary event on the task in the diagram.

await job.throwError("PAYMENT_DECLINED", "Card declined by issuer", { code: "05" })

When the engine refuses a call

complete, fail and throwError reject when the engine answers with an error status — for example because the job has timed out and been handed to another worker, or was cancelled. The error message names the job key, the status and the response body. Catch it where you settle the job if your worker should keep going:

try {
  await job.complete({ sent: true })
} catch (err) {
  console.error(err instanceof Error ? err.message : err)
}

Before this change these calls resolved even when the engine had refused them, so a lost completion went unnoticed.

OAuth2 (Camunda SaaS)

When clientId and clientSecret are present (either via options or env vars), the client fetches an OAuth2 token before the first request and refreshes it automatically 60 seconds before expiry. No manual token management required.

ZEEBE_ADDRESS=https://your-cluster.bru-2.zeebe.camunda.io:443
ZEEBE_CLIENT_ID=abc123
ZEEBE_CLIENT_SECRET=def456
node dist/index.js

Environment variables

VariableDescription
ZEEBE_ADDRESSZeebe REST base URL
ZEEBE_CLIENT_IDOAuth2 client ID
ZEEBE_CLIENT_SECRETOAuth2 client secret
ZEEBE_TOKEN_URLOAuth2 token URL
ZEEBE_TOKEN_AUDIENCEOAuth2 audience

See also