Skip to main content
Version: 8.8

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.

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.
info

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:

PrerequisiteDescription
Node.js

Get started

Get started with the Orchestration Cluster API on Camunda 8.8 and above.

  1. Create a new Node.js project that uses TypeScript:

    npm init -y
    npm install -D typescript
    npx tsc --init
  2. Install the SDK as a dependency:

    npm i @camunda8/sdk
info

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.

info

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'
caution

To set these values explicitly in code (not recommended), pass them with the same key names to the Camunda8 constructor.

Use the SDK

  1. Create a file index.ts in your IDE.

  2. Import the SDK:

    import { Camunda8 } from "@camunda8/sdk";
    import path from "path"; // we'll use this later

    const clientFactory = new Camunda8();
  3. 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:

Example BPMN process model

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.
  1. Locate the following line in the main function:

    console.log(
    `[Zeebe] Deployed process ${res.deployments[0].process.bpmnProcessId}`
    );
  2. Inside the main function, 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
  3. 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.

  1. Locate the following line in the main function:

    console.log(
    `[Camunda] serviceTaskOutcome is "${result.variables.serviceTaskOutcome}"`
    );
  2. Add the following after this line and inside the main function:

    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.