The @bpmnkit/api package is a fully-typed Camunda 8 REST API client. Use it to deploy
processes, start instances, and manage your cluster from Node.js scripts, backend services,
or CI/CD pipelines. The casen CLI does the same from a terminal.
Setup
baseUrl is the cluster’s Orchestration Cluster REST address, ending in /v2. Every method
path is appended to it.
SaaS (Camunda 8 Cloud)
In Camunda Hub, open Console → Clusters, pick your cluster, and on its API tab click Create new client. The credentials file it offers holds every value below.
import { CamundaClient } from "@bpmnkit/api";
const client = new CamundaClient({
// e.g. https://bru-2.zeebe.camunda.io/<cluster-id>
baseUrl: `${process.env.ZEEBE_REST_ADDRESS}/v2`,
auth: {
type: "oauth2",
clientId: process.env.ZEEBE_CLIENT_ID ?? "",
clientSecret: process.env.ZEEBE_CLIENT_SECRET ?? "",
tokenUrl: process.env.ZEEBE_AUTHORIZATION_SERVER_URL ?? "",
audience: "zeebe.camunda.io",
},
});
Self-Managed
const client = new CamundaClient({
baseUrl: "http://localhost:8080/v2",
auth: {
type: "bearer",
token: process.env.ZEEBE_TOKEN ?? "",
},
});
No Auth (local dev)
const client = new CamundaClient({
baseUrl: "http://localhost:8080/v2",
auth: { type: "none" },
});
new CamundaClient() with no arguments reads CAMUNDA_BASE_URL and the CAMUNDA_AUTH_*
variables instead.
Deploying a Process
A deployment is a multipart upload: append each file under the resources field.
import { Bpmn } from "@bpmnkit/core";
const xml = Bpmn.export(
Bpmn.createProcess("invoice-approval")
.startEvent("start")
.userTask("review", { name: "Review Invoice" })
.endEvent("end")
.withAutoLayout()
.build()
);
const form = new FormData();
form.append("resources", new Blob([xml]), "invoice-approval.bpmn");
const result = await client.resource.createDeployment(form);
console.log("Deployed version:", result.deployments[0]?.processDefinition?.processDefinitionVersion);
Starting Process Instances
const instance = await client.processInstance.createProcessInstance({
processDefinitionId: "invoice-approval",
variables: {
invoiceId: "inv-1234",
amount: 2500,
submittedBy: "[email protected]",
},
});
console.log("Instance key:", instance.processInstanceKey);
With a Specific Version
const instance = await client.processInstance.createProcessInstance({
processDefinitionId: "invoice-approval",
processDefinitionVersion: 2,
variables: { invoiceId: "inv-5678" },
});
Handling Jobs
Service tasks wait for a job worker. @bpmnkit/worker-client
long-polls for jobs. Set ZEEBE_ADDRESS to the cluster’s REST address (ZEEBE_REST_ADDRESS
from the credentials file, without /v2), plus ZEEBE_CLIENT_ID and ZEEBE_CLIENT_SECRET.
ZEEBE_TOKEN_URL defaults to the SaaS token endpoint.
import { createWorkerClient } from "@bpmnkit/worker-client";
const worker = createWorkerClient({ workerName: "email-service" });
for await (const job of worker.poll("send-email")) {
try {
await sendEmail(job.variables);
await job.complete({ sent: true, sentAt: new Date().toISOString() });
} catch (err) {
await job.fail(err instanceof Error ? err.message : String(err), job.retries - 1);
}
}
Querying Instances
// List running instances
const instances = await client.processInstance.searchProcessInstances({
filter: { processDefinitionId: "invoice-approval", state: "ACTIVE" },
});
// Get a specific instance
const instance = await client.processInstance.getProcessInstance("2251799813685249");
// Get its variables
const variables = await client.variable.searchVariables({
filter: { processInstanceKey: instance.processInstanceKey },
});
Managing Incidents
// List open incidents
const incidents = await client.incident.searchIncidents({
filter: { state: "ACTIVE" },
});
// Resolve an incident (after fixing the underlying issue)
for (const { incidentKey } of incidents.items) {
if (incidentKey) await client.incident.resolveIncident(incidentKey);
}
Lifecycle Events
Use the TypedEventEmitter to react to API events:
client.on("request", (e) => {
console.log(`→ ${e.method} ${e.url}`);
});
client.on("response", (e) => {
console.log(`← ${e.status} in ${e.durationMs}ms`);
});
client.on("error", (e) => {
metrics.increment("camunda.api.error", { url: e.url });
});
From the CLI
casen profile import reads the credentials file, so the CLI needs no flags after it:
casen profile import saas ./camunda-credentials.sh
casen profile use saas
casen deploy deploy invoice-approval.bpmn --target camunda8
casen process-instance create --data '{"processDefinitionId":"invoice-approval"}'
casen resource create-deployment <file...> deploys several files, such as a process and
its DMN tables, as one deployment.
CI/CD: Deploy on Push
A typical GitHub Actions step:
- name: Deploy BPMN processes
run: node scripts/deploy.mjs
env:
ZEEBE_REST_ADDRESS: ${{ secrets.ZEEBE_REST_ADDRESS }}
ZEEBE_CLIENT_ID: ${{ secrets.ZEEBE_CLIENT_ID }}
ZEEBE_CLIENT_SECRET: ${{ secrets.ZEEBE_CLIENT_SECRET }}
ZEEBE_AUTHORIZATION_SERVER_URL: ${{ secrets.ZEEBE_AUTHORIZATION_SERVER_URL }}
// scripts/deploy.mjs
import { CamundaClient } from "@bpmnkit/api";
import { readdir, readFile } from "node:fs/promises";
const client = new CamundaClient({
baseUrl: `${process.env.ZEEBE_REST_ADDRESS}/v2`,
auth: {
type: "oauth2",
clientId: process.env.ZEEBE_CLIENT_ID,
clientSecret: process.env.ZEEBE_CLIENT_SECRET,
tokenUrl: process.env.ZEEBE_AUTHORIZATION_SERVER_URL,
audience: "zeebe.camunda.io",
},
});
const form = new FormData();
for (const file of await readdir("./processes")) {
if (!/\.(bpmn|dmn|form)$/.test(file)) continue;
form.append("resources", new Blob([await readFile(`./processes/${file}`)]), file);
}
// One deployment: every file goes in, or none does
const result = await client.resource.createDeployment(form);
console.log(`Deployed ${result.deployments.length} resources`);