@camunda8/orchestration-cluster-api 8.9.0-alpha.9 → 9.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,5 +1,12 @@
1
1
  # Camunda 8 Orchestration Cluster TypeScript SDK
2
2
 
3
+ [![npm](https://img.shields.io/npm/v/@camunda8/orchestration-cluster-api)](https://www.npmjs.com/package/@camunda8/orchestration-cluster-api)
4
+ [![npm downloads](https://img.shields.io/npm/dw/@camunda8/orchestration-cluster-api)](https://www.npmjs.com/package/@camunda8/orchestration-cluster-api)
5
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue.svg)](https://github.com/camunda/orchestration-cluster-api-js/blob/main/LICENSE)
6
+ [![GitHub release](https://img.shields.io/github/v/release/camunda/orchestration-cluster-api-js)](https://github.com/camunda/orchestration-cluster-api-js/releases)
7
+
8
+ <!-- WARNING: The content and specific structure of this file drives Docusaurus generation in camunda-docs. Also, code examples are injected during build. Please refer to MAINTAINER.md before editing. -->
9
+
3
10
  Type‑safe, promise‑based client for the Camunda 8 Orchestration Cluster REST API.
4
11
 
5
12
  ## Highlights
@@ -13,7 +20,8 @@ Type‑safe, promise‑based client for the Camunda 8 Orchestration Cluster REST
13
20
  - Eventual consistency helper for polling endpoints
14
21
  - Immutable, deep‑frozen configuration accessible through a factory‑created client instance
15
22
  - Automatic body-level tenantId defaulting: if a request body supports an optional tenantId and you omit it, the SDK fills it from CAMUNDA_DEFAULT_TENANT_ID (path params are never auto-filled)
16
- - Automatic transient HTTP retry (429, 503, network) with exponential backoff + full jitter (configurable via CAMUNDA_SDK_HTTP_RETRY\*). Non-retryable 500s fail fast. Pluggable strategy surface (default uses p-retry when available, internal fallback otherwise).
23
+ - Automatic transient HTTP retry (429, 503, network) with exponential backoff + full jitter (configurable via CAMUNDA_SDK_HTTP_RETRY\*). Non-retryable 500s fail fast.
24
+ - Per-method retry override: disable or customize retry policy on any individual API call without changing global settings
17
25
 
18
26
  ## Install
19
27
 
@@ -28,27 +36,129 @@ Runtime support:
28
36
 
29
37
  For older Node versions supply a fetch ponyfill AND a `File` shim (or upgrade). For legacy browsers, add a fetch polyfill (e.g. `whatwg-fetch`).
30
38
 
31
- ## Versioning
39
+ ### Versioning
40
+
41
+ This SDK has a different release cadence from the Camunda server. Features and fixes land in the SDK during a server release.
42
+
43
+ The major version of the SDK signals a 1:1 type coherence with the server API for a Camunda minor release.
44
+
45
+ SDK version `n.y.z` -> server version `8.n`, so the type surface of SDK version 9.y.z matches the API surface of Camunda 8.9.
46
+
47
+ Using a later SDK version, for example: SDK version 10.y.z with Camunda 8.9, means that the SDK contains additive surfaces that are not guaranteed at runtime, and the compiler cannot warn of unsupported operations.
48
+
49
+ Using an earlier SDK version, for example: SDK version 9.y.z with Camunda 8.10, results in slightly degraded compiler reasoning: exhaustiveness checks cannot be guaranteed by the compiler for any extended surfaces (principally, enums with added members).
50
+
51
+ In the vast majority of use-cases, this will not be an issue; but you should be aware that using the matching SDK major version for the server minor version provides the strongest compiler guarantees about runtime reliability.
52
+
53
+ **Recommended approach**:
54
+
55
+ - Check the [CHANGELOG](https://github.com/camunda/orchestration-cluster-api-js/releases).
56
+ - As a sanity check during server version upgrade, rebuild applications with the matching SDK major version to identify any affected runtime surfaces.
57
+
58
+ ## Migrating from 8.8
59
+
60
+ SDK 9.x (for Camunda 8.9) introduces two categories of breaking type changes relative to SDK 8.x (for Camunda 8.8). Neither change affects runtime behavior — existing code that compiled against 8.x will run identically — but the compiler will flag type mismatches until you update.
61
+
62
+ ### Search results: optional → required fields
63
+
64
+ The search result types changed several fields from **optional** to **required**:
65
+
66
+ **`SearchQueryPageResponse` (page metadata)**
67
+
68
+ | Field | SDK 8.x (Camunda 8.8) | SDK 9.x (Camunda 8.9) |
69
+ |-------|----------------------|----------------------|
70
+ | `totalItems` | `totalItems?: number` | `totalItems: number` |
71
+ | `hasMoreTotalItems` | `hasMoreTotalItems?: boolean` | `hasMoreTotalItems: boolean` |
72
+ | `endCursor` | `endCursor?: EndCursor` | `endCursor: EndCursor \| null` |
73
+ | `startCursor` | `startCursor?: StartCursor` | `startCursor: StartCursor \| null` |
74
+
75
+ **`*SearchQueryResult` types (result containers)**
76
+
77
+ | Field | SDK 8.x (Camunda 8.8) | SDK 9.x (Camunda 8.9) |
78
+ |-------|----------------------|----------------------|
79
+ | `items` | `items?: T[]` | `items: T[]` |
80
+ | `page` | `page?: SearchQueryPageResponse` | `page: SearchQueryPageResponse` |
81
+
82
+ This reflects upstream OpenAPI spec changes where these fields are now always present in the response, with `null` indicating "no value" for cursors rather than being absent.
83
+
84
+ **What to change**: Update code that checks for these fields using optional chaining or `undefined` comparisons. The `items` array and `page` object are now always present, so optional chaining on them is unnecessary:
85
+
86
+ <!-- snippet-exempt: migration example showing before/after patterns -->
87
+
88
+ ```ts
89
+ // Before (8.x) — checking for undefined
90
+ if (result.page?.endCursor !== undefined) {
91
+ nextPage(result.page.endCursor);
92
+ }
93
+ const count = result.items?.length ?? 0;
94
+
95
+ // After (9.x) — page and items are always present; check cursors for null
96
+ if (result.page.endCursor !== null) {
97
+ nextPage(result.page.endCursor);
98
+ }
99
+ const count = result.items.length;
100
+ ```
101
+
102
+ If you have a custom `PagedResponse` type, update its `page` shape to match:
103
+
104
+ <!-- snippet-exempt: migration example showing type definition update -->
105
+
106
+ ```ts
107
+ // Before (8.x)
108
+ type PagedResponse<T> = {
109
+ items?: T[];
110
+ page?: {
111
+ totalItems?: number;
112
+ endCursor?: string;
113
+ startCursor?: string;
114
+ hasMoreTotalItems?: boolean;
115
+ };
116
+ };
117
+
118
+ // After (9.x)
119
+ type PagedResponse<T> = {
120
+ items: T[];
121
+ page: {
122
+ totalItems: number;
123
+ endCursor: EndCursor | null;
124
+ startCursor: StartCursor | null;
125
+ hasMoreTotalItems: boolean;
126
+ };
127
+ };
128
+ ```
32
129
 
33
- This SDK does **not** follow traditional semver. The **major.minor** version tracks the Camunda server version, so you can easily match the SDK to your deployment target (e.g. SDK `8.9.x` targets Camunda `8.9`).
130
+ ### Branded key types for `tenantId`
34
131
 
35
- **Patch releases** contain fixes, features, and occasionally **breaking type changes**. A breaking type change typically means an upstream API definition fix that corrects the shape of a request or response model your code may stop type-checking even though it worked before.
132
+ The `tenantId` field on request types (e.g. `CreateDeploymentData`) changed from `string` to the branded `TenantId` type. A plain `string` is no longer assignable:
36
133
 
37
- When this happens, we signal it in the [CHANGELOG](https://github.com/camunda/orchestration-cluster-api-js/releases).
134
+ <!-- snippet-exempt: migration example showing branded type usage -->
38
135
 
39
- **Recommended approach:**
136
+ ```ts
137
+ import { TenantId } from '@camunda8/orchestration-cluster-api';
138
+
139
+ // Before (8.x) — plain string worked
140
+ await camunda.createDeployment({
141
+ tenantId: 'my-tenant',
142
+ resources: [file],
143
+ });
144
+
145
+ // After (9.x) — use the branded type helper
146
+ await camunda.createDeployment({
147
+ tenantId: TenantId.assumeExists('my-tenant'),
148
+ resources: [file],
149
+ });
150
+ ```
40
151
 
41
- - **Ride the latest** accept that types may shift and update your code when it happens. This keeps you on the most accurate API surface.
42
- - **Pin and review** — pin to a specific patch version in `package.json` and review the [CHANGELOG](https://github.com/camunda/orchestration-cluster-api-js/releases) before upgrading:
152
+ `TenantId.assumeExists()` validates the string against the tenant ID pattern and brands it at zero runtime cost. See [Branded Keys](#branded-keys) for more on this pattern.
43
153
 
44
- ```json
45
- "@camunda8/orchestration-cluster-api": "8.9.3"
46
- ```
154
+ > **Tip**: If your tenant ID comes from a validated source (environment variable, config file), call `TenantId.assumeExists()` once at startup and pass the branded value throughout your application.
47
155
 
48
156
  ## Quick Start (Zero‑Config – Recommended)
49
157
 
50
158
  Keep configuration out of application code. Let the factory read `CAMUNDA_*` variables from the environment (12‑factor style). This makes rotation, secret management, and environment promotion safer & simpler.
51
159
 
160
+ <!-- snippet-source: examples/readme-imports.txt,examples/readme.ts | regions: ReadmeDefaultImport+ReadmeQuickStart -->
161
+
52
162
  ```ts
53
163
  import createCamundaClient from '@camunda8/orchestration-cluster-api';
54
164
 
@@ -89,6 +199,8 @@ CAMUNDA_SDK_HTTP_RETRY_MAX_DELAY_MS=2000 # optional: cap (ms)
89
199
 
90
200
  Use only when you must supply or mutate configuration dynamically (e.g. multi‑tenant routing, tests, ephemeral preview environments) or in the browser. Keys mirror their `CAMUNDA_*` env names.
91
201
 
202
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeOverrides -->
203
+
92
204
  ```ts
93
205
  const camunda = createCamundaClient({
94
206
  config: {
@@ -104,6 +216,8 @@ const camunda = createCamundaClient({
104
216
 
105
217
  Inject a custom `fetch` to add tracing, mock responses, instrumentation, circuit breakers, etc.
106
218
 
219
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeCustomFetch -->
220
+
107
221
  ```ts
108
222
  const camunda = createCamundaClient({
109
223
  fetch: (input, init) => {
@@ -149,15 +263,65 @@ Behavior:
149
263
  - `strict` - fail on type mismatch or missing required fields
150
264
  - `fanatical` - fail on type mismatch, missing required fields, or unknown additional fields
151
265
 
266
+ > **Note on `int64` fields**: The upstream OpenAPI spec declares some fields (e.g. `totalItems`, `timeout`, `timestamp`) as `integer` with `format: int64`. The TypeScript types map these to `number`. JSON responses also deserialize as `number` (with precision loss beyond `Number.MAX_SAFE_INTEGER`). The Zod schemas use `z.coerce.number().int()` for these fields, preserving the integer constraint while keeping the runtime type aligned with TypeScript. All validation modes (`none`, `warn`, `strict`, `fanatical`) return `number`.
267
+
268
+ ## Per-Method Retry Override
269
+
270
+ Every API method accepts an optional trailing `options` parameter that lets you override or disable the global retry policy for that single call.
271
+
272
+ ### Disable Retry for a Single Call
273
+
274
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeDisableRetry -->
275
+
276
+ ```ts
277
+ // This call will not retry on transient errors
278
+ await camunda.completeJob({ jobKey }, { retry: false });
279
+ ```
280
+
281
+ ### Override Specific Retry Settings
282
+
283
+ Pass a partial `HttpRetryPolicy` to override individual fields. Unspecified fields inherit from the global configuration.
284
+
285
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeRetryOverride -->
286
+
287
+ ```ts
288
+ // More aggressive retry for this operation only
289
+ await camunda.createProcessInstance(
290
+ { processDefinitionId },
291
+ { retry: { maxAttempts: 8, maxDelayMs: 5000 } }
292
+ );
293
+
294
+ // Minimal retry: single retry with short backoff
295
+ await camunda.getTopology({ retry: { maxAttempts: 2, baseDelayMs: 50 } });
296
+ ```
297
+
298
+ ### How It Works
299
+
300
+ | `options.retry` value | Behavior |
301
+ | --------------------- | -------------------------------------------------------------------- |
302
+ | omitted / `undefined` | Uses global policy (`CAMUNDA_SDK_HTTP_RETRY_*` env vars) |
303
+ | `false` | Disables retry entirely (single attempt, no backoff) |
304
+ | `{ maxAttempts: 5 }` | Merges with global policy — only the specified fields are overridden |
305
+
306
+ The `HttpRetryPolicy` fields available for override:
307
+
308
+ | Field | Type | Description |
309
+ | ------------- | -------- | --------------------------------------- |
310
+ | `maxAttempts` | `number` | Total attempts (initial + retries) |
311
+ | `baseDelayMs` | `number` | Base delay for exponential backoff (ms) |
312
+ | `maxDelayMs` | `number` | Maximum delay cap (ms) |
313
+
152
314
  ## Advanced HTTP Retry: Cockatiel Adapter (Optional)
153
315
 
154
- The SDK includes built‑in transient HTTP retry (429, 503, network errors) using a p‑retry based engine plus a fallback implementation. For advanced resilience patterns (circuit breakers, timeouts, custom classification, combining policies) you can integrate [cockatiel](https://github.com/connor4312/cockatiel).
316
+ For advanced resilience patterns beyond per-method overrides circuit breakers, timeouts, custom classification, combining policies you can integrate [cockatiel](https://github.com/connor4312/cockatiel).
317
+
318
+ > **Tip:** For most use cases, per-method retry override (above) is sufficient. Reach for Cockatiel when you need circuit breaking, hedging, or bulkhead controls.
155
319
 
156
320
  ### When To Use Cockatiel
157
321
 
158
- - You need different retry policies per operation (e.g. idempotent GET vs mutating POST)
159
322
  - You want circuit breaking, hedging, timeout, or bulkhead controls
160
323
  - You want to add custom classification (e.g. retry certain 5xx only on safe verbs)
324
+ - You need to compose multiple resilience policies together
161
325
 
162
326
  ### Disable Built‑In HTTP Retries
163
327
 
@@ -165,6 +329,7 @@ Set `CAMUNDA_SDK_HTTP_RETRY_MAX_ATTEMPTS=1` so the SDK does only the initial att
165
329
 
166
330
  ### Minimal Example (Single Operation)
167
331
 
332
+ <!-- snippet-exempt: uses external cockatiel library -->
168
333
  ```ts
169
334
  import { createCamundaClient } from '@camunda8/orchestration-cluster-api';
170
335
  import { retry, ExponentialBackoff, handleAll } from 'cockatiel';
@@ -193,6 +358,7 @@ console.log(topo.brokers?.length);
193
358
 
194
359
  ### Bulk Wrapping All Operations
195
360
 
361
+ <!-- snippet-exempt: uses external cockatiel library -->
196
362
  ```ts
197
363
  import { createCamundaClient } from '@camunda8/orchestration-cluster-api';
198
364
  import { retry, ExponentialBackoff, handleAll } from 'cockatiel';
@@ -271,6 +437,7 @@ Refer to `./docs/CONFIG_REFERENCE.md` for the full list of related environment v
271
437
 
272
438
  Retry only network errors + 429/503, plus optionally 500 on safe GET endpoints you mark:
273
439
 
440
+ <!-- snippet-exempt: uses external cockatiel library -->
274
441
  ```ts
275
442
  import { retry, ExponentialBackoff, handleWhen } from 'cockatiel';
276
443
 
@@ -292,7 +459,7 @@ const policy = retry(classify, {
292
459
  - Keep SDK retries disabled to prevent duplicate layers.
293
460
  - SDK synthesizes `Error` objects with a `status` for retry-significant HTTP responses (429, 503, 500), enabling classification.
294
461
  - You can tag errors (e.g. assign `err.__opVerb`) in a wrapper if verb-level logic is needed.
295
- - Future improvement: an official `retryStrategy` injection hook—current approach is non-invasive.
462
+ - For per-operation retry customization without external dependencies, use the built-in [per-method retry override](#per-method-retry-override) instead.
296
463
 
297
464
  > Combine cockatiel retry with a circuit breaker, timeout, or bulkhead policy for more robust behavior in partial outages.
298
465
 
@@ -394,12 +561,29 @@ Factors use integer percentages to avoid floating point drift in env parsing; th
394
561
 
395
562
  If you have concrete tuning needs, open an issue describing workload patterns (operation mix, baseline concurrency, observed broker limits) to help prioritize which knobs to surface.
396
563
 
564
+ ### What Should I Set?
565
+
566
+ If you're unsure about your workload shape, **don't set anything**. The default BALANCED profile activates automatically and outperforms no-gating (LEGACY) in most scenarios — on raw throughput alone, not just error reduction.
567
+
568
+ Benchmark results against a single-node local cluster with multiple independent clients (no shared state between them):
569
+
570
+ | Scenario | BALANCED | LEGACY (no gating) |
571
+ | ----------------------------- | --------------- | ------------------ |
572
+ | Single-client (1K processes) | **80.1 ops/s** | 67.8 ops/s |
573
+ | Single-client sustained (10K) | **119.6 ops/s** | 87.8 ops/s |
574
+ | Multi-client 3+2 spike | **86.3 ops/s** | 48.0 ops/s |
575
+ | Stress 8 clients ×1000 | 76.3 ops/s | **106.4 ops/s** |
576
+
577
+ BALANCED wins 3 of 4 on pure throughput. The only scenario where LEGACY is faster is extreme overload (800 concurrent requests against a single broker) — and in that case LEGACY accumulates 44,505 errors vs BALANCED's 15,527. The default just works.
578
+
397
579
  ## Job Workers (Polling API)
398
580
 
399
581
  The SDK provides a lightweight polling job worker for service task job types using `createJobWorker`. It activates jobs in batches (respecting a concurrency limit), validates variables (optional), and offers action helpers on each job.
400
582
 
401
583
  ### Minimal Example
402
584
 
585
+ <!-- snippet-source: examples/readme-imports.txt,examples/readme.ts | regions: ReadmeJobWorkerImport+ReadmeJobWorkerMinimal -->
586
+
403
587
  ```ts
404
588
  import createCamundaClient from '@camunda8/orchestration-cluster-api';
405
589
  import { z } from 'zod';
@@ -413,7 +597,7 @@ const Output = z.object({ processed: z.boolean() });
413
597
  const worker = client.createJobWorker({
414
598
  jobType: 'process-order',
415
599
  maxParallelJobs: 10,
416
- timeoutMs: 15_000, // long‑poll timeout (server side requestTimeout)
600
+ jobTimeoutMs: 15_000, // long‑poll timeout (server side requestTimeout)
417
601
  pollIntervalMs: 100, // delay between polls when no jobs / at capacity
418
602
  // Optional: only fetch specific variables during activation
419
603
  fetchVariables: ['orderId'],
@@ -425,8 +609,9 @@ const worker = client.createJobWorker({
425
609
  jobHandler: (job) => {
426
610
  // Access typed variables
427
611
  const vars = job.variables; // inferred from Input schema
612
+ console.log(`Processing order: ${vars.orderId}`);
428
613
  // Do work...
429
- return job.complete({ variables: { processed: true } });
614
+ return job.complete({ processed: true });
430
615
  },
431
616
  });
432
617
 
@@ -444,6 +629,8 @@ TypeScript inference:
444
629
 
445
630
  - When you provide `inputSchema`, the type of `fetchVariables` is constrained to the keys of the inferred `variables` type from that schema. Example:
446
631
 
632
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeJobWorkerInference -->
633
+
447
634
  ```ts
448
635
  const Input = z.object({ orderId: z.string(), amount: z.number() });
449
636
  client.createJobWorker({
@@ -463,7 +650,7 @@ client.createJobWorker({
463
650
 
464
651
  Your `jobHandler` must ultimately invoke exactly one of:
465
652
 
466
- - `job.complete({ variables? })` OR `job.complete()`
653
+ - `job.complete(variables?, result?)` OR `job.complete()`
467
654
  - `job.fail({ errorMessage, retries?, retryBackoff? })`
468
655
  - `job.cancelWorkflow({})` (cancels the process instance)
469
656
  - `job.error({ errorCode, errorMessage? })` (throws a business error)
@@ -488,6 +675,7 @@ Recommended usage:
488
675
 
489
676
  Example patterns:
490
677
 
678
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeJobCompletionPatterns -->
491
679
  ```ts
492
680
  // GOOD: explicit completion
493
681
  return job.complete({ variables: { processed: true } });
@@ -498,13 +686,61 @@ const ack = await job.complete();
498
686
  return ack;
499
687
 
500
688
  // GOOD: explicit ignore
501
- const ack = await job.ignore();
689
+ const ack2 = await job.ignore();
690
+ ```
691
+
692
+ ### Job Corrections (User Task Listeners)
693
+
694
+ When a job worker handles a [user task listener](https://docs.camunda.io/docs/components/concepts/user-task-listeners/), it can correct task properties (assignee, due date, candidate groups, etc.) by passing a `result` to `job.complete()`:
695
+
696
+ <!-- snippet-source: examples/readme-imports.txt,examples/readme.ts | regions: ReadmeJobCorrectionsImport+ReadmeJobCorrections -->
697
+
698
+ ```ts
699
+ import type { JobResult } from '@camunda8/orchestration-cluster-api';
700
+
701
+ const worker = client.createJobWorker({
702
+ jobType: 'io.camunda:userTaskListener',
703
+ jobTimeoutMs: 30_000,
704
+ maxParallelJobs: 5,
705
+ jobHandler: async (job) => {
706
+ const result: JobResult = {
707
+ type: 'userTask',
708
+ corrections: {
709
+ assignee: 'corrected-user',
710
+ priority: 80,
711
+ },
712
+ };
713
+ return job.complete({}, result);
714
+ },
715
+ });
502
716
  ```
503
717
 
718
+ To deny a task completion (reject the work):
719
+
720
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeJobCorrectionsDenial -->
721
+
504
722
  ```ts
505
- // No-arg completion example
723
+ return job.complete(
724
+ {},
725
+ {
726
+ type: 'userTask',
727
+ denied: true,
728
+ deniedReason: 'Insufficient documentation',
729
+ }
730
+ );
506
731
  ```
507
732
 
733
+ | Correctable attribute | Type | Clear value |
734
+ |---|---|---|
735
+ | `assignee` | `string` | Empty string `""` |
736
+ | `dueDate` | `string` (ISO 8601) | Empty string `""` |
737
+ | `followUpDate` | `string` (ISO 8601) | Empty string `""` |
738
+ | `candidateUsers` | `string[]` | Empty array `[]` |
739
+ | `candidateGroups` | `string[]` | Empty array `[]` |
740
+ | `priority` | `number` (0–100) | — |
741
+
742
+ Omitting an attribute or passing `null` preserves the persisted value.
743
+
508
744
  ### Concurrency & Backpressure
509
745
 
510
746
  Set `maxParallelJobs` to the maximum number of jobs you want actively processing concurrently. The worker will long‑poll for up to the remaining capacity each cycle. Global backpressure (adaptive concurrency) still applies to the underlying REST calls; activation itself is a normal operation.
@@ -521,6 +757,8 @@ If `validateSchemas` is true:
521
757
 
522
758
  Use `await worker.stopGracefully({ waitUpToMs?, checkIntervalMs? })` to drain without force‑cancelling the current activation request.
523
759
 
760
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeJobWorkerGraceful -->
761
+
524
762
  ```ts
525
763
  // Attempt graceful drain for up to 8 seconds
526
764
  const { remainingJobs, timedOut } = await worker.stopGracefully({ waitUpToMs: 8000 });
@@ -548,6 +786,8 @@ You can register multiple workers on a single client instance—one per job type
548
786
 
549
787
  When deploying multiple application instances simultaneously (e.g. a rolling restart or scale-up), all workers start polling at the same time and can saturate the server with activation requests. Set `startupJitterMaxSeconds` to spread out the initial poll across a random window:
550
788
 
789
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeJobWorkerJitter -->
790
+
551
791
  ```ts
552
792
  client.createJobWorker({
553
793
  jobType: 'process-order',
@@ -560,13 +800,71 @@ client.createJobWorker({
560
800
 
561
801
  A value of `0` (the default) means no delay.
562
802
 
803
+ ### Heritable Worker Defaults
804
+
805
+ When running many workers with the same base configuration, you can set global defaults via environment variables (or equivalent keys in `CamundaOptions.config`). These apply to every worker created by the client (both `createJobWorker` and `createThreadedJobWorker`) unless the individual worker config explicitly overrides them.
806
+
807
+ | Environment Variable | Worker Config Field | Type |
808
+ | ------------------------------------------ | -------------------------- | ------ |
809
+ | `CAMUNDA_WORKER_TIMEOUT` | `jobTimeoutMs` | number |
810
+ | `CAMUNDA_WORKER_MAX_CONCURRENT_JOBS` | `maxParallelJobs` | number |
811
+ | `CAMUNDA_WORKER_REQUEST_TIMEOUT` | `pollTimeoutMs` | number |
812
+ | `CAMUNDA_WORKER_NAME` | `workerName` | string |
813
+ | `CAMUNDA_WORKER_STARTUP_JITTER_MAX_SECONDS`| `startupJitterMaxSeconds` | number |
814
+
815
+ **Precedence:** explicit worker config value > `CAMUNDA_WORKER_*` (from environment variables or `CamundaOptions.config` overrides) > hardcoded default (where applicable).
816
+
817
+ Example — set defaults via environment:
818
+
819
+ ```bash
820
+ export CAMUNDA_WORKER_TIMEOUT=30000
821
+ export CAMUNDA_WORKER_MAX_CONCURRENT_JOBS=8
822
+ export CAMUNDA_WORKER_NAME=order-service
823
+ ```
824
+
825
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeWorkerDefaultsEnv -->
826
+ ```ts
827
+ // Workers inherit timeout, concurrency, and name from environment
828
+ const w1 = client.createJobWorker({
829
+ jobType: 'validate-order',
830
+ jobHandler: async (job) => job.complete(),
831
+ });
832
+
833
+ const w2 = client.createJobWorker({
834
+ jobType: 'ship-order',
835
+ jobHandler: async (job) => job.complete(),
836
+ });
837
+
838
+ // Per-worker override: this worker uses 32 concurrent jobs instead of the global 8
839
+ const w3 = client.createJobWorker({
840
+ jobType: 'bulk-import',
841
+ maxParallelJobs: 32,
842
+ jobHandler: async (job) => job.complete(),
843
+ });
844
+ ```
845
+
846
+ You can also pass defaults programmatically via the client constructor:
847
+
848
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeWorkerDefaultsClient -->
849
+ ```ts
850
+ const client = createCamundaClient({
851
+ config: {
852
+ CAMUNDA_WORKER_TIMEOUT: 30000,
853
+ CAMUNDA_WORKER_MAX_CONCURRENT_JOBS: 8,
854
+ },
855
+ });
856
+ ```
857
+
563
858
  ### Receipt Type (Unique Symbol)
564
859
 
565
860
  Action methods return a unique symbol (not a string) to avoid accidental misuse and allow internal metrics. If you store the receipt, annotate its type as `JobActionReceipt` to preserve uniqueness:
566
861
 
862
+ <!-- snippet-source: examples/readme-imports.txt,examples/readme.ts | regions: ReadmeReceiptImport+ReadmeReceipt -->
863
+
567
864
  ```ts
568
- import { JobActionReceipt } from '@camunda8/orchestration-cluster-api';
569
- const receipt: JobActionReceipt = await job.complete({ variables: { processed: true } });
865
+ import type { JobActionReceipt } from '@camunda8/orchestration-cluster-api';
866
+
867
+ const receipt: JobActionReceipt = await job.complete({ processed: true });
570
868
  ```
571
869
 
572
870
  If you ignore the return value you don’t need to import the symbol.
@@ -599,16 +897,126 @@ This reverts to only per‑request retry for transient errors (no global gating)
599
897
 
600
898
  Call `client.getBackpressureState()` to obtain:
601
899
 
900
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeBackpressureState -->
602
901
  ```ts
603
- {
604
- severity: 'healthy' | 'soft' | 'severe';
605
- consecutive: number; // consecutive backpressure signals observed
606
- permitsMax: number | null; // current concurrency cap (null => unlimited/not engaged)
607
- permitsCurrent: number; // currently acquired permits
608
- waiters: number; // queued operations waiting for a permit
609
- }
902
+ const state = client.getBackpressureState();
903
+ // state.severity: 'healthy' | 'soft' | 'severe'
904
+ // state.consecutive: number consecutive backpressure signals observed
905
+ // state.permitsMax: number | null current concurrency cap (null => unlimited/not engaged)
906
+ // state.permitsCurrent: number currently acquired permits
907
+ // state.waiters: number queued operations waiting for a permit
908
+ ```
909
+
910
+ ### Threaded Job Workers (Node.js Only)
911
+
912
+ For CPU-intensive job handlers, `createThreadedJobWorker` offloads handler execution to a pool of Node.js `worker_threads`. Polling and I/O remain on the main event loop, while handler logic runs in parallel threads — dramatically improving throughput when the handler does CPU-bound work (JSON processing, validation, transformation, cryptography).
913
+
914
+ #### When to use
915
+
916
+ - Your handler spends significant time on CPU work (not just waiting for HTTP responses)
917
+ - You observe that a single-threaded worker saturates one CPU core while throughput plateaus
918
+ - You need to process more jobs per second without deploying additional instances
919
+
920
+ If your handler is mostly I/O-bound (HTTP calls, database queries), the standard `createJobWorker` is sufficient.
921
+
922
+ #### Handler module
923
+
924
+ The handler must be a **separate file** (not an inline function) that exports a default async function:
925
+
926
+ <!-- snippet-exempt: pseudo-code referencing heavyComputation which cannot be type-checked -->
927
+ ```ts
928
+ // my-handler.ts (or my-handler.js)
929
+ import type { ThreadedJobHandler } from '@camunda8/orchestration-cluster-api';
930
+
931
+ const handler: ThreadedJobHandler = async (job, client) => {
932
+ const { orderId } = job.variables;
933
+ // CPU-intensive work here...
934
+ const result = heavyComputation(orderId);
935
+ return job.complete({ result });
936
+ };
937
+ export default handler;
938
+ ```
939
+
940
+ Typing your handler as `ThreadedJobHandler` gives full intellisense for `job` (variables, action methods like `complete()`, `fail()`, `error()`) and `client` (every `CamundaClient` API method).
941
+
942
+ The handler receives two arguments:
943
+
944
+ 1. **`job`** — a proxy with the same shape as a regular job worker job (`variables`, `customHeaders`, `jobKey`, plus action methods: `complete()`, `fail()`, `error()`, `cancelWorkflow()`, `ignore()`)
945
+ 2. **`client`** — a proxy to the `CamundaClient` on the main thread. You can call any SDK method (e.g. `client.publishMessage(...)`, `client.createProcessInstance(...)`) and it will be forwarded to the main thread and executed there.
946
+
947
+ #### Minimal example
948
+
949
+ <!-- snippet-source: examples/readme-imports.txt,examples/readme.ts | regions: ReadmeThreadedWorkerImport+ReadmeThreadedWorker -->
950
+
951
+ ```ts
952
+ import createCamundaClient from '@camunda8/orchestration-cluster-api';
953
+ import path from 'node:path';
954
+ import { fileURLToPath } from 'node:url';
955
+
956
+ const client = createCamundaClient();
957
+
958
+ const worker = client.createThreadedJobWorker({
959
+ jobType: 'cpu-heavy-task',
960
+ handlerModule: path.join(path.dirname(fileURLToPath(import.meta.url)), 'my-handler.js'),
961
+ maxParallelJobs: 32,
962
+ jobTimeoutMs: 30_000,
963
+ });
964
+ ```
965
+
966
+ #### Configuration
967
+
968
+ `createThreadedJobWorker` accepts all the same options as `createJobWorker` (except `jobHandler`), plus:
969
+
970
+ | Option | Type | Default | Description |
971
+ | ---------------- | -------- | --------------------------- | ---------------------------------------------------------------- |
972
+ | `handlerModule` | `string` | (required) | Path to handler module (absolute or relative to `process.cwd()`) |
973
+ | `threadPoolSize` | `number` | `os.availableParallelism()` | Number of worker threads in the pool |
974
+
975
+ Other familiar options: `jobType`, `maxParallelJobs`, `jobTimeoutMs`, `pollIntervalMs`, `pollTimeoutMs`, `fetchVariables`, `inputSchema`, `outputSchema`, `customHeadersSchema`, `validateSchemas`, `autoStart`, `startupJitterMaxSeconds`, `workerName`.
976
+
977
+ #### Lifecycle
978
+
979
+ Threaded workers integrate with the same lifecycle as regular workers:
980
+
981
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeThreadedLifecycle -->
982
+ ```ts
983
+ // Returned by getWorkers()
984
+ const allWorkers = client.getWorkers();
985
+
986
+ // Stopped by stopAllWorkers()
987
+ client.stopAllWorkers();
988
+ ```
989
+
990
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeThreadedGraceful -->
991
+ ```ts
992
+ // Graceful shutdown (waits for in-flight jobs to finish)
993
+ const { timedOut, remainingJobs } = await worker.stopGracefully({ waitUpToMs: 10_000 });
610
994
  ```
611
995
 
996
+ #### Pool stats
997
+
998
+ <!-- snippet-source: examples/readme.ts | regions: ReadmePoolStats -->
999
+ ```ts
1000
+ worker.poolSize; // number of threads
1001
+ worker.busyThreads; // threads currently processing a job
1002
+ worker.activeJobs; // total jobs dispatched but not yet completed
1003
+ ```
1004
+
1005
+ #### How it works
1006
+
1007
+ 1. The main thread polls `activateJobs` using the same mechanism as `createJobWorker`
1008
+ 2. Activated jobs are serialized and dispatched to an idle thread via `MessageChannel`
1009
+ 3. The thread loads the handler module (lazy, on first job), creates a proxy for `job` action methods and `client` API calls
1010
+ 4. Action methods (`job.complete()`, `job.fail()`, etc.) and client calls are forwarded back to the main thread over the `MessagePort` and executed there
1011
+ 5. The result is relayed back, and the thread is marked idle for the next job
1012
+
1013
+ #### Constraints
1014
+
1015
+ - **Node.js only**: `worker_threads` is not available in browsers or Deno
1016
+ - **Handler must be a file module**: Inline functions cannot be transferred to threads
1017
+ - **Job variables must be JSON-serializable**: Functions and class instances on the job are stripped during transfer
1018
+ - **Client calls are async round-trips**: Each `client.xyz()` call crosses a thread boundary, adding a small amount of latency per call
1019
+
612
1020
  ---
613
1021
 
614
1022
  ## Authentication
@@ -678,25 +1086,58 @@ Browser usage: There is no disk concept—if executed in a browser the SDK (when
678
1086
 
679
1087
  If you need a custom persistence strategy (e.g. Redis / encrypted keychain), wrap the client and periodically call `client.forceAuthRefresh()` while storing and re‑injecting the token via a headers hook; first measure whether the built‑in disk cache already meets your needs.
680
1088
 
681
- ## mTLS (Node only)
1089
+ ## Self-signed TLS / mTLS (Node only)
1090
+
1091
+ The SDK supports custom TLS certificates via environment variables. This is useful for:
1092
+
1093
+ - **Self-signed server certificates** — trust a CA that signed your server's certificate, without presenting a client identity.
1094
+ - **Mutual TLS (mTLS)** — present a client certificate and key to prove the client's identity.
1095
+ - **Both** — trust a custom CA _and_ present client credentials.
1096
+
1097
+ ### Trusting a self-signed server certificate
1098
+
1099
+ Set only the CA certificate to trust the server's self-signed certificate:
1100
+
1101
+ ```bash
1102
+ # Path to PEM file:
1103
+ CAMUNDA_MTLS_CA_PATH=/path/to/ca.pem
1104
+
1105
+ # Or inline PEM (must contain real newlines, not literal '\n'):
1106
+ CAMUNDA_MTLS_CA="$(cat /path/to/ca.pem)"
1107
+ ```
1108
+
1109
+ ### Mutual TLS (client certificate)
1110
+
1111
+ To present a client certificate for mutual TLS, provide both the certificate and private key:
682
1112
 
683
- Provide inline or path variables (inline wins):
1113
+ ```bash
1114
+ CAMUNDA_MTLS_CERT_PATH=/path/to/client.crt
1115
+ CAMUNDA_MTLS_KEY_PATH=/path/to/client.key
684
1116
 
1117
+ # Optional — passphrase if the key is encrypted:
1118
+ # CAMUNDA_MTLS_KEY_PASSPHRASE=secret
685
1119
  ```
686
- CAMUNDA_MTLS_CERT / CAMUNDA_MTLS_CERT_PATH
687
- CAMUNDA_MTLS_KEY / CAMUNDA_MTLS_KEY_PATH
688
- CAMUNDA_MTLS_CA / CAMUNDA_MTLS_CA_PATH (optional)
689
- CAMUNDA_MTLS_KEY_PASSPHRASE (optional)
1120
+
1121
+ ### Full mTLS with custom CA
1122
+
1123
+ Combine a custom CA with client credentials:
1124
+
1125
+ ```bash
1126
+ CAMUNDA_MTLS_CA_PATH=/path/to/ca.pem
1127
+ CAMUNDA_MTLS_CERT_PATH=/path/to/client.crt
1128
+ CAMUNDA_MTLS_KEY_PATH=/path/to/client.key
690
1129
  ```
691
1130
 
692
- If both cert & key are available an https.Agent is attached to all outbound calls (including token fetches).
1131
+ Inline PEM values (`CAMUNDA_MTLS_CERT`, `CAMUNDA_MTLS_KEY`, `CAMUNDA_MTLS_CA`) take precedence over their `_PATH` counterparts. An `https.Agent` is attached to all outbound calls (including token fetches).
693
1132
 
694
1133
  ## Branded Keys
695
1134
 
696
1135
  Import branded key helpers directly:
697
1136
 
1137
+ <!-- snippet-source: examples/readme-imports.txt,examples/readme.ts | regions: ReadmeBrandedKeysImport+ReadmeBrandedKeys -->
1138
+
698
1139
  ```ts
699
- import { ProcessDefinitionKey, ProcessInstanceKey } from '@camunda8/orchestration-cluster';
1140
+ import { ProcessDefinitionKey, ProcessInstanceKey } from '@camunda8/orchestration-cluster-api';
700
1141
 
701
1142
  const defKey = ProcessDefinitionKey.assumeExists('2251799813686749');
702
1143
  // @ts-expect-error – cannot assign def key to instance key
@@ -709,8 +1150,13 @@ They are zero‑cost runtime strings with compile‑time separation.
709
1150
 
710
1151
  All methods return a `CancelablePromise<T>`:
711
1152
 
1153
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeCancelable -->
1154
+
712
1155
  ```ts
713
- const p = camunda.searchProcessInstances({ filter: { processDefinitionKey: defKey } });
1156
+ const p = camunda.searchProcessInstances(
1157
+ { filter: { processDefinitionKey: defKey } },
1158
+ { consistency: { waitUpToMs: 0 } }
1159
+ );
714
1160
  setTimeout(() => p.cancel(), 100); // best‑effort cancel
715
1161
  try {
716
1162
  await p; // resolves if not cancelled
@@ -731,8 +1177,17 @@ Notes:
731
1177
 
732
1178
  @experimental - this feature is not guaranteed to be tested or stable.
733
1179
 
1180
+ > **Peer dependency:** `fp-ts` is an optional peer dependency. If you use real `fp-ts` functions
1181
+ > (e.g. `pipe`, `TE.match`) alongside this subpath, install it separately:
1182
+ > ```sh
1183
+ > npm install fp-ts
1184
+ > ```
1185
+ > The `/fp` subpath works without `fp-ts` installed — it exposes structurally-compatible
1186
+ > `Either`/`TaskEither` shapes that interoperate with `fp-ts` but do not require it at runtime.
1187
+
734
1188
  The main entry stays minimal. To opt in to a TaskEither-style facade & helper combinators import from the dedicated subpath:
735
1189
 
1190
+ <!-- snippet-exempt: uses SDK /fp subpath not available in examples project -->
736
1191
  ```ts
737
1192
  import {
738
1193
  createCamundaFpClient,
@@ -740,7 +1195,7 @@ import {
740
1195
  withTimeoutTE,
741
1196
  eventuallyTE,
742
1197
  isLeft,
743
- } from '@camunda8/orchestration-cluster/fp';
1198
+ } from '@camunda8/orchestration-cluster-api/fp';
744
1199
 
745
1200
  const fp = createCamundaFpClient();
746
1201
  const deployTE = fp.deployResourcesFromFiles(['./bpmn/process.bpmn']);
@@ -808,16 +1263,22 @@ Use this to understand convergence speed and data shape evolution during tests o
808
1263
 
809
1264
  ### Example
810
1265
 
1266
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeEventualConsistency -->
1267
+
811
1268
  ```ts
812
- const jobs = await camunda.searchJobs({
813
- filter: { type: 'payment' },
814
- consistency: {
815
- waitUpToMs: 5000,
816
- pollIntervalMs: 200,
817
- trace: true,
818
- predicate: (r) => Array.isArray(r.items) && r.items.some((j) => j.state === 'CREATED'),
1269
+ const jobs = await camunda.searchJobs(
1270
+ {
1271
+ filter: { type: 'payment' },
819
1272
  },
820
- });
1273
+ {
1274
+ consistency: {
1275
+ waitUpToMs: 5000,
1276
+ pollIntervalMs: 200,
1277
+ trace: true,
1278
+ predicate: (r) => Array.isArray(r.items) && r.items.some((j) => j.state === 'CREATED'),
1279
+ },
1280
+ }
1281
+ );
821
1282
  ```
822
1283
 
823
1284
  On timeout an `EventualConsistencyTimeoutError` includes diagnostic fields: `{ attempts, elapsedMs, lastStatus, lastResponse, operationId }`.
@@ -826,6 +1287,8 @@ On timeout an `EventualConsistencyTimeoutError` includes diagnostic fields: `{ a
826
1287
 
827
1288
  Per‑client logger; no global singleton. The level defaults from `CAMUNDA_SDK_LOG_LEVEL` (default `error`).
828
1289
 
1290
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeLogging -->
1291
+
829
1292
  ```ts
830
1293
  const client = createCamundaClient({
831
1294
  log: {
@@ -877,9 +1340,10 @@ Provide a `transport` function to forward structured `LogEvent` objects into any
877
1340
 
878
1341
  #### Pino
879
1342
 
1343
+ <!-- snippet-exempt: uses external pino dependency -->
880
1344
  ```ts
881
1345
  import pino from 'pino';
882
- import createCamundaClient from '@camunda8/orchestration-cluster';
1346
+ import createCamundaClient from '@camunda8/orchestration-cluster-api';
883
1347
 
884
1348
  const p = pino();
885
1349
  const client = createCamundaClient({
@@ -895,9 +1359,10 @@ const client = createCamundaClient({
895
1359
 
896
1360
  #### Winston
897
1361
 
1362
+ <!-- snippet-exempt: uses external winston dependency -->
898
1363
  ```ts
899
1364
  import winston from 'winston';
900
- import createCamundaClient from '@camunda8/orchestration-cluster';
1365
+ import createCamundaClient from '@camunda8/orchestration-cluster-api';
901
1366
 
902
1367
  const w = winston.createLogger({ transports: [new winston.transports.Console()] });
903
1368
  const client = createCamundaClient({
@@ -920,9 +1385,10 @@ const client = createCamundaClient({
920
1385
 
921
1386
  #### loglevel
922
1387
 
1388
+ <!-- snippet-exempt: uses external loglevel dependency -->
923
1389
  ```ts
924
1390
  import log from 'loglevel';
925
- import createCamundaClient from '@camunda8/orchestration-cluster';
1391
+ import createCamundaClient from '@camunda8/orchestration-cluster-api';
926
1392
 
927
1393
  log.setLevel('info'); // host app level
928
1394
  const client = createCamundaClient({
@@ -963,9 +1429,10 @@ May throw:
963
1429
 
964
1430
  All SDK-thrown operational errors normalize to a discriminated union (`SdkError`) when they originate from HTTP, network, auth, or validation layers. Use the guard `isSdkError` to narrow inside a catch:
965
1431
 
1432
+ <!-- snippet-source: examples/readme-imports.txt,examples/readme.ts | regions: ReadmeErrorHandlingImport+ReadmeErrorHandling -->
1433
+
966
1434
  ```ts
967
- import { createCamundaClient } from '@camunda8/orchestration-cluster-api';
968
- import { isSdkError } from '@camunda8/orchestration-cluster-api/dist/runtime/errors';
1435
+ import { createCamundaClient, isSdkError } from '@camunda8/orchestration-cluster-api';
969
1436
 
970
1437
  const client = createCamundaClient();
971
1438
 
@@ -1012,13 +1479,15 @@ _Note that this feature is experimental and subject to change._
1012
1479
 
1013
1480
  If you prefer FP‑style explicit error handling instead of exceptions, use the result client wrapper:
1014
1481
 
1482
+ <!-- snippet-source: examples/readme-imports.txt,examples/readme.ts | regions: ReadmeResultClientImport+ReadmeResultClient -->
1483
+
1015
1484
  ```ts
1016
- import { createCamundaResultClient, isOk } from '@camunda8/orchestration-cluster';
1485
+ import { createCamundaResultClient, isOk } from '@camunda8/orchestration-cluster-api';
1017
1486
 
1018
1487
  const camundaR = createCamundaResultClient();
1019
1488
  const res = await camundaR.createDeployment({ resources: [file] });
1020
1489
  if (isOk(res)) {
1021
- console.log('Deployment key', res.value.deployments[0].deploymentKey);
1490
+ console.log('Deployment key', res.value.deploymentKey);
1022
1491
  } else {
1023
1492
  console.error('Deployment failed', res.error);
1024
1493
  }
@@ -1032,8 +1501,9 @@ API surface differences:
1032
1501
 
1033
1502
  Helpers:
1034
1503
 
1504
+ <!-- snippet-exempt: one-liner import example -->
1035
1505
  ```ts
1036
- import { isOk, isErr } from '@camunda8/orchestration-cluster';
1506
+ import { isOk, isErr } from '@camunda8/orchestration-cluster-api';
1037
1507
  ```
1038
1508
 
1039
1509
  When to use:
@@ -1048,8 +1518,9 @@ _Note that this feature is experimental and subject to change._
1048
1518
 
1049
1519
  For projects using `fp-ts`, wrap the throwing client in a lazy `TaskEither` facade:
1050
1520
 
1521
+ <!-- snippet-exempt: requires external fp-ts dependency -->
1051
1522
  ```ts
1052
- import { createCamundaFpClient } from '@camunda8/orchestration-cluster';
1523
+ import { createCamundaFpClient } from '@camunda8/orchestration-cluster-api/fp';
1053
1524
  import { pipe } from 'fp-ts/function';
1054
1525
  import * as TE from 'fp-ts/TaskEither';
1055
1526
 
@@ -1097,6 +1568,8 @@ The deployment endpoint requires each resource to have a filename (extension use
1097
1568
 
1098
1569
  ### Browser
1099
1570
 
1571
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeDeployBrowser -->
1572
+
1100
1573
  ```ts
1101
1574
  const bpmnXml = `<definitions id="process" xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL">...</definitions>`;
1102
1575
  const file = new File([bpmnXml], 'order-process.bpmn', { type: 'application/xml' });
@@ -1106,6 +1579,7 @@ console.log(result.deployments.length);
1106
1579
 
1107
1580
  From an existing Blob:
1108
1581
 
1582
+ <!-- snippet-exempt: uses hypothetical getBlob() function -->
1109
1583
  ```ts
1110
1584
  const blob: Blob = getBlob();
1111
1585
  const file = new File([blob], 'model.bpmn');
@@ -1116,6 +1590,8 @@ await camunda.createDeployment({ resources: [file] });
1116
1590
 
1117
1591
  Use the built-in helper `deployResourcesFromFiles(...)` to read local files and create `File` objects automatically. It returns the enriched `ExtendedDeploymentResult` (adds typed arrays: `processes`, `decisions`, `decisionRequirements`, `forms`, `resources`).
1118
1592
 
1593
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeDeployNode -->
1594
+
1119
1595
  ```ts
1120
1596
  const result = await camunda.deployResourcesFromFiles([
1121
1597
  './bpmn/order-process.bpmn',
@@ -1129,12 +1605,14 @@ console.log(result.decisions.length);
1129
1605
 
1130
1606
  With explicit tenant (overriding tenant from configuration):
1131
1607
 
1608
+ <!-- snippet-exempt: small variant of injected deploy example above -->
1132
1609
  ```ts
1133
1610
  await camunda.deployResourcesFromFiles(['./bpmn/order-process.bpmn'], { tenantId: 'tenant-a' });
1134
1611
  ```
1135
1612
 
1136
1613
  Error handling:
1137
1614
 
1615
+ <!-- snippet-exempt: small variant of injected deploy example above -->
1138
1616
  ```ts
1139
1617
  try {
1140
1618
  await camunda.deployResourcesFromFiles([]); // throws (empty array)
@@ -1145,6 +1623,7 @@ try {
1145
1623
 
1146
1624
  Manual construction alternative (if you need custom logic):
1147
1625
 
1626
+ <!-- snippet-exempt: alternative construction pattern using node:buffer -->
1148
1627
  ```ts
1149
1628
  import { File } from 'node:buffer';
1150
1629
  const bpmnXml =
@@ -1166,6 +1645,8 @@ Empty arrays are rejected. Always use correct extensions so the server can class
1166
1645
 
1167
1646
  Create isolated clients per test file:
1168
1647
 
1648
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeTestingClient -->
1649
+
1169
1650
  ```ts
1170
1651
  const client = createCamundaClient({
1171
1652
  config: { CAMUNDA_REST_ADDRESS: 'http://localhost:8080', CAMUNDA_AUTH_STRATEGY: 'NONE' },
@@ -1174,9 +1655,11 @@ const client = createCamundaClient({
1174
1655
 
1175
1656
  Inject a mock fetch:
1176
1657
 
1658
+ <!-- snippet-source: examples/readme.ts | regions: ReadmeTestingMock -->
1659
+
1177
1660
  ```ts
1178
1661
  const client = createCamundaClient({
1179
- fetch: async (input, init) => new Response(JSON.stringify({ ok: true }), { status: 200 }),
1662
+ fetch: async (_input, _init) => new Response(JSON.stringify({ ok: true }), { status: 200 }),
1180
1663
  });
1181
1664
  ```
1182
1665