{"id":19767258,"url":"https://github.com/camunda/camunda-8-js-sdk","last_synced_at":"2026-04-22T06:06:34.601Z","repository":{"id":210779014,"uuid":"725791900","full_name":"camunda/camunda-8-js-sdk","owner":"camunda","description":"The Camunda 8 JavaScript SDK for Node.js","archived":false,"fork":false,"pushed_at":"2026-03-05T07:28:19.000Z","size":22298,"stargazers_count":31,"open_issues_count":30,"forks_count":15,"subscribers_count":3,"default_branch":"main","last_synced_at":"2026-03-05T10:59:24.841Z","etag":null,"topics":["camunda","javascript","nodejs","typescript"],"latest_commit_sha":null,"homepage":"https://camunda.github.io/camunda-8-js-sdk/","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/camunda.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":"CONTRIBUTING.md","funding":null,"license":"LICENSE","code_of_conduct":"CODE_OF_CONDUCT.md","threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2023-11-30T22:08:11.000Z","updated_at":"2026-03-05T07:28:22.000Z","dependencies_parsed_at":"2026-03-05T08:01:03.435Z","dependency_job_id":null,"html_url":"https://github.com/camunda/camunda-8-js-sdk","commit_stats":null,"previous_names":["camunda-community-hub/camunda-8-js-sdk"],"tags_count":110,"template":false,"template_full_name":null,"purl":"pkg:github/camunda/camunda-8-js-sdk","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camunda%2Fcamunda-8-js-sdk","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camunda%2Fcamunda-8-js-sdk/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camunda%2Fcamunda-8-js-sdk/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camunda%2Fcamunda-8-js-sdk/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/camunda","download_url":"https://codeload.github.com/camunda/camunda-8-js-sdk/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/camunda%2Fcamunda-8-js-sdk/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":30326883,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-03-10T05:25:20.737Z","status":"ssl_error","status_checked_at":"2026-03-10T05:25:17.430Z","response_time":106,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["camunda","javascript","nodejs","typescript"],"created_at":"2024-11-12T04:28:46.983Z","updated_at":"2026-03-10T06:12:45.535Z","avatar_url":"https://github.com/camunda.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Camunda 8 JavaScript SDK\n\n[![NPM](https://nodei.co/npm/@camunda8/sdk.png)](https://www.npmjs.com/package/@camunda8/sdk)\n\n[![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0)\n\n[SDK API docs](https://camunda.github.io/camunda-8-js-sdk/).\n[Orchestration Cluster API Client full API docs](https://camunda.github.io/orchestration-cluster-api-js/classes/index.CamundaClient.html).\n\nThis is the official Camunda 8 JavaScript SDK. It is written in TypeScript and runs on Node.js. See why [this SDK does not run in a web browser](https://github.com/camunda/camunda-8-js-sdk/issues/79).\n\nIf you need to run an application in the web browser, then look at using the [@camunda8/orchestration-cluster-api](https://www.npmjs.com/package/@camunda8/orchestration-cluster-api) package directly.\n\nSee the [Getting Started Example](https://docs.camunda.io/docs/next/guides/getting-started-example/) in the Camunda Docs for a quick start.\n\n## Which package should I use?\n\nThis SDK provides API clients for various versions of Camunda 8. If you are doing a greenfield project on Camunda 8.8 or later, then you should consider directly using the [@camunda8/orchestration-cluster-api](https://www.npmjs.com/package/@camunda8/orchestration-cluster-api) package. That package provides a client for the Camunda 8 Orchestration Cluster API, a REST API with full functionality.\n\nThis SDK includes that client, but relying on this SDK package pulls in other API clients, which - if you are not using them - exposes you to several factors:\n\n* Increased dependency size for dependencies that are irrelevant to your application\n* Node.js only. The other package runs in the browser.\n\nHow to choose?\n\nUse the `@camunda8/sdk` package if:\n\n* You need the gRPC API to do Job Streaming.\n* Your server target is 8.7 or earlier.\n* You are progressively migrating an existing application to use the 8.8 Orchestration Cluster API.\n\nUse the `@camunda8/orchestration-cluster-api` package directly if:\n\n* You are developing a green-field application.\n* Your server target is 8.8 or later.\n* You do not need to use the gRPC API.\n\n## Which API should I use?\n\nThe SDK provides clients for several Camunda 8 APIs.\n\nIf you are doing a greenfield project on Camunda 8.8.0 or later, then you should use the Camunda Orchestration Cluster API with `getOrchestrationClusterApiClient`. This is a REST API that provides complete cluster functionality in one API surface. This client is strongly typed and provides strong types for request and response fields (eg: `ProcessInstanceKey`, `ProcessDefinitionId`, etc).\n\nTo progressively adopt this client in existing projects alongside previous clients, you can call `getOrchestrationClusterApiClientLoose` to get a client that does not strongly type the request and response fields (they remain primitive scalar `string` type).\n\n## What does \"supported\" mean?\n\nThis is the official supported-by-Camunda Nodejs SDK for Camunda Platform 8.\n\nThe Node.js SDK will not always support all features of Camunda Platform 8 immediately upon their release. Complete API coverage for a platform release will lag behind the platform release.\n\nPrioritisation of implementing features is influenced by customer demand.\n\n## Semantic versioning\n\nThe SDK package tracks Camunda Platform 8 minor versioning. Feature releases to support current Platform minor version features result in a patch release of the SDK.\n\n## Using the SDK in your project\n\nInstall the SDK as a dependency:\n\n```bash\nnpm i @camunda8/sdk\n```\n\n## Usage\n\nThe functionality of Camunda 8 is exposed via dedicated clients for the component APIs. The recommended API client for Camunda 8.8 is the Orchestration Cluster API, using the `CamundaClient` returned by `getOrchestrationClusterApiClient()`.\n\n```typescript\nimport { Camunda8 } from '@camunda8/sdk'\n\nconst c8 = new Camunda8()\n\n// Camunda 8 Orchestration Cluster API (STRICT client) - recommended from 8.8.0\n// Provides strongly typed request \u0026 response IDs (e.g. ProcessInstanceKey, ProcessDefinitionId)\nconst orchestration = c8.getOrchestrationClusterApiClient()\n\n// Camunda 8 Orchestration Cluster API (LOOSE client) - progressive adoption variant\n// Same methods, but all IDs are plain string. Start here in existing codebases, then\n// migrate to the strict client when convenient.\nconst orchestrationLoose = c8.getOrchestrationClusterApiClientLoose()\n```\n\n## Configuration\n\nThe configuration for the SDK can be done by any combination of environment variables and explicit configuration passed to the `Camunda8` constructor.\n\nThe recommended method of configuration is using the zero-conf constructor, which hydrates the configuration completely from the environment:\n\n```typescript\nconst camunda = new Camunda8()\n```\n\nThis allows you to cleanly separate the concern of configuration from your code and locate it purely in environment variable management.\n\nThe complete documentation of all configuration parameters (environment variables) can be found [here](https://camunda.github.io/camunda-8-js-sdk/variables/index.CamundaSDKConfiguration.html).\n\nAny configuration passed in to the `Camunda8` constructor is merged over any configuration in the environment. The configuration object fields and the environment variables have exactly the same names.\n\n## Orchestration Cluster API: Strict vs Loose Clients\n\nFrom Camunda 8.8 onwards the unified Orchestration Cluster API is the preferred interface. This SDK exposes it via two factory methods to enable smooth, incremental migration:\n\n| Factory | Purpose | ID / Key Types | Recommended Use |\n| ------- | ------- | -------------- | --------------- |\n| `getOrchestrationClusterApiClient()` | Strict (strongly typed) client | Branded TypeScript types (e.g. `ProcessInstanceKey`, `ProcessDefinitionId`) | Greenfield projects or code already prepared for branded types |\n| `getOrchestrationClusterApiClientLoose()` | Loose client | Plain `string` | Progressive adoption in existing code expecting raw string IDs |\n\n### Why two clients?\n\nBranded (nominal) types significantly reduce accidental mixing of unrelated IDs (for example passing a decision definition id where a process instance key is expected). However, adopting them in an existing codebase can require refactors across many modules. The loose client lets you start calling the new endpoints immediately without touching those modules. You can then migrate incrementally to the strict client as you update code.\n\n### Migration Path\n1. Introduce the loose client alongside existing v1 component clients: `const ocaLoose = c8.getOrchestrationClusterApiClientLoose()`.\n2. Replace calls to older clients (Operate / Tasklist / Optimize for read operations, Zeebe REST for workflow interactions) with equivalent Orchestration API calls using the loose client.\n3. When convenient, switch a module to the strict client: change imports to use `getOrchestrationClusterApiClient()` and address TypeScript compile errors by adopting the branded ID types (usually by propagating the type instead of `string`).\n4. Remove the loose client usage once all modules compile cleanly with the strict client.\n\n### REST Address Normalization (/v2)\n\nSet `ZEEBE_REST_ADDRESS` without the version suffix; the SDK will append `/v2` exactly once:\n\n```bash\nexport ZEEBE_REST_ADDRESS='http://localhost:8888'\n```\n\nResults in effective base URL: `http://localhost:8888/v2`.\n\nIf you include `/v2` already:\n\n```bash\nexport ZEEBE_REST_ADDRESS='http://localhost:8888/v2'\n```\n\nIt is preserved (not duplicated). Trailing slashes are stripped before normalization (`http://localhost:8888/` -\u003e `http://localhost:8888/v2`).\n\n### Choosing Between Clients\n\nUse the strict client for new code, libraries, or examples where type safety improves clarity. Use the loose client when:\n- You need a rapid drop-in replacement without changing existing function signatures.\n- You are migrating many modules gradually.\n- You want to defer adoption of branded ID types until later.\n\nBoth clients expose identical method names and behaviors; only the TypeScript surface differs. Runtime semantics and responses are otherwise equivalent.\n\n### Example (Strict vs Loose)\n\n```typescript\nimport { Camunda8 } from '@camunda8/sdk'\nconst c8 = new Camunda8()\n\n// Strict client (preferred once migrated)\nconst oca = c8.getOrchestrationClusterApiClient()\nconst proc = await oca.createProcessInstance({\n  processDefinitionId: ProcessDefinitionId.assumeExists('order-process'),\n  variables: { orderId: 'A123' },\n})\n// proc.processInstanceKey is a branded type (not a plain string)\n\n// Loose client (progressive adoption)\nconst ocaLoose = c8.getOrchestrationClusterApiClientLoose()\nconst procLoose = await ocaLoose.createProcessInstance({\n  processDefinitionId: 'order-process',\n  variables: { orderId: 'B456' },\n})\n// procLoose.processInstanceKey is a plain string\n```\n\nWhen converting code from loose to strict, remove unnecessary casts like `(key as string)` and allow the compiler to guide corrections where branded types surface.\n\n### Lifting Legacy String IDs\n\nTo migrate existing code that uses plain strings for identifiers without immediately switching every module to the strict client, you can \"lift\" those strings into branded key types incrementally.\n\nAll branded key lifter namespaces are grouped under `OrchestrationLifters` so the root SDK export surface stays tidy:\n\n```typescript\nimport { OrchestrationLifters } from '@camunda8/sdk'\n\n// Suppose you previously passed raw strings:\nconst rawProcessInstanceKey = '2251799813685249'\n\n// Lift it to the branded type (runtime value is still a string, but type safety now applies):\nconst processInstanceKey = OrchestrationLifters.ProcessInstanceKey.assumeExists(rawProcessInstanceKey)\n\n// Use with strict client\nconst c8 = new Camunda8()\nconst oca = c8.getOrchestrationClusterApiClient()\nawait oca.cancelProcessInstance({ processInstanceKey })\n```\n\nCommon helper: `assumeExists(value: string)` turns a known-valid legacy identifier into its branded equivalent. Use this when you are certain the ID refers to an existing entity (the lifter does not perform a network validation; it is purely nominal typing).\n\nSuggested incremental approach:\n1. Start by replacing output values first (e.g. responses from `createProcessInstance`) where the strict client already returns branded types.\n2. Introduce lifters at the boundaries where you still receive legacy strings (configuration, environment, persistence layer, legacy SDK modules).\n3. Remove lifters once those boundaries natively produce branded types.\n\nAll available lifters (examples): `ProcessInstanceKey`, `ProcessDefinitionId`, `JobKey`, `ElementInstanceKey`, `DecisionDefinitionKey`, `FormKey`, `UserTaskKey`, `VariableKey`, etc. See `OrchestrationLifters` export for the complete list.\n\n## A note on how int64 is handled in the JavaScript SDK\n\nEntity keys in Camunda 8.6 and earlier are stored and represented as `int64` numbers. The range of `int64` extends to numbers that cannot be represented by the JavaScript `number` type. To deal with this, `int64` keys are serialised by the SDK to the JavaScript `string` type. See [this issue](https://github.com/camunda/camunda-8-js-sdk/issues/78) for more details.\n\nFor `int64` values whose type is not known ahead of time, such as job variables, you can pass an annotated data transfer object (DTO) to decode them reliably. See the section on [Process Variable Typing](#process-variable-typing). If no DTO is specified, the default behavior of the SDK is to serialise all numbers to JavaScript `number`, and to throw an exception if a number value is detected at a runtime that cannot be accurately represented as the JavaScript `number` type (that is, a value greater than 2^53-1).\n\n## Authorization\n\nCalls to APIs can be authorized using a number of strategies. The most common is OAuth - a token that is obtained via a client id/secret pair exchange.\n\n- Camunda SaaS and Self-Managed by default are configured to use OAuth with token exchange. In most cases, you will use the `OAUTH` strategy and provide configuration only. The token exchange and its lifecycle are managed by the SDK in this strategy. This passes the token in the `authorization` header of requests.\n- If you secure the gateway behind an Nginx reverse-proxy secured with basic authentication, you will use the `BASIC` strategy. This adds the login credentials as the `authorization` header on requests.\n- For C8Run 8.7, you will need to use the `COOKIE` strategy. This manages a session cookie obtained from a login endpoint, and adds it as a cookie header to requests.\n- For more customisation, you can use the `BEARER` strategy. This is a strategy that allows you to dynamically provide the `Bearer` token directly. The currently set token is added as the `authorization` header on requests.\n- If you have a even more advanced use-case (for example, the need to add specific headers with specific values to authenticate with a proxy gateway), you can construct your own AuthProvider and pass it to the constructor.\n- You can also disable header auth completely and use mTLS (client certificate) — or no authentication at all — with the `NONE` strategy.\n\nFor more details on each of these scenarios, see the relevant section below.\n\n### Disable Auth\n\nTo disable Auth, set the environment variable `CAMUNDA_AUTH_STRATEGY=NONE`. You can use this when running against a minimal Zeebe broker in a development environment, for example. You can also use this when your authentication is being done using an x509 mTLS certificate (see the section on mTLS).\n\n### OAuth\n\nIf your platform is secured with OAuth token exchange (Camunda SaaS or Self-Managed with Identity), provide the following configuration fields at a minimum, either via the `Camunda8` constructor or in environment variables:\n\n```bash\nCAMUNDA_AUTH_STRATEGY=OAUTH\nZEEBE_GRPC_ADDRESS=...\nZEEBE_REST_ADDRESS=...\nZEEBE_CLIENT_ID=...\nZEEBE_CLIENT_SECRET=...\nCAMUNDA_OAUTH_URL=...\n```\n\nTo get a token for the Camunda SaaS Administration API or the Camunda SaaS Modeler API, set the following:\n\n```bash\nCAMUNDA_AUTH_STRATEGY=OAUTH\nCAMUNDA_CONSOLE_CLIENT_ID=...\nCAMUNDA_CONSOLE_CLIENT_SECRET=...\n```\n\n### Token caching\n\nOAuth tokens are cached in-memory and on-disk. The disk cache is useful to prevent token endpoint saturation when restarting or rolling over workers, for example. They can all hit the cache instead of requesting new tokens.\n\nYou can turn off the disk caching by setting `CAMUNDA_TOKEN_DISK_CACHE_DISABLE` to true. This will cache tokens in-memory only.\n\nBy default, the token cache directory is `$HOME/.camunda`. You can specify a different directory by providing a full file path value for `CAMUNDA_TOKEN_CACHE_DIR`.\n\nHere is an example of specifying a different cache directory via the constructor:\n\n```typescript\nimport { Camunda8 } from '@camunda8/sdk'\n\nconst c8 = new Camunda8({\n  CAMUNDA_TOKEN_CACHE_DIR: '/tmp/cache',\n})\n```\n\nIf the cache directory does not exist, the SDK will attempt to create it (recursively). If the SDK is unable to create it, or the directory exists but is not writeable by your application, the SDK will throw an exception.\n\n### Basic Auth\n\nTo use basic auth, set the following values either via the environment or explicitly in code via the constructor:\n\n```bash\nCAMUNDA_AUTH_STRATEGY=BASIC\nCAMUNDA_BASIC_AUTH_USERNAME=....\nCAMUNDA_BASIC_AUTH_PASSWORD=...\n```\n\n### Cookie Auth\n\nFor C8Run with 8.7, you need to use [Cookie Authentication](https://docs.camunda.io/docs/apis-tools/camunda-api-rest/camunda-api-rest-authentication/#authentication-via-cookie-c8run-only).\n\nTo use cookie auth, set the following value:\n\n```\nCAMUNDA_AUTH_STRATEGY=COOKIE\n\n# Optional configurable values - these are the defaults\nCAMUNDA_COOKIE_AUTH_URL=http://localhost:8080/api/login\nCAMUNDA_COOKIE_AUTH_USERNAME=demo\nCAMUNDA_COOKIE_AUTH_PASSWORD=demo\n```\n\n### Bearer Token Auth\n\nThe BEARER auth strategy is provided as a low-level primitive for advanced use-cases that are not covered by the others. In this case, you need to pass an `authorization` header with a `Bearer` token on requests, but it is not issued by a supported OAuth token exchange endpoint. In this case, you can implement a token exchange mechanism and manage the lifecycle of the token, and dynamically inject it as a header through a `BearerAuthProvider`.\n\nTo use a Bearer token that you have already obtained, and that does not need to be dynamically updated during the lifetime of the application, simply set the following values:\n\n```bash\nCAMUNDA_AUTH_STRATEGY=BEARER\nCAMUNDA_OAUTH_TOKEN=....\n```\n\nTo refresh the bearer token dynamically at runtime (for example, when it has expired and your obtain a new one), you pass in a `BearerAuthProvider` that you control:\n\n```typescript\nimport { Camunda8, Auth } from '@camunda8/sdk'\n\nconst bearerAuth = new Auth.BearerAuthProvider()\nconst c8 = new Camunda8({ oauthProvider: bearerAuth }) // All clients and workers will use bearerAuth\n// ... after obtaining a new token\nbearerAuth.setToken('SOMETOKENVALUE....') // Dynamically update the bearer token value\n```\n\n### Advanced Custom Headers\n\nYou can add arbitrary headers to all requests by implementing `IOAuthProvider`:\n\n```typescript\nimport { Camunda8, Auth } from '@camunda8/sdk'\n\nclass MyCustomAuthProvider implements Auth.IOAuthProvider {\n  async getToken(audience: string) {\n    // here we give a static example, but this class may read configuration,\n    // exchange credentials with an endpoint, manage token lifecycles, and so forth...\n    // Return an object which will be merged with the headers on the request\n  return {\n    'x-custom-auth-header': 'someCustomValue',\n    }\n  }\n}\n\nconst customAuthProvider = new MyCustomAuthProvider()\nconst c8 = new Camunda8({ oauthProvider: customAuthProvider })\n```\n\n### Camunda SaaS persistent 401 tarpit\n\nCamunda SaaS continues returning `401 Unauthorized` for a misconfigured credential \u0026 audience pair, but with a 30-second delay for subsequent requests (server-side cooldown). The SDK proactively creates a persistent \"tarpit\" marker on the first SaaS `401` to prevent network call delays.\n\nBehavior:\n\n- First `401` for `(clientId, clientSecret, audienceType)` creates `oauth-401-tarpit-\u003cclientId\u003e-\u003caudience\u003e-\u003chash\u003e.json` in the cache directory (`$HOME/.camunda` by default). `\u003chash\u003e` is a truncated PBKDF2 (100K iterations) hash of the secret.\n- Subsequent `getHeaders()` calls for that tuple immediately throw a tarpit error without hitting the token endpoint.\n- The tarpit does not auto-expire.\n\nIsolation \u0026 rotation:\n\n- Keyed on clientId + secret hash + audience; different audiences are independent.\n- Rotating the secret produces a new hash (old tarpit file can be removed manually if desired).\n\nClearing a tarpit:\n\n```typescript\nimport { Auth } from '@camunda8/sdk'\nAuth.OAuthProvider.clear401Tarpit({\n  clientId: 'myClientId',\n  clientSecret: 'currentSecret',\n  audienceType: 'ZEEBE',\n})\n```\n\nAfter clearing, the next call attempts a real token request.\n\nBackoff interaction:\n\n- Token endpoint backoff/failure counters are suppressed for tarpit 401s to surface configuration issues quickly.\n- Other error types retain normal backoff behavior.\n\nObservability:\n\n- `DEBUG=camunda:oauth` logs creation: `Created persistent 401 tarpit file ...`\n- Inspect cache dir for `oauth-401-tarpit-*` files.\n\nRemediation steps:\n\n1. Fix credential / audience configuration.\n2. Clear the tarpit file via helper (or delete manually).\n3. Retry token acquisition.\n\nThis persistent tarpit behavior is automatic for SaaS environments in this SDK version.\n\n## TLS\n\nIf you are using self-signed certificates, you can provide the self-signed certificate path using the configuration parameter / environment variable: `CAMUNDA_CUSTOM_ROOT_CERT_PATH`.\n\n### mTLS\n\nThe Zeebe gRPC client supports mTLS. You can provide the mTLS certificate and key with:\n\n```\nCAMUNDA_CUSTOM_CERT_CHAIN_PATH # path to mTLS certificate\nCAMUNDA_CUSTOM_PRIVATE_KEY_PATH # path to mTLS (client-side) key\n```\n\n## Connection configuration examples\n\n### Camunda 8 Run 8.8\n\nThis is the configuration for [Camunda 8 Run](https://developers.camunda.com/install-camunda-8/) - a local Camunda 8 instance that is the best way to get started.\n\n```bash\nexport ZEEBE_REST_ADDRESS='http://localhost:8080'\nexport ZEEBE_GRPC_ADDRESS='grpc://localhost:26500'\nexport CAMUNDA_AUTH_STRATEGY=NONE\n```\n\n### Self-Managed\n\nThis is the complete environment configuration needed to run against the Dockerised Self-Managed stack in the `docker` subdirectory:\n\n```bash\n# Self-Managed\nexport ZEEBE_GRPC_ADDRESS='grpc://localhost:26500'\nexport ZEEBE_REST_ADDRESS='http://localhost:8080'\nexport ZEEBE_CLIENT_ID='zeebe'\nexport ZEEBE_CLIENT_SECRET='zecret'\nexport CAMUNDA_OAUTH_STRATEGY='OAUTH'\nexport CAMUNDA_OAUTH_URL='http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token'\nexport CAMUNDA_TASKLIST_BASE_URL='http://localhost:8082'\nexport CAMUNDA_OPERATE_BASE_URL='http://localhost:8081'\nexport CAMUNDA_OPTIMIZE_BASE_URL='http://localhost:8083'\nexport CAMUNDA_MODELER_BASE_URL='http://localhost:8070/api'\n\n# Turn off the tenant ID, which may have been set by multi-tenant tests\n# You can set this in a constructor config, or in the environment if running multi-tenant\nexport CAMUNDA_TENANT_ID=''\n```\n\nIf you are using an OIDC that requires a `scope` parameter to be passed with the token request, set the following variable:\n\n```\nCAMUNDA_TOKEN_SCOPE\n```\n\nHere is an example of doing this via the constructor, rather than via the environment:\n\n```typescript\nimport { Camunda8 } from '@camunda8/sdk'\n\nconst c8 = new Camunda8({\n  ZEEBE_GRPC_ADDRESS: 'grpc://localhost:26500',\n  ZEEBE_REST_ADDRESS: 'http://localhost:8080',\n  ZEEBE_CLIENT_ID: 'zeebe',\n  ZEEBE_CLIENT_SECRET: 'zecret',\n  CAMUNDA_OAUTH_STRATEGY: 'OAUTH',\n  CAMUNDA_OAUTH_URL:\n    'http://localhost:18080/auth/realms/camunda-platform/protocol/openid-connect/token',\n  CAMUNDA_TASKLIST_BASE_URL: 'http://localhost:8082',\n  CAMUNDA_OPERATE_BASE_URL: 'http://localhost:8081',\n  CAMUNDA_OPTIMIZE_BASE_URL: 'http://localhost:8083',\n  CAMUNDA_MODELER_BASE_URL: 'http://localhost:8070/api',\n  CAMUNDA_TENANT_ID: '', // We can override values in the env by passing an empty string value\n  CAMUNDA_SECURE_CONNECTION: false,\n})\n```\n\n### Camunda SaaS\n\nHere is a complete configuration example for connection to Camunda SaaS:\n\n```bash\nexport ZEEBE_GRPC_ADDRESS='grpcs://5c34c0a7-7f29-4424-8414-125615f7a9b9.syd-1.zeebe.camunda.io:443'\nexport ZEEBE_REST_ADDRESS='https://syd-1.zeebe.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9'\nexport ZEEBE_CLIENT_ID='yvvURO9TmBnP3zx4Xd8Ho6apgeiZTjn6'\nexport ZEEBE_CLIENT_SECRET='iJJu-SHgUtuJTTAMnMLdcb8WGF8s2mHfXhXutEwe8eSbLXn98vUpoxtuLk5uG0en'\n\nexport CAMUNDA_TASKLIST_BASE_URL='https://syd-1.tasklist.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9'\nexport CAMUNDA_OPTIMIZE_BASE_URL='https://syd-1.optimize.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9'\nexport CAMUNDA_OPERATE_BASE_URL='https://syd-1.operate.camunda.io/5c34c0a7-7f29-4424-8414-125615f7a9b9'\nexport CAMUNDA_OAUTH_URL='https://login.cloud.camunda.io/oauth/token'\nexport CAMUNDA_AUTH_STRATEGY='OAUTH'\n\n# Admin Console and Modeler API Client\nexport CAMUNDA_CONSOLE_CLIENT_ID='e-JdgKfJy9hHSXzi'\nexport CAMUNDA_CONSOLE_CLIENT_SECRET='DT8Pe-ANC6e3Je_ptLyzZvBNS0aFwaIV'\nexport CAMUNDA_CONSOLE_BASE_URL='https://api.cloud.camunda.io'\nexport CAMUNDA_CONSOLE_OAUTH_AUDIENCE='api.cloud.camunda.io'\n```\n\n## Logging\n\nThe SDK uses a Winston / Pino compatible logging setup. By default it uses Winston.\n\nWhen using the default logging library, you can set the logging level of the SDK via the environment variable (or constructor configuration property) `CAMUNDA_LOG_LEVEL`. This defaults to 'info'. Values (in order of priority): `error`, `warn`, `info`, `http`, `verbose`, `debug`, `silly`.\n\n### Custom logger\n\nYou can supply a custom logger via the constructor. For example, to use the [Pino](https://getpino.io/) logging library:\n\n```typescript\nimport pino from 'pino'\n\nimport { Camunda8 } from '@camunda8/sdk'\n\nconst level = process.env.CAMUNDA_LOG_LEVEL ?? 'trace'\nconst logger = pino({ level }) // Logging level controlled via the logging library\n\nlogger.info('Pino logger created')\nconst c8 = new Camunda8({\n  logger,\n})\nc8.log.info('Using pino logger')\n```\n\n## Awaiting Asynchronous Query Data\n\nCamunda 8 uses an eventually-consistent data architecture. When you start a process instance, data related to this process instance is not immediately available in the datastore. This leads to data synchronisation issues you need to manage in your application. To aid you with this, the SDK provides a utility: `PollingOperation`. You can pass a query API operation to this utility with a polling interval and a timeout.\n\nThe `PollingOperation` will execute the query repeatedly until the expected data is available in the data store, or the timeout is reached.\n\nThe following example will return the query response for a newly-created process instance as soon as the element instance data is available:\n\n```typescript\nimport { Camunda8, PollingOperation } from '@camunda8/sdk'\n\nconst c8 = new Camunda8()\n\nconst elementInstances = await PollingOperation({\n  operation: () =\u003e\n  c8.searchElementInstances({\n    sort: [{ field: 'processInstanceKey' }],\n    filter: {\n      processInstanceKey: processInstance.processInstanceKey,\n      type: 'SERVICE_TASK',\n    },\n  }),\n  interval: 500,\n  timeout: 10000,\n})\n```\n\nBy default, the `PollingOperation` waits for a query response from the Orchestration Cluster API that has one or more results in the `items` array. If you have a more specific predicate, or are using one of the v1 component APIs, you can pass in a custom predicate function.\n\nThe following example waits for a process instance to be available to a query over the Operate API:\n\n```typescript\nimport { Camunda8, PollingOperation } from '@camunda8/sdk'\n\nconst c8 = new Camunda8()\nconst c = c8.getOperateApiClient()\n\nconst process = await PollingOperation({\n  operation: () =\u003e c.getProcessInstance(p.processInstanceKey),\n  predicate: (res) =\u003e res.key === p.processInstanceKey,\n  interval: 500,\n  timeout: 15000,\n})\n```\n## Subscribing to queries\n\nYou can subscribe to queries using a `QuerySubscription`. This is an event emitter that emits a `data` event when new data is available. You pass a predicate function that takes a `previous` and `current` query result set. In this predicate function you can examine the two states to see whether or not to emit a data event, and also perform data-processing — for example, removing items that were emitted in the last update.\n\nNote that this is an experimental feature and may change in future releases. We are looking for feedback on this feature, please report issues in the GitHub repo or via a JIRA ticket if you have an account with Camunda.\n\nHere is an example of using `QuerySubscription`:\n\n```typescript\nimport { Camunda8, QuerySubscription } from '@camunda8/sdk'\n\nconst c8 = new Camunda8()\n\nconst query = () =\u003e\n c8.searchProcessInstances({\n  filter: {\n    processDefinitionKey: key,\n    state: 'ACTIVE',\n  },\n  sort: [{ field: 'startDate', order: 'ASC' }],\n})\n\nconst subscription = QuerySubscription({\n  query,\n  predicate: (previous, current) =\u003e {\n    // This is the default predicate, provided here as an example\n\tconst previousItems = (previous?.items ?? []) as Array\u003cunknown\u003e\n\tconst currentItems = current.items.filter(\n\t\t(item) =\u003e\n\t\t\t!previousItems.some((prevItem) =\u003e isDeepStrictEqual(prevItem, item))\n\t)\n\tif (currentItems.length \u003e 0) {\n\t\treturn {\n\t\t\t...current,\n\t\t\titems: currentItems,\n\t\t\tpage: { ...current.page, totalItems: currentItems.length },\n\t\t}\n\t}\n\treturn false // No new items, do not emit\n  },\n  interval: 5000,\n  trackingWindow: 5, // Remember emitted items from last 5 poll cycles (default)\n})\n\nsubscription.on('data', data =\u003e {\n    // new process instances\n})\n//...\nsubscription.cancel() // close subscription and free resources\n// You can also use subscription.pause() and subscription.resume() to pause and resume the subscription\n```\n\n**Note**:\n- `QuerySubscription` uses a rolling window approach to prevent memory leaks when tracking emitted items. By default, it remembers items from the last 5 poll cycles to prevent duplicates. You can adjust this with the `trackingWindow` parameter:\n\n```typescript\nconst subscription = QuerySubscription({\n  query,\n  predicate: myCustomPredicate,\n  interval: 5000,\n  trackingWindow: 10, // Remember items from the last 10 polling cycles\n})\n```\n\nIf you implement a custom predicate, your predicate determines what data should be emitted, and then the rolling window mechanism prevents duplicate emissions across multiple poll cycles.\n\n**Important:** The rolling window mechanism operates by serializing each emitted item to a string and tracking it for the specified number of poll cycles. This means that if an entity changes state and then returns to a previous state within the tracking window timeframe, the second appearance in the original state might not trigger an emission. For example, if a process instance transitions from \"READY\" to \"NOT_READY\" and back to \"READY\" within the tracking window, the second \"READY\" state might not be emitted. If you need to track all state transitions regardless of previous states, consider:\n\n1. Setting a smaller `trackingWindow` value to reduce the tracking period\n2. Implementing a custom state tracking mechanism in your application\n3. Including a timestamp or version field in your item identification logic\n\n## Debugging\n\nThe SDK uses the [`debug`](https://github.com/debug-js/debug) library to help you debug specific issues. This produces verbose, low-level output from specific components to the console.\n\nTo enable debugging output, set a value for the `DEBUG` environment variable. The value is a comma-separated list of debugging namespaces. The SDK has the following namespaces:\n\n| Value                       | Component                                 |\n| --------------------------- | ----------------------------------------- |\n| `camunda:adminconsole`      | Administration API                        |\n| `camunda:modeler`           | Modeler API                               |\n| `camunda:operate`           | Operate API                               |\n| `camunda:optimize`          | Optimize API                              |\n| `camunda:tasklist`          | Tasklist API                              |\n| `camunda:oauth`             | OAuth Token Exchange                      |\n| `camunda:querySubscription` | QuerySubscription debugging               |\n| `camunda:grpc`              | Zeebe gRPC channel                        |\n| `camunda:worker`            | Zeebe Worker                              |\n| `camunda:worker:verbose`    | Zeebe Worker (additional detail)          |\n| `camunda:zeebeclient`       | Zeebe Client                              |\n| `camunda:orchestration-rest`              | Camunda Orchestration Cluster API Client  |\n\nHere is an example of turning on debugging for the OAuth and Operate components:\n\n```bash\nDEBUG=camunda:oauth,camunda:operate node app.js\n```\n\nThis is intended for development debugging and it should not be enabled in production. The debug trace can include sensitive information such as secrets.\n\n## Diagnostic Trace file\n\nYou can output a diagnostic trace file to use with Camunda technical support by setting the environment variable `CAMUNDA_SUPPORT_LOG_ENABLED` to true. This will output a file `camunda-support.log` containing diagnostic information and tracing calls. This information is useful for debugging. Be aware that this log file will contain secrets such as bearer tokens, as well as exposing urls.\n\n## Process Variable Typing\n\nProcess variables - the `variables` of Zeebe messages, jobs, and process instance creation requests and responses - are stored in the broker as key:value pairs. They are transported as a JSON string. The SDK parses the JSON string into a JavaScript object.\n\nVarious Zeebe methods accept DTO classes for variable input and output. These DTO classes are used to provide design-time type information on the `variables` object. They are also used to safely decode 64-bit integer values that cannot be accurately represented by the JavaScript `number` type.\n\nTo create a DTO to represent the expected shape and type of the `variables` object, extend the `LosslessDto` class:\n\n```typescript\nclass myVariableDTO extends LosslessDto {\n\tfirstName!: string\n\tlastName!: string\n\tage!: number\n\toptionalValue?: number\n\t@Int64String\n\tveryBigInteger?: string\n\tconstructor(data: Partial\u003cmyVariableDTO\u003e) {\n\t\tsuper(data)\n\t}\n}\n```\n\n## Typing of Zeebe worker variables\n\nThe variable payload in a Zeebe worker task handler is available as an object `job.variables`. By default, this is of type `any` for the gRPC API, and `unknown` for the REST API.\n\nThe `ZBClient.createWorker()` method accepts an `inputVariableDto` to control the parsing of number values and provide design-time type information. Passing an `inputVariableDto` class to a Zeebe worker is optional. If a DTO class is passed to the Zeebe worker, it is used for two purposes:\n\n- To provide design-time type information on the `job.variables` object.\n- To specify the parsing of JSON number fields. These can potentially represent `int64` values that cannot be represented accurately by the JavaScript `number` type. With a DTO, you can specify that a specific JSON number fields be parsed losslessly to a `string` or `BigInt`.\n\nWith no DTO specified, there is no design-time type safety. At run-time, all JSON numbers are converted to the JavaScript `number` type. If a variable field has a number value that cannot be safely represented using the JavaScript number type (a value greater than 2^53 -1), an exception is thrown.\n\nTo provide a DTO, extend the `LosslessDto` class like so:\n\n```typescript\nclass MyVariableDto extends LosslessDto {\n\tname!: string\n\tmaybeAge?: number\n\t@Int64String\n\tveryBigNumber!: string\n\t@BigIntValue\n\tveryBigInteger!: bigint\n}\n```\n\nIn this case, `veryBigNumber` is an `int64` value. It is transferred as a JSON number on the wire, but the parser will parse it into a `string` so that no loss of precision occurs. Similarly, `veryBigInteger` is a very large integer value. In this case, we direct the parser to parse this variable field as a `bigint`.\n\nYou can nest DTOs like this:\n\n```typescript\nclass MyLargerDto extends LosslessDto {\n\tid!: string\n\t@ChildDto(MyVariableDto)\n\tentry!: MyVariableDto\n}\n```\n\n## Typing of custom headers\n\nThe Zeebe worker receives custom headers as `job.customHeaders`. The `ZBClient.createWorker()` method accepts a `customHeadersDto` to control the behavior of custom header parsing of number values and provide design-time type information.\n\nThis follows the same strategy as the job variables, as previously described.\n\n## Zeebe User Tasks\n\nFrom 8.5, you can use Zeebe user tasks. See the documentation on [how to migrate to Zeebe user tasks](https://docs.camunda.io/docs/apis-tools/tasklist-api-rest/migrate-to-zeebe-user-tasks/).\n\nThe SDK supports the Zeebe REST API. Be sure to set the `ZEEBE_REST_ADDRESS` either via environment variable or configuration field.\n\n## Job Streaming\n\nThe Zeebe gRPC API supports streaming available jobs, rather than polling for them.\n\nThe ZeebeGrpcClient method `streamJobs` allows you to use this API.\n\nWhen `streamJobs` is called, it first opens a stream to receive any newly created jobs, then performs an initial poll (via `activateJobs`) to pick up any jobs that were already queued before the stream was established. This \"subscribe then backfill\" approach ensures no jobs are missed — the stream catches anything created after it opens, the poll catches anything that existed before, and the broker's single-activation guarantee prevents duplicates.\n\n### Sidecar polling\n\nAfter the initial backfill, a low-frequency sidecar poll runs alongside the stream as a safety net. This catches edge cases the stream alone cannot handle — for example, jobs that are re-queued after a worker fails to complete them within the timeout, or jobs that arrive during a brief stream reconnect.\n\nEach poll is a command on the broker, so the default interval is deliberately long (30 seconds) to minimise server load. The stream remains the primary delivery mechanism.\n\nConfigure the sidecar poll with:\n\n- **`pollInterval`** — milliseconds between sidecar poll cycles. Defaults to `30000` (30 s). Set to `0` or `-1` to disable recurring polling entirely (the initial backfill poll still runs).\n- **`pollMaxJobsToActivate`** — maximum jobs per poll cycle (both initial and recurring). Defaults to `32`.\n\nYou can optionally pass a `jitter` parameter (in milliseconds) to delay the start by a random period up to that value. This is useful when starting multiple stream workers simultaneously, to avoid saturating the gateway at application startup.\n\nExample:\n\n```typescript\nclient.streamJobs({\n\ttype: 'payment-service',\n\tworker: 'my-worker',\n\ttimeout: 30000,\n\ttenantIds: ['\u003cdefault\u003e'],\n\ttaskHandler: async (job) =\u003e {\n\t\t// business logic\n\t\treturn job.complete()\n\t},\n\t// Optional: stagger startup across workers\n\tjitter: 2000,\n\t// Optional: control poll batch size (default: 32)\n\tpollMaxJobsToActivate: 64,\n\t// Optional: sidecar poll every 60 s instead of the default 30 s\n\tpollInterval: 60_000,\n})\n```\n\n## Multi-tenant workers\n\nWorkers, both polling and streaming, can be multi-tenanted, requesting jobs from more than one tenant.\n\nExample:\n\n```typescript\nclient.createWorker({\n\ttaskHandler: (job) =\u003e {\n\t\tconsole.log(job.tenantId) // '\u003cdefault\u003e' | 'green'\n\t\treturn job.complete()\n\t},\n\ttaskType: 'multi-tenant-work',\n\ttenantIds: ['\u003cdefault\u003e', 'green'],\n})\n\nclient.streamJobs({\n\ttaskHandler: async (job) =\u003e {\n\t\tconsole.log(job.tenantId) // '\u003cdefault\u003e' | 'green'\n\t\treturn job.complete()\n\t},\n\ttype: 'multi-tenant-stream-work',\n\ttenantIds: ['\u003cdefault\u003e', 'green'],\n\tworker: 'stream-worker',\n\ttimeout: 2000,\n})\n```\n\n## Polling worker backoff in error conditions\n\nWhen a polling worker encounters an error, including not being authenticated, the worker will back off subsequent polls by +2 seconds with each subsequent failure, up to a maximum of `CAMUNDA_JOB_WORKER_MAX_BACKOFF_MS`, which is 15000 by default (15 seconds). If the failure is due to invalid credentials and occurs during the token request, then the worker backoff will be compounded with a token endpoint backoff, which is +1000ms for each subsequent failure up to a maximum of 15s.\n\nThis means that if you start a worker with invalid credentials, then the polling backoff will look like this, by default (times in seconds): 3, 6, 9, 12, 15, 18, 21, 23, 24, 25, 26, 27, 28, 29, 30, 30, 30...\n\nIf the worker is backing off for a reason other than invalid credentials - for example a backpressure signal from the gateway - it will be: 2, 4, 6, 8, 10, 12, 14, 15, 15, 15.....\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcamunda%2Fcamunda-8-js-sdk","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcamunda%2Fcamunda-8-js-sdk","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcamunda%2Fcamunda-8-js-sdk/lists"}