@unstable-dev/unmeshed-mcp 0.1.0

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.
Files changed (47) hide show
  1. package/README.md +64 -0
  2. package/dist/auth.d.ts +6 -0
  3. package/dist/auth.js +11 -0
  4. package/dist/client.d.ts +46 -0
  5. package/dist/client.js +97 -0
  6. package/dist/config.d.ts +10 -0
  7. package/dist/config.js +31 -0
  8. package/dist/get-docs.d.ts +8 -0
  9. package/dist/get-docs.js +64 -0
  10. package/dist/index.d.ts +2 -0
  11. package/dist/index.js +35 -0
  12. package/dist/server.d.ts +4 -0
  13. package/dist/server.js +203 -0
  14. package/knowledge/README.md +16 -0
  15. package/knowledge/SKILL.md +359 -0
  16. package/knowledge/assets/patterns.md +637 -0
  17. package/knowledge/execution/debugging-guide.md +18 -0
  18. package/knowledge/execution/process-run.schema.md +24 -0
  19. package/knowledge/execution/step-run.schema.md +21 -0
  20. package/knowledge/process-definition.schema.md +36 -0
  21. package/knowledge/references/integrations.md +914 -0
  22. package/knowledge/references/steps-knowledge.md +834 -0
  23. package/knowledge/step-definition.schema.md +140 -0
  24. package/knowledge/step-output-paths.md +45 -0
  25. package/knowledge/steps/DECISION_ENGINE.md +248 -0
  26. package/knowledge/steps/DEPENDSON.md +296 -0
  27. package/knowledge/steps/EXIT.md +220 -0
  28. package/knowledge/steps/FAIL.md +198 -0
  29. package/knowledge/steps/FLOW_GATEWAY.md +405 -0
  30. package/knowledge/steps/FOREACH.md +250 -0
  31. package/knowledge/steps/HTTP.md +183 -0
  32. package/knowledge/steps/JAVASCRIPT.md +192 -0
  33. package/knowledge/steps/JQ.md +189 -0
  34. package/knowledge/steps/LIST.md +279 -0
  35. package/knowledge/steps/NOOP.md +165 -0
  36. package/knowledge/steps/PARALLEL.md +366 -0
  37. package/knowledge/steps/PYTHON.md +206 -0
  38. package/knowledge/steps/SEND_RESPONSE.md +301 -0
  39. package/knowledge/steps/SQLITE.md +301 -0
  40. package/knowledge/steps/SUB_PROCESS.md +296 -0
  41. package/knowledge/steps/SWITCH.md +369 -0
  42. package/knowledge/steps/UPDATE_STEP.md +257 -0
  43. package/knowledge/steps/WAIT.md +218 -0
  44. package/knowledge/steps/WHILE.md +328 -0
  45. package/knowledge/steps/WORKER.md +233 -0
  46. package/knowledge/system-prompt.md +274 -0
  47. package/package.json +39 -0
@@ -0,0 +1,834 @@
1
+ # Unmeshed Step Types — Full Reference
2
+
3
+ ---
4
+
5
+ ## JAVASCRIPT
6
+
7
+ Executes an inline JavaScript transformation. Input/output data mapping between steps.
8
+
9
+ ```json
10
+ {
11
+ "type": "JAVASCRIPT",
12
+ "input": {
13
+ "script": "(steps, context) => {\n // your logic here\n return { result: value };\n}"
14
+ }
15
+ }
16
+ ```
17
+
18
+ **Rules:**
19
+ - Script signature is always `(steps, context) => { ... }`
20
+ - Access previous step output via `steps.<ref>.output.result.<field>`
21
+ - Access workflow input via `context.input.<field>`
22
+ - Access workflow/process metadata via `context.id`
23
+ - Must `return` an object — this becomes `output.result`
24
+ - Use `steps.__self.id` for current step ID
25
+ - Use `steps.__self.startTime` for the step's start timestamp (milliseconds) — useful in WAIT steps
26
+ - Use `steps.__self.executionList` — array of past execution attempts for this step (populated when an error policy retries). `steps.__self.executionList.length` = number of attempts so far.
27
+ - `preExecutionScript` in configuration can be used for setup (rare)
28
+ - To throw a test failure: `throw Error("Testcase failed")` — this causes the step and workflow to fail
29
+
30
+ **Accessing a prior step's INPUT** (not output) from a later step:
31
+ ```javascript
32
+ steps.<ref>.input.body.content.<field> // HTTP step request body field
33
+ steps.<ref>.input.<field> // any step input field
34
+ ```
35
+ **Accessing a prior JS/Python step's OUTPUT** from another step:
36
+ ```javascript
37
+ steps.<ref>.output.result.<field>
38
+ ```
39
+ **Accessing error output from an optional failed step:**
40
+ ```javascript
41
+ const errorMessage = steps.my_optional_step.output.error;
42
+ if (errorMessage.includes("some error text")) { ... }
43
+ ```
44
+
45
+ **Return `__statePut` to update workflow state:**
46
+ ```javascript
47
+ return {
48
+ "__statePut": { "count": (context.state.count || 0) + 1 }
49
+ };
50
+ ```
51
+
52
+ **Deep equality helper** (no JSON.stringify, handles nested objects):
53
+ ```javascript
54
+ function deepEqual(obj1, obj2) {
55
+ if (obj1 === obj2) return true;
56
+ if (typeof obj1 !== 'object' || obj1 === null ||
57
+ typeof obj2 !== 'object' || obj2 === null) return false;
58
+ const keys1 = Object.keys(obj1);
59
+ const keys2 = Object.keys(obj2);
60
+ if (keys1.length !== keys2.length) return false;
61
+ for (let key of keys1) {
62
+ if (!keys2.includes(key)) return false;
63
+ if (!deepEqual(obj1[key], obj2[key])) return false;
64
+ }
65
+ return true;
66
+ }
67
+ ```
68
+
69
+ ---
70
+
71
+ ## PYTHON
72
+
73
+ Executes inline Python for heavier processing (file parsing, binary ops, filesystem access).
74
+
75
+ ```json
76
+ {
77
+ "type": "PYTHON",
78
+ "input": {
79
+ "script": "def main(steps, context):\n # your logic\n return { 'key': 'value' }"
80
+ }
81
+ }
82
+ ```
83
+
84
+ **Rules:**
85
+ - Entry point is always `def main(steps, context):`
86
+ - `steps` and `context` are dicts — use `.get()` for safe access
87
+ - Files available at `/app/files/<location>/<filename>`
88
+ - Must return a dict
89
+ - Standard Python libraries available (os, re, zlib, json, base64, mimetypes, etc.)
90
+ - Output is under `steps.<ref>.output.result.<field>`
91
+ - Use `steps["<ref>"].output.result["<field>"]` when referring to output from another Python or JavaScript step
92
+ - Use `steps["__self"]["id"]` for current step ID inside Python
93
+ - Use `context.get("id")` for the workflow process/run ID
94
+
95
+ **File system patterns:**
96
+ ```python
97
+ import os, base64, mimetypes
98
+
99
+ def main(steps, context):
100
+ base_dir = "/app/files/my-folder"
101
+ attachments = []
102
+ for filename in os.listdir(base_dir):
103
+ path = os.path.join(base_dir, filename)
104
+ if os.path.isfile(path):
105
+ with open(path, "rb") as f:
106
+ raw = f.read()
107
+ encoded = base64.b64encode(raw).decode("utf-8")
108
+ mime, _ = mimetypes.guess_type(filename)
109
+ attachments.append({
110
+ "name": filename,
111
+ "contentType": mime or "application/octet-stream",
112
+ "type": "CONTENT_BYTES",
113
+ "contentBase64": encoded
114
+ })
115
+ return {"attachments": attachments}
116
+ ```
117
+
118
+ ---
119
+
120
+ ## preExecutionScript
121
+
122
+ `preExecutionScript` is supported for Python and JavaScript steps. It runs before the step executes and can override the step definition input at runtime.
123
+
124
+ Required fields:
125
+ - `preExecutionScript`: the actual script to execute
126
+ - `constructInputFromScript`: set to `true` to enable script-driven input construction
127
+ - `scriptLanguage`: `JAVASCRIPT` or `PYTHON` (`JAVASCRIPT` is the default when omitted)
128
+
129
+ The step schema fields are: `preExecutionScript`, `constructInputFromScript`, and `scriptLanguage`.
130
+
131
+ When enabled, the script runs before the step and its returned keys are merged into the step definition. Any returned values override the actual step configuration during runtime.
132
+
133
+ Example (JavaScript):
134
+ ```json
135
+ {
136
+ "type": "HTTP",
137
+ "preExecutionScript": "(steps, context) => { return {\n url: `https://api.example.com/users/${steps.create_user.output.result.id}`,\n method: 'GET'\n }; }",
138
+ "constructInputFromScript": true,
139
+ "scriptLanguage": "JAVASCRIPT",
140
+ "input": {
141
+ "method": "GET",
142
+ "url": "https://placeholder.example.com",
143
+ "headers": {
144
+ "Accept": "application/json"
145
+ },
146
+ "params": {}
147
+ }
148
+ }
149
+ ```
150
+
151
+ Example (Python):
152
+ ```json
153
+ {
154
+ "type": "HTTP",
155
+ "preExecutionScript": "def main(steps, context):\n return {\n 'url': f'https://api.example.com/users/{steps[\"create_user\"].output.result[\"id\"]}',\n 'method': 'GET'\n }\n",
156
+ "constructInputFromScript": true,
157
+ "scriptLanguage": "PYTHON",
158
+ "input": {
159
+ "method": "GET",
160
+ "url": "https://placeholder.example.com",
161
+ "headers": {
162
+ "Accept": "application/json"
163
+ },
164
+ "params": {}
165
+ }
166
+ }
167
+ ```
168
+
169
+ Note:
170
+ - `preExecutionScript` must return an object
171
+ - returned fields override the step configuration
172
+ - the default `scriptLanguage` is `JAVASCRIPT`
173
+
174
+ ## NOOP
175
+
176
+ A pass-through step that holds static configuration/constants and exposes them as output.
177
+
178
+ ```json
179
+ {
180
+ "type": "NOOP",
181
+ "input": {
182
+ "anyKey": "anyStaticValue",
183
+ "nestedObject": { "field": "value" },
184
+ "arrayField": ["item1", "item2"]
185
+ }
186
+ }
187
+ ```
188
+
189
+ **Rules:**
190
+ - No computation — purely a data carrier
191
+ - All `input` fields become directly accessible as `steps.<ref>.output.<field>`
192
+ (note: NOT wrapped in `.result` — access directly)
193
+ - Ideal for job definitions, config objects, feature flags
194
+
195
+ ---
196
+
197
+ ## INTEGRATION
198
+
199
+ Calls an external service. The `type` field in `input` determines which integration.
200
+
201
+ ```json
202
+ {
203
+ "type": "INTEGRATION",
204
+ "input": {
205
+ "type": "<integration-type>",
206
+ "name": "<connection-name>",
207
+ "publishProperties": { /* integration-specific config */ },
208
+ "messageBody": { /* payload / prompts / operation */ }
209
+ }
210
+ }
211
+ ```
212
+
213
+ See `integrations.md` for full per-integration schemas.
214
+
215
+ ---
216
+
217
+ ## HTTP (native step — not INTEGRATION)
218
+
219
+ A dedicated HTTP step type (distinct from the `http` INTEGRATION). Used for direct HTTP calls with optional polling/repeat support.
220
+
221
+ ### GET request
222
+ ```json
223
+ {
224
+ "type": "HTTP",
225
+ "input": {
226
+ "method": "GET",
227
+ "url": "{{variables.dev_server}}/api/resource/{{steps.create.output.response.id}}",
228
+ "headers": {
229
+ "Content-Type": "application/json",
230
+ "Accept": "application/json",
231
+ "Authorization": "Bearer {{secrets.api_token}}"
232
+ },
233
+ "params": { "key": "value" },
234
+ "repeatUntilEnabled": null,
235
+ "repeatUntilCondition": { "script": "(steps, context) => { return false; }" },
236
+ "repeatIntervalSeconds": null,
237
+ "maxRepeatCount": null,
238
+ "includeFullResponseString": false,
239
+ "noEncode": false,
240
+ "extraLongTimeouts": false
241
+ }
242
+ }
243
+ ```
244
+
245
+ ### POST / PUT request (with body)
246
+ ```json
247
+ {
248
+ "type": "HTTP",
249
+ "input": {
250
+ "method": "POST",
251
+ "url": "{{variables.dev_server}}/api/resource",
252
+ "headers": { "Content-Type": "application/json", "Authorization": "Bearer {{secrets.api_token}}" },
253
+ "params": {},
254
+ "body": {
255
+ "type": "json",
256
+ "content": {
257
+ "title": "foo",
258
+ "body": "{{ steps.prev_step.output.response.someField }}",
259
+ "userId": 1,
260
+ "fullObject": "{{ steps.prev_step.output.response }}"
261
+ }
262
+ },
263
+ "repeatUntilEnabled": null,
264
+ "repeatUntilCondition": { "script": "(steps, context) => { return false; }" },
265
+ "repeatIntervalSeconds": null,
266
+ "maxRepeatCount": null,
267
+ "includeFullResponseString": false,
268
+ "noEncode": false,
269
+ "extraLongTimeouts": false
270
+ }
271
+ }
272
+ ```
273
+
274
+ ### DELETE request
275
+ ```json
276
+ {
277
+ "input": {
278
+ "method": "DELETE",
279
+ "url": "{{variables.dev_server}}/api/resource",
280
+ "params": { "id": "{{ steps.create.output.response.id }}" },
281
+ "body": { "type": "json", "content": {} }
282
+ }
283
+ }
284
+ ```
285
+
286
+ ### Polling (repeat until condition)
287
+ ```json
288
+ {
289
+ "input": {
290
+ "method": "GET",
291
+ "url": "https://api.example.com/status",
292
+ "params": { "count": "{{ context.state.count }}" },
293
+ "repeatUntilEnabled": true,
294
+ "repeatUntilCondition": {
295
+ "script": "(steps, context) => { return steps.__self.output?.response?.status === 'done'; }"
296
+ },
297
+ "repeatIntervalSeconds": 2,
298
+ "maxRepeatCount": 10,
299
+ "includeFullResponseString": false,
300
+ "noEncode": false,
301
+ "extraLongTimeouts": false
302
+ }
303
+ }
304
+ ```
305
+
306
+ **Rules:**
307
+ - `type` is `"HTTP"` (uppercase, not `"INTEGRATION"`)
308
+ - `ref` naming: `new_http_<n>`, `http_<n>`, or a descriptive name like `fetch_all`, `create_new`
309
+ - Output: `steps.<ref>.output.response.<field>` (response body fields), `steps.<ref>.output.statusCode` (integer)
310
+ - Access a prior step's INPUT from a later step: `steps.<ref>.input.body.content.<field>` (real pattern — useful for assertions)
311
+ - `body`: for POST/PUT/DELETE — structure is `{ "type": "json", "content": { ... } }`. GET requests omit `body`.
312
+ - `body.content` can include `{{...}}` template references to inject previous step outputs
313
+ - `params`: query string key-value pairs (always include even if empty `{}`)
314
+ - `{{variables.<name>}}`: inject environment/namespace variables (e.g. server URLs)
315
+ - `{{secrets.<name>}}`: inject secrets (e.g. API tokens) — never hardcode credentials
316
+ - URL path segments can include `{{steps.<ref>.output.response.<field>}}` directly in the URL string
317
+ - `repeatUntilEnabled: true` + `repeatIntervalSeconds` + `maxRepeatCount` — polling mode; stops when condition returns `true`
318
+ - `repeatUntilCondition.script`: use optional chaining (`?.`) for safety since early polls may have no response yet
319
+ - When NOT polling: set `repeatUntilEnabled: null`, `repeatIntervalSeconds: null`, `maxRepeatCount: null`
320
+ - `includeFullResponseString: false` — set `true` only when you need the raw response string
321
+ - `extraLongTimeouts: false` — set `true` for very slow endpoints
322
+ - Collect FOREACH loop results: `steps["http[0]"].output`, `steps["http[1]"].output`, etc.
323
+
324
+ ---
325
+
326
+ ## SQLITE
327
+
328
+ A native SQLITE step that executes a SQL statement against a managed SQLite store in the current namespace.
329
+
330
+ Example:
331
+ ```json
332
+ {
333
+ "orgId": 1,
334
+ "namespace": "automated_tests",
335
+ "name": "update",
336
+ "type": "SQLITE",
337
+ "ref": "update",
338
+ "optional": false,
339
+ "createdBy": "system",
340
+ "updatedBy": "system",
341
+ "description": null,
342
+ "label": null,
343
+ "created": 1765971852407,
344
+ "updated": 1765971852407,
345
+ "configuration": {
346
+ "errorPolicyName": null,
347
+ "useCache": false,
348
+ "cacheKey": null,
349
+ "cacheTimeoutSeconds": 0,
350
+ "stream": false,
351
+ "streamAllStatuses": false,
352
+ "preExecutionScript": null,
353
+ "constructInputFromScript": false,
354
+ "scriptLanguage": null,
355
+ "jqTransformer": null,
356
+ "rateLimitMaxRequests": 0,
357
+ "rateLimitWindowSeconds": 0
358
+ },
359
+ "children": [],
360
+ "input": {
361
+ "storeName": "automated-test-db",
362
+ "sql": "update sample_table set name = :#newName where name = 'Alice' or name = :secondName;",
363
+ "parameters": {
364
+ "newName": "Bob",
365
+ "secondName": "Alex"
366
+ }
367
+ },
368
+ "output": null
369
+ }
370
+ ```
371
+
372
+ **Rules:**
373
+ - `type` must be `SQLITE`
374
+ - `storeName` specifies the SQLite store name in the current namespace
375
+ - `sql` contains the SQL statement to execute
376
+ - `parameters` is optional, and is used for parameterized SQL values
377
+ - `preExecutionScript`, `constructInputFromScript`, and `scriptLanguage` behave the same way as other step types
378
+ - SQL results are returned in `steps.<ref>.output` if the step is configured to expose output
379
+
380
+ ---
381
+
382
+ ## WORKER
383
+
384
+ Executes code in external worker processes using the Unmeshed Polyglot SDK. Workers run outside the main Unmeshed engine and can be implemented in Java, Python, Go, TypeScript/NodeJs or other languages.
385
+
386
+ Example:
387
+ ```json
388
+ {
389
+ "orgId": 1,
390
+ "namespace": "automated_tests",
391
+ "name": "not_exist_x",
392
+ "type": "WORKER",
393
+ "ref": "worker_1",
394
+ "optional": false,
395
+ "createdBy": "system",
396
+ "updatedBy": "system",
397
+ "description": null,
398
+ "label": null,
399
+ "created": 1776247373914,
400
+ "updated": 1776247373914,
401
+ "configuration": {
402
+ "errorPolicyName": null,
403
+ "useCache": false,
404
+ "cacheKey": null,
405
+ "cacheTimeoutSeconds": 0,
406
+ "stream": false,
407
+ "streamAllStatuses": false,
408
+ "preExecutionScript": null,
409
+ "constructInputFromScript": false,
410
+ "scriptLanguage": null,
411
+ "jqTransformer": null,
412
+ "rateLimitMaxRequests": 0,
413
+ "rateLimitWindowSeconds": 0
414
+ },
415
+ "children": [],
416
+ "input": {},
417
+ "output": null
418
+ }
419
+ ```
420
+
421
+ **Rules:**
422
+ - `type` must be `WORKER`
423
+ - `name` specifies the worker queue name that the SDK worker is registered to
424
+ - `input` contains the data passed to the worker function
425
+ - Workers run asynchronously outside the main engine using the Unmeshed SDK
426
+ - Worker implementations can be in Java (Spring Boot), Python, or other supported languages
427
+ - Worker output is returned in `steps.<ref>.output` when the worker completes
428
+ - `preExecutionScript`, `constructInputFromScript`, and `scriptLanguage` behave the same way as other step types
429
+ - Workers must be registered and running for the step to execute successfully
430
+
431
+ ---
432
+
433
+ ## WAIT
434
+
435
+ Pauses workflow execution until a specified time.
436
+
437
+ ```json
438
+ {
439
+ "type": "WAIT",
440
+ "input": {
441
+ "script": "(steps, context) => {\n return {\n \"waitUntil\": steps.__self.startTime + (25 * 1000)\n };\n}"
442
+ }
443
+ }
444
+ ```
445
+
446
+ **Rules:**
447
+ - `input.script` is a JS function returning `{ waitUntil: <timestamp_ms> }`
448
+ - `steps.__self.startTime` — start time of this WAIT instance in milliseconds (use for relative waits)
449
+ - `steps.__self.start` — alternative property (also milliseconds)
450
+ - Inside a WHILE loop body: use the WHILE step's `.updated` timestamp as the loop iteration reference:
451
+ ```javascript
452
+ (steps, context) => {
453
+ const lastIterationTime = steps.my_while_step.updated;
454
+ return { "waitUntil": lastIterationTime + 1000 };
455
+ }
456
+ ```
457
+ - Inside FOREACH loops: use `steps.__self.executionList?.at(-1)?.scheduled` as reference
458
+ - `waitUntil` is an absolute epoch timestamp in milliseconds
459
+ - `ref` naming: `wait_<n>` or `wait`
460
+
461
+ ---
462
+
463
+ ## FOREACH
464
+
465
+ Iterates over an array, executing its child steps for each element.
466
+
467
+ ```json
468
+ {
469
+ "type": "FOREACH",
470
+ "children": [
471
+ { /* child step(s) — usually a LIST */ }
472
+ ],
473
+ "input": {
474
+ "inputArray": [1, 2, 3, 4, 5],
475
+ "concurrency": 1
476
+ }
477
+ }
478
+ ```
479
+
480
+ **Rules:**
481
+ - `inputArray`: the array to iterate over — can be static or a `{{...}}` template reference
482
+ - `concurrency`: how many iterations run in parallel — `1` = sequential, `N` = N at a time
483
+ - Children execute for each element in the array
484
+ - Access the current iteration value inside child scripts via `context.input` or loop context
485
+ - After the loop, access individual iteration outputs: `steps["<ref>[0]"].output`, `steps["<ref>[1]"].output`, etc.
486
+ - `ref` naming: use a descriptive name like `foreachstep` or `foreach_<n>`
487
+ - Children are typically a single `LIST` containing the loop body steps
488
+
489
+ **Collecting FOREACH results in JAVASCRIPT:**
490
+ ```javascript
491
+ (steps, context) => {
492
+ const allOutputs = [];
493
+ allOutputs.push(steps["http[0]"].output);
494
+ allOutputs.push(steps["http[1]"].output);
495
+ // ... etc
496
+ return allOutputs;
497
+ }
498
+ ```
499
+
500
+ ---
501
+
502
+ ## WHILE
503
+
504
+ Loops while a condition is true, executing its child steps each iteration.
505
+
506
+ ```json
507
+ {
508
+ "type": "WHILE",
509
+ "children": [
510
+ {
511
+ "type": "LIST",
512
+ "ref": "loop_body",
513
+ "children": [
514
+ { /* WAIT step (throttle iterations) */ },
515
+ { /* JAVASCRIPT step (do work + update state) */ }
516
+ ],
517
+ "input": {}
518
+ }
519
+ ],
520
+ "input": {
521
+ "script": "(steps, context) => {\n return whileloop.iteration < 5;\n}"
522
+ }
523
+ }
524
+ ```
525
+
526
+ **Rules:**
527
+ - `input.script` is a JS function returning a boolean — loop continues while `true`
528
+ - `whileloop.iteration` — built-in counter starting at 0, increments each iteration
529
+ - Child is typically a single `LIST` containing the loop body
530
+ - Always include a `WAIT` step inside the loop body to prevent tight-loop runaway
531
+ - Use `context.state.<key>` to read mutable state that persists across iterations
532
+ - Use `__statePut` in a JAVASCRIPT step to write to state (see context.state below)
533
+ - `steps.<while_ref>.updated` — timestamp of the last loop iteration (use in WAIT as base time)
534
+ - `ref` naming: `new_while_<n>` or `while_<n>`
535
+
536
+ **Loop body with state update:**
537
+ ```javascript
538
+ // WAIT step — throttle using last iteration time
539
+ (steps, context) => {
540
+ return { "waitUntil": steps.my_while_step.updated + 1000 };
541
+ }
542
+
543
+ // JAVASCRIPT step — increment counter in state
544
+ (steps, context) => {
545
+ return {
546
+ "__statePut": {
547
+ "count": (context.state.count || 0) + 1
548
+ }
549
+ };
550
+ }
551
+ ```
552
+
553
+ ---
554
+
555
+ ## context.state and __statePut
556
+
557
+ Workflows support mutable in-run state accessible across all steps via `context.state`.
558
+
559
+ **Reading state:**
560
+ ```javascript
561
+ (steps, context) => {
562
+ const count = context.state.count || 0;
563
+ const lastId = context.state.lastProcessedId;
564
+ // ...
565
+ }
566
+ ```
567
+
568
+ **Writing state** (return `__statePut` from any JAVASCRIPT step):
569
+ ```javascript
570
+ (steps, context) => {
571
+ return {
572
+ "__statePut": {
573
+ "count": (context.state.count || 0) + 1,
574
+ "lastProcessedId": someId
575
+ }
576
+ };
577
+ }
578
+ ```
579
+
580
+ **Rules:**
581
+ - `context.state` is a plain object, readable in any step script
582
+ - To write: return `{ "__statePut": { key: value } }` from a JAVASCRIPT step
583
+ - `__statePut` merges into existing state (not a full replace)
584
+ - State is scoped to the current workflow run — not persisted across runs (use PERSISTED_STATE for that)
585
+ - Common use: loop counters, accumulating results, tracking IDs across iterations
586
+ - Also accessible in HTTP step `params`: `"count": "{{ context.state.count }}"`
587
+
588
+ ---
589
+
590
+ ## {{variables.*}} and {{secrets.*}}
591
+
592
+ Template placeholders available in HTTP step URLs, headers, params, and body content.
593
+
594
+ ```json
595
+ {
596
+ "url": "{{variables.dev_server}}/api/resource",
597
+ "headers": {
598
+ "Authorization": "Bearer {{secrets.unmeshed_test_token}}"
599
+ }
600
+ }
601
+ ```
602
+
603
+ **Rules:**
604
+ - `{{variables.<name>}}` — namespace/environment variables (e.g. base URLs, config values)
605
+ - `{{secrets.<name>}}` — secret values (API tokens, passwords) — never hardcode these
606
+ - Available in HTTP step `url`, `headers`, `params`, and `body.content`
607
+ - Also available as `{{steps.<ref>.output.response.<field>}}` template refs in the same fields
608
+
609
+ ---
610
+
611
+ ## DECISION_ENGINE
612
+
613
+ Routes based on a named decision table (rule engine).
614
+
615
+ ```json
616
+ {
617
+ "type": "DECISION_ENGINE",
618
+ "input": {
619
+ "decisionRuleStrategy": "FIRST_MATCH",
620
+ "decisionTable": "<table-name>",
621
+ "decisionContext": {
622
+ "FieldName": "{{ steps.<ref>.output.results.<field> }}"
623
+ },
624
+ "decisionTableVersion": "",
625
+ "decisionOutputColumns": ["OutputCol1", "OutputCol2"]
626
+ }
627
+ }
628
+ ```
629
+
630
+ **Rules:**
631
+ - `decisionTable` must reference a table configured in Unmeshed
632
+ - `decisionRuleStrategy`: `FIRST_MATCH` (most common) or `ALL_MATCH`
633
+ - `decisionContext` keys must match column names in the decision table
634
+ - Context values can be `{{...}}` template expressions or empty string `""` for optional fields
635
+ - Output columns define what fields are returned from the matched rule
636
+ - Decision output accessed via `steps.<ref>.output.result.<ColumnName>` or `steps.<ref>.output.results.<ColumnName>` — defensively check both
637
+
638
+ ---
639
+
640
+ ## SWITCH
641
+
642
+ Branches the workflow based on script logic. Must have `children` array with branch steps.
643
+
644
+ ```json
645
+ {
646
+ "type": "SWITCH",
647
+ "children": [
648
+ { /* branch step — any type: LIST, INTEGRATION, NOOP, EXIT, JAVASCRIPT */ },
649
+ { /* another branch */ }
650
+ ],
651
+ "input": {
652
+ "script": "(steps, context) => {\n if (condition) return 'branch-a';\n return 'branch-b';\n}",
653
+ "responseMapping": [
654
+ { "targetRef": "<ref-of-branch-a>", "value": "branch-a" },
655
+ { "targetRef": "<ref-of-branch-b>", "value": "branch-b" },
656
+ { "targetRef": "<ref-of-default>", "defaultBranch": true }
657
+ ]
658
+ }
659
+ }
660
+ ```
661
+
662
+ **Rules:**
663
+ - Script must return a string matching one of the `value` fields in `responseMapping`
664
+ - `defaultBranch: true` entry is required — points to the fallback branch ref
665
+ - Children can be **any step type** — `LIST`, `INTEGRATION`, `NOOP`, `EXIT`, `JAVASCRIPT`
666
+ - Only the matched branch executes
667
+ - The `ref` of each child must match the `targetRef` in `responseMapping`
668
+ - A SWITCH branch child that is a single step does NOT need to be wrapped in a LIST
669
+ - Real-world pattern: check for API errors (e.g. rate limiting) and branch to a skip path vs main path
670
+
671
+ ---
672
+
673
+ ## LIST
674
+
675
+ A sequential container of steps. Used as a branch body inside SWITCH or PARALLEL, or as a standalone grouping.
676
+
677
+ ```json
678
+ {
679
+ "type": "LIST",
680
+ "children": [
681
+ { /* step 1 */ },
682
+ { /* step 2 */ },
683
+ { /* step 3 */ }
684
+ ],
685
+ "input": {},
686
+ "output": null
687
+ }
688
+ ```
689
+
690
+ **Rules:**
691
+ - Steps in `children` execute in order
692
+ - Used as a named branch inside SWITCH (`ref` must match SWITCH `responseMapping`)
693
+ - Used as a branch inside PARALLEL (each LIST runs concurrently with others)
694
+ - Can contain any step types including nested SWITCH, PARALLEL, WAIT, FOREACH
695
+ - `input` is always `{}` for LIST
696
+
697
+ ---
698
+
699
+ ## PARALLEL
700
+
701
+ Executes multiple branches simultaneously. Children are typically LIST steps (each containing multiple steps), not just SUB_PROCESS.
702
+
703
+ ```json
704
+ {
705
+ "type": "PARALLEL",
706
+ "children": [
707
+ {
708
+ "type": "LIST",
709
+ "ref": "branch_1",
710
+ "children": [ /* steps */ ],
711
+ "input": {}
712
+ },
713
+ {
714
+ "type": "LIST",
715
+ "ref": "branch_2",
716
+ "children": [ /* steps */ ],
717
+ "input": {}
718
+ }
719
+ ],
720
+ "input": {
721
+ "failIfAnyBranchFails": true
722
+ }
723
+ }
724
+ ```
725
+
726
+ **Rules:**
727
+ - All children execute concurrently
728
+ - `failIfAnyBranchFails: true` — workflow fails if any branch fails (strict mode)
729
+ - `failIfAnyBranchFails: false` — continue even if branches fail (lenient mode)
730
+ - Children are **LIST steps** (for multi-step branches) or **SUB_PROCESS steps** (for sub-workflow fan-out)
731
+ - Each LIST branch has its own `ref` and `children`
732
+ - Steps inside different PARALLEL branches can reference each other's outputs after the PARALLEL completes
733
+
734
+ ---
735
+
736
+ ## SUB_PROCESS
737
+
738
+ Invokes another named Unmeshed workflow.
739
+
740
+ ```json
741
+ {
742
+ "type": "SUB_PROCESS",
743
+ "input": {
744
+ "processName": "<workflow-name>",
745
+ "waitForCompletion": true,
746
+ "input": { /* optional input to pass */ }
747
+ }
748
+ }
749
+ ```
750
+
751
+ **Rules:**
752
+ - `processName` must match the `name` of another workflow in the same org
753
+ - `waitForCompletion: true` — waits for sub-process to finish before continuing
754
+ - `waitForCompletion: false` — fire-and-forget
755
+ - `input` is optional; omit or pass `{}` if no input needed
756
+ - Common pattern: SUB_PROCESS followed by WAIT to allow async side effects (e.g. emails) to propagate before assertions
757
+
758
+ ---
759
+
760
+ ## PERSISTED_STATE
761
+
762
+ Reads or writes persistent key-value state scoped to the org/namespace.
763
+
764
+ ```json
765
+ {
766
+ "type": "PERSISTED_STATE",
767
+ "input": {
768
+ "operation": "READ",
769
+ "name": "<state-key>",
770
+ "path": "$"
771
+ }
772
+ }
773
+ ```
774
+
775
+ For UPSERT (write):
776
+ ```json
777
+ {
778
+ "type": "PERSISTED_STATE",
779
+ "input": {
780
+ "operation": "UPSERT",
781
+ "name": "<state-key>",
782
+ "path": "$",
783
+ "value": "<value-to-store>"
784
+ }
785
+ }
786
+ ```
787
+
788
+ **Rules:**
789
+ - `operation`: `"READ"` or `"UPSERT"`
790
+ - `name`: the key — can be a static string or a `{{...}}` template expression
791
+ - `path`: use `"$"` for root; supports JSONPath for nested access
792
+ - `value`: only for UPSERT — the value to persist
793
+ - READ output: `steps.<ref>.output.result.value` — returns `null` if key doesn't exist
794
+ - Common pattern: READ → SWITCH on whether result is null → UPSERT after creating resource
795
+
796
+ ---
797
+
798
+ ## EXIT
799
+
800
+ Terminates the workflow immediately with a given status.
801
+
802
+ ```json
803
+ {
804
+ "type": "EXIT",
805
+ "input": {
806
+ "message": "Exit message",
807
+ "status": "COMPLETED"
808
+ }
809
+ }
810
+ ```
811
+
812
+ **Rules:**
813
+ - `status`: `"COMPLETED"` or `"FAILED"`
814
+ - Used inside SWITCH branches to short-circuit the workflow
815
+ - Common pattern: gate check via PERSISTED_STATE → SWITCH → EXIT if flag is false
816
+ - Can also be used mid-workflow as a temporary stub (e.g. `"message": "Exiting temporarily"`)
817
+
818
+ ---
819
+
820
+ ## Optional Steps
821
+
822
+ Any step can be marked `"optional": true` to allow the workflow to continue even if that step fails.
823
+
824
+ ```json
825
+ {
826
+ "optional": true,
827
+ ...
828
+ }
829
+ ```
830
+
831
+ **Rules:**
832
+ - Default is `false` — step failure stops the workflow
833
+ - Set `optional: true` for steps that may legitimately fail (e.g. external API calls that might be rate-limited, best-effort cleanup steps)
834
+ - Real-world use: `get_all_tables` in an Airtable workflow marked optional so the SWITCH after it can check for rate-limit errors gracefully