TypeScript SDK
Use the TypeScript SDK to connect to Camunda 8, deploy process models, and work with the Orchestration Cluster API.
About this SDK
The TypeScript SDK provides typed access to Camunda 8 APIs.
- It includes IntelliSense support and works in both JavaScript and TypeScript projects.
- The Camunda 8 TypeScript SDK for Node.js is available via npm.
When to use this package
Use the @camunda8/sdk package if:
- You need to use the gRPC API for job streaming.
- Your server target is 8.7 or earlier.
- You want to migrate an existing application to the 8.8 Orchestration Cluster API.
If you are new to Camunda 8.8+ and do not need gRPC or v1 APIs, use the Orchestration Cluster API TypeScript client.
Prerequisites
The following prerequisites are required to use the TypeScript SDK:
| Prerequisite | Description |
|---|---|
| Node.js |
|
Get started
Get started with the Orchestration Cluster API on Camunda 8.8 and above.
-
Create a new Node.js project that uses TypeScript:
npm init -y
npm install -D typescript
npx tsc --init -
Install the SDK as a dependency:
npm i @camunda8/sdk
- A complete working version of the quickstart code is available on GitHub.
- For earlier versions using the v1 APIs, refer to the SDK README file.
Configure the connection
Choose one of the following configuration options:
- Explicit configuration in code
- Zero-configuration constructor with environment variables
The recommended configuration is via the zero-configuration constructor, with all values for configuration supplied via environment variables. This makes rotation, secret management, and environment promotion safer and simpler.
The environment variables you must set are outlined below. Replace these with your secrets and URLs.
To configure a client and capture these values when creating the client, see setting up client connection credentials.
Self-managed configuration
Minimal configuration:
# Self-Managed with Orchestration Cluster API
export ZEEBE_REST_ADDRESS='http://localhost:8080/v2'
With OAuth:
export ZEEBE_REST_ADDRESS='http://localhost:8080/v2'
export ZEEBE_GRPC_ADDRESS='grpc://localhost:26500'
export ZEEBE_CLIENT_ID='zeebe'
export ZEEBE_CLIENT_SECRET='zecret'
export CAMUNDA_OAUTH_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token'
If you are running with multi-tenancy enabled:
export CAMUNDA_TENANT_ID='my-tenant' # tenant <default> used by default if none set
Camunda SaaS configuration
export ZEEBE_REST_ADDRESS='5c34c0a7-...-125615f7a9b9.syd-1.zeebe.camunda.io'
export ZEEBE_GRPC_ADDRESS='grpcs://5c34c0a7-...-125615f7a9b9.syd-1.zeebe.camunda.io:443'
export ZEEBE_CLIENT_ID='yvvURO...'
export ZEEBE_CLIENT_SECRET='iJJu-SHg...'
export CAMUNDA_OAUTH_URL='https://login.cloud.camunda.io/oauth/token'
To set these values explicitly in code (not recommended), pass them with the same key names to the Camunda8 constructor.
Use the SDK
-
Create a file
index.tsin your IDE. -
Import the SDK:
import { Camunda8 } from "@camunda8/sdk";
import path from "path"; // we'll use this later
const clientFactory = new Camunda8(); -
Get an Orchestration API client. This is used to deploy process models and start process instances:
const camunda = camunda.getOrchestrationClusterApiClient();
Deploy a process model
Next, deploy a process model. Network operations are asynchronous and methods that operate over the network return promises. Wrap the main function in an async function:
async function main() {
const deployResponse = await camunda.deployResourcesFromFiles([
path.join(process.cwd(), "process.bpmn"),
]);
console.log(
`[Camunda] Deployed process ${deployResponse.processes[0].processDefinitionId}`
);
}
main(); // remember to invoke the function
Paste the process model XML below into a file named process.bpmn:
<?xml version="1.0" encoding="UTF-8"?>
<bpmn:definitions xmlns:bpmn="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI" xmlns:dc="http://www.omg.org/spec/DD/20100524/DC" xmlns:zeebe="http://camunda.org/schema/zeebe/1.0" xmlns:di="http://www.omg.org/spec/DD/20100524/DI" xmlns:modeler="http://camunda.org/schema/modeler/1.0" id="Definitions_14f3xb6" targetNamespace="http://bpmn.io/schema/bpmn" exporter="Camunda Modeler" exporterVersion="5.36.1" modeler:executionPlatform="Camunda Cloud" modeler:executionPlatformVersion="8.8.0">
<bpmn:process id="c8-sdk-demo" name="C8 SDK Demo" isExecutable="true">
<bpmn:startEvent id="StartEvent_1">
<bpmn:outgoing>Flow_0yqo0wz</bpmn:outgoing>
</bpmn:startEvent>
<bpmn:sequenceFlow id="Flow_0yqo0wz" sourceRef="StartEvent_1" targetRef="Activity_1gwbbuy" />
<bpmn:sequenceFlow id="Flow_0qugen1" sourceRef="Activity_1gwbbuy" targetRef="Activity_0tp91ve" />
<bpmn:endEvent id="Event_0j28rou">
<bpmn:incoming>Flow_03qgl0x</bpmn:incoming>
</bpmn:endEvent>
<bpmn:sequenceFlow id="Flow_03qgl0x" sourceRef="Activity_0tp91ve" targetRef="Event_0j28rou" />
<bpmn:serviceTask id="Activity_1gwbbuy" name="Service worker task">
<bpmn:extensionElements>
<zeebe:taskDefinition type="service-task" />
</bpmn:extensionElements>
<bpmn:incoming>Flow_0yqo0wz</bpmn:incoming>
<bpmn:outgoing>Flow_0qugen1</bpmn:outgoing>
</bpmn:serviceTask>
<bpmn:userTask id="Activity_0tp91ve" name="User task">
<bpmn:extensionElements>
<zeebe:userTask />
</bpmn:extensionElements>
<bpmn:incoming>Flow_0qugen1</bpmn:incoming>
<bpmn:outgoing>Flow_03qgl0x</bpmn:outgoing>
</bpmn:userTask>
</bpmn:process>
<bpmndi:BPMNDiagram id="BPMNDiagram_1">
<bpmndi:BPMNPlane id="BPMNPlane_1" bpmnElement="c8-sdk-demo">
<bpmndi:BPMNShape id="_BPMNShape_StartEvent_2" bpmnElement="StartEvent_1">
<dc:Bounds x="179" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Event_0j28rou_di" bpmnElement="Event_0j28rou">
<dc:Bounds x="592" y="99" width="36" height="36" />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1rvlo9s_di" bpmnElement="Activity_1gwbbuy">
<dc:Bounds x="270" y="77" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNShape id="Activity_1wxn0pq_di" bpmnElement="Activity_0tp91ve">
<dc:Bounds x="430" y="77" width="100" height="80" />
<bpmndi:BPMNLabel />
</bpmndi:BPMNShape>
<bpmndi:BPMNEdge id="Flow_0yqo0wz_di" bpmnElement="Flow_0yqo0wz">
<di:waypoint x="215" y="117" />
<di:waypoint x="270" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_0qugen1_di" bpmnElement="Flow_0qugen1">
<di:waypoint x="370" y="117" />
<di:waypoint x="430" y="117" />
</bpmndi:BPMNEdge>
<bpmndi:BPMNEdge id="Flow_03qgl0x_di" bpmnElement="Flow_03qgl0x">
<di:waypoint x="530" y="117" />
<di:waypoint x="592" y="117" />
</bpmndi:BPMNEdge>
</bpmndi:BPMNPlane>
</bpmndi:BPMNDiagram>
</bpmn:definitions>
For reference, this is the model you use in this example:

Run the program to deploy the process model to Camunda:
npx tsx index.ts
If your configuration is correct, you should see output similar to the following:
[Camunda] Deployed process c8-sdk-demo
Create a service worker
Outside the main function, add the following code:
console.log("Starting worker...");
const worker = camunda.createJobWorker({
jobType: "service-task",
workerName: "test-worker",
maxParallelJobs: 20,
pollIntervalMs: 1000,
pollTimeoutMs: 50_000,
jobTimeoutMs: 5000,
jobHandler: (job) => {
console.log(
`[worker]: Completing job ${job.jobKey} from process ${job.processInstanceKey}\n`
);
return job.complete({
serviceTaskOutcome: "We did it!",
});
},
});
This code starts a service task worker that runs in an asynchronous loop and invokes jobHandler when a job of type service-task becomes available.
The handler must return a job completion function such as fail, complete, error, or ignore. The type system enforces this to ensure every code path responds to Zeebe after taking a job. The job.complete function can take an object with variables to update.
Create a programmatic user task worker
The process has a user task after the service task. The service task worker completes the service task job. You complete the user task using the Tasklist API client.
Add the following code below the service worker:
// User task poller
const last = new Set<OrchestrationLifters.UserTaskKey>();
const userTaskPoller = camunda.searchUserTasks(
{
filter: {
state: "CREATED",
},
},
{
// To set up a subscription, set waitUpToMs to Infinity
consistency: {
waitUpToMs: Infinity,
pollIntervalMs: 1_000,
// predicate now becomes a polling subscription function
predicate: async (results) => {
// polling memoization - handles idempotency with eventually consistent mutation
const current = results.items.filter(
(item) => !last.has(item.userTaskKey)
);
last.clear();
results.items.forEach((task) => last.add(task.userTaskKey));
for (const userTask of current) {
console.log(
`[usertask poller]: Claiming task ${userTask.userTaskKey} from process ${userTask.processInstanceKey}\n`
);
await camunda.assignUserTask({
userTaskKey: userTask.userTaskKey,
assignee: "jwulf",
});
console.log(
`[usertask poller]: Completing user task ${userTask.userTaskKey} from process ${userTask.processInstanceKey}\n`
);
await camunda.completeUserTask({
userTaskKey: userTask.userTaskKey,
variables: {
userTaskStatus: "Got done",
},
});
}
return false; // return false to keep polling
},
},
}
);
You now have an asynchronously polling service and user task worker.
The final step is to create a process instance.
Create a process instance
There are two options for creating a process instance:
- For long-running processes, use
createProcessInstance. It returns as soon as the process instance is created with the process instance ID. - For the shorter-running process we are using, set
awaitCompletion: true. It awaits the completion of the process and returns with the final variable values.
-
Locate the following line in the
mainfunction:console.log(
`[Zeebe] Deployed process ${res.deployments[0].process.bpmnProcessId}`
); -
Inside the
mainfunction, add the following:const result = await zeebe.createProcessInstanceWithResult({
processDefinitionId,
variables: {
userTaskStatus: "Needs doing",
},
awaitCompletion: true,
});
console.log(
`[Camunda] Finished Process Instance ${result.processInstanceKey}`
);
console.log(
`[Camunda] userTaskStatus is "${result.variables.userTaskStatus}"`
);
console.log(
`[Camunda] serviceTaskOutcome is "${result.variables.serviceTaskOutcome}"`
);
worker.stop();
userTaskPoller.catch((e) => e); // Swallow cancel exception
userTaskPoller.cancel(); // Cancel poller to exit app -
Run the program with the following command:
npx tsx index.ts
You see output similar to the following:
[Camunda] Deployed process c8-sdk-demo
[worker]: Completing job 4503599632829668 from process 4503599632829662
[usertask poller]: Claiming task 4503599632829678 from process 4503599632829662
[usertask poller]: Completing user task 4503599632829678 from process 4503599632829662
[Camunda] Finished Process Instance 4503599632829662
[Camunda] userTaskStatus is "Got done"
[Camunda] serviceTaskOutcome is "We did it!"
The program continues running until you press Ctrl+C because both the service worker and the user task poller run in continuous loops.
To explore more SDK functionality, use the examples below.
Retrieve a process instance
When you create a long-running process instance, you typically use createProcessInstance and get back the process instance key of the running process immediately instead of waiting for it to complete.
To examine the process instance status, use the process instance key to query the Operate API. You can also check completed process instances in the same way. In the following example, you query the process instance created earlier.
-
Locate the following line in the
mainfunction:console.log(
`[Camunda] serviceTaskOutcome is "${result.variables.serviceTaskOutcome}"`
); -
Add the following after this line and inside the
mainfunction:const historicalProcessInstance = await camunda.getProcessInstance(
{
processInstanceKey: result.processInstanceKey,
},
{ consistency: { waitUpToMs: 5000 } }
);
console.log("[Camunda]", JSON.stringify(historicalProcessInstance, null, 2));
When you run the program now, you should see additional output similar to the following:
{
processInstanceKey: 4503599632829662,
processVersion: 1,
processDefinitionId: 'c8-sdk-demo',
startDate: '2025-11-08T09:11:06.157+0000',
endDate: '2025-11-08T09:11:12.403+0000',
state: 'COMPLETED',
processDefinitionKey: 2251799814900879,
}
The state may appear as ACTIVE rather than COMPLETED. This happens because the data read over the API is historical data from the Zeebe exporter, and lags behind the actual state of the system. It is eventually consistent.
Further resources
See the complete API documentation for the SDK and the Orchestration Cluster API client.