woml-cli 1.0.5 → 1.0.7

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,786 +1,355 @@
1
1
  # WOML: Workflow Orchestration Markup Language
2
2
 
3
- WOML is a declarative markup language for building and running production-grade workflow automation. A WOML workflow is a structured, HTML-inspired document that compiles into a typed, durable execution graph. Triggers, steps, control flow, lifecycle hooks, concurrency, and human approvals all live in one readable file with a Rust engine underneath.
3
+ WOML is an HTML-inspired language for building and running workflow automation. It keeps triggers, steps, control flow, approvals, lifecycle hooks, and runtime policies in one readable file—then gives every step real JavaScript when markup alone is not enough.
4
4
 
5
- When a step needs real logic, JavaScript runs inside `<script>`, so there is no ceiling on what a workflow can do. WOML handles everything around that code — execution order, retries, concurrency, lifecycle, human-in-the-loop, external services, and a durable, inspectable history of every run.
6
-
7
- ---
5
+ The `woml-cli` package provides the `woml` command, the Bun script runtime, and the native Rust engine selected for your operating system.
8
6
 
9
7
  ## Install
10
8
 
9
+ WOML requires [Bun](https://bun.sh/) 1.3.14 or later. Install it globally with your preferred package manager:
10
+
11
11
  ```bash
12
- npm install -g woml-cli
12
+ npm install --global woml-cli
13
13
  ```
14
14
 
15
- Or with Bun:
16
-
17
15
  ```bash
18
- bun add -g woml-cli
16
+ bun add --global woml-cli
19
17
  ```
20
18
 
21
- Or with pnpm:
22
-
23
19
  ```bash
24
- pnpm add -g woml-cli
20
+ pnpm add --global woml-cli
25
21
  ```
26
22
 
27
- Verify:
23
+ Verify the installation:
28
24
 
29
25
  ```bash
30
26
  woml --version
31
27
  ```
32
28
 
33
- **Requirements:** macOS (x64, arm64), Linux (x64, glibc), or Windows (x64, arm64). No database to set up the default state store is bundled.
34
-
35
- ---
29
+ Native engines are installed automatically for supported macOS, Linux, and Windows systems. You do not need to install `@woml-org/*` packages directly or configure an external database.
36
30
 
37
- ## Document structure
31
+ ## Quick Start: Route an Order
38
32
 
39
- A workflow is one `<woml>` document with a `<workflow>` root plus the standard containers: `<config>` for runtime policies, `<lifecycle>` for hooks, `<triggers>` for what starts a run, and `<steps>` for what runs.
33
+ Save this as `order-router.woml`:
40
34
 
41
35
  ```xml
42
36
  <woml>
43
- <workflow id="..." name="..." version="1.0.0">
44
- <config concurrency="4" timeout="10m" />
45
- <lifecycle>
46
- <on-success><script>...</script></on-success>
47
- <on-error><script>...</script></on-error>
48
- </lifecycle>
49
- <triggers>...</triggers>
50
- <steps>...</steps>
51
- </workflow>
52
- </woml>
53
- ```
54
-
55
- Runtime bindings available inside every `<script>`:
56
-
57
- - `context.payload` — trigger input
58
- - `context.steps.<id>` — earlier step output
59
- - `context.run` durable run metadata
60
- - `services.http`, `services.database`, `services.slack`, `services.storage`, `services.cache`, `services.event`, `services.messaging` — supervised capabilities (each must be declared by the workflow or its modules)
61
- - `secrets.<NAME>` only the secrets proven necessary at compile time
62
-
63
- Scripts return JSON-compatible values. The Rust engine records every outcome durably.
64
-
65
- ---
66
-
67
- ## Triggers
68
-
69
- Triggers decide **when** a workflow runs. Place one or more inside `<triggers>`.
70
-
71
- ### `<manual>` — run on demand
72
-
73
- The simplest trigger. Starts a run when the operator calls `woml run` with a payload.
74
-
75
- ```xml
76
- <manual id="start" />
77
- ```
78
-
79
- ### `<webhook>` — accept HTTP requests
80
-
81
- Registers a static HTTP route that starts a run for every validated payload.
82
-
83
- ```xml
84
- <webhook id="hook"
85
- path="/webhooks/orders"
86
- method="POST"
87
- auth="bearer"
88
- secret="{{secrets.ORDER_WEBHOOK_TOKEN}}">
89
- <schema>
90
- { "type": "object", "required": ["orderId"], "properties": { "orderId": { "type": "string" } } }
91
- </schema>
92
- </webhook>
93
- ```
94
-
95
- - `auth="bearer"` requires a `secret`; `auth="none"` is for deliberately public routes.
96
- - The inline `<schema>` is JSON Schema Draft 2020-12; invalid payloads return `400 Bad Request` with the `WOML_TRIGGER_SCHEMA_INVALID` code and never start a run.
97
-
98
- ### `<schedule>` — cron expressions
99
-
100
- Starts a run on a WOML Cron v1 schedule. Five numeric fields (`minute hour day-of-month month day-of-week`), wildcards, lists, inclusive ranges, and `/step` are supported. Seconds, names, and Quartz-only tokens are rejected.
101
-
102
- ```xml
103
- <schedule id="daily"
104
- cron="0 9 * * MON-FRI"
105
- timezone="UTC"
106
- on-missed="skip" />
107
- ```
108
-
109
- `timezone` defaults to UTC. `on-missed` chooses `skip` or `run-once` after a restart.
110
-
111
- ### `<interval>` — fixed cadence
112
-
113
- Starts a run on a fixed interval. The compiler must not translate it into cron if semantics would change.
114
-
115
- ```xml
116
- <interval id="heartbeat" every="30s" on-missed="skip" />
117
- ```
118
-
119
- ### `<event>` — react to internal events
120
-
121
- Starts a run when another workflow emits a named event through the durable event bus.
122
-
123
- ```xml
124
- <event id="created"
125
- name="order.created"
126
- secret="{{secrets.EVENT_CONTROL_TOKEN}}">
127
- <schema>
128
- { "type": "object", "required": ["orderId"], "properties": { "orderId": { "type": "string" } } }
129
- </schema>
130
- </event>
131
- ```
132
-
133
- ### `<slack>` — Slack Socket Mode
134
-
135
- Starts a run for Slack workspace events via a single Socket Mode connection per credential pair.
136
-
137
- ```xml
138
- <slack id="msg"
139
- events="app-mention,direct-message"
140
- channels="ops,alerts"
141
- bot-token="{{secrets.SLACK_BOT_TOKEN}}"
142
- app-token="{{secrets.SLACK_APP_TOKEN}}" />
143
- ```
144
-
145
- `events` accepts `app-mention` and `direct-message`. `channels` is optional and limits mentions to a comma-separated set.
146
-
147
- ### `<telegram>` — Telegram long polling
148
-
149
- Starts a run for every incoming Telegram message via long polling and durable admission.
150
-
151
- ```xml
152
- <telegram id="bot"
153
- events="message"
154
- bot-token="{{secrets.TELEGRAM_BOT_TOKEN}}" />
155
- ```
156
-
157
- Telegram v1 supports the single `message` event.
158
-
159
- ### `<discord>` — Discord Gateway
160
-
161
- Starts a run for Discord activity via a shared resumable Gateway connection.
162
-
163
- ```xml
164
- <discord id="bot"
165
- events="app-mention,direct-message"
166
- bot-token="{{secrets.DISCORD_BOT_TOKEN}}" />
167
- ```
168
-
169
- `channels` is optional and accepts comma-separated numeric channel IDs (17–20 digits). Channel names are rejected because they are mutable display labels.
170
-
171
- ### `<whatsapp>` — WhatsApp Cloud API
172
-
173
- Starts a run for inbound WhatsApp messages via signed Meta Cloud API callbacks.
174
-
175
- ```xml
176
- <whatsapp id="bot"
177
- events="message"
178
- phone-number-id="123456789012345"
179
- verify-token="{{secrets.WHATSAPP_VERIFY_TOKEN}}"
180
- app-secret="{{secrets.WHATSAPP_APP_SECRET}}" />
181
- ```
182
-
183
- `phone-number-id` is Meta's durable Phone Number ID, not the display phone number.
184
-
185
- ---
186
-
187
- ## Steps
188
-
189
- Steps run sequentially inside `<steps>`. Each `<step>` returns a value that becomes available at `context.steps.<stepId>`.
190
-
191
- ```xml
192
- <step id="greet">
193
- <script>
194
- return { message: `Hello, ${context.payload.name}!` };
195
- </script>
196
- </step>
197
- ```
198
-
199
- ---
200
-
201
- ## Control flow
202
-
203
- Two compact routing primitives cover most branching needs.
204
-
205
- ### `<choose>` — mutually exclusive routes
206
-
207
- `<choose id="...">` selects the first `<when>` whose `test` reference is true and publishes a merged result at `context.steps.<chooseId>`. The `test` attribute holds exactly one context reference — complex conditions belong in named steps.
208
-
209
- ```xml
210
- <step id="needsReview">
211
- <script>
212
- return { value: context.steps.analysis.risk > 0.3 };
213
- </script>
214
- </step>
215
-
216
- <choose id="reviewRoute">
217
- <when test="{{context.steps.needsReview.value}}">
218
- <step id="humanDecision">
219
- <script>return { routed: 'review' };</script>
220
- </step>
221
- <result value="{{context.steps.humanDecision}}" />
222
- </when>
223
- <otherwise>
224
- <step id="automaticDecision">
225
- <script>return { routed: 'auto' };</script>
226
- </step>
227
- <result value="{{context.steps.automaticDecision}}" />
228
- </otherwise>
229
- </choose>
230
- ```
231
-
232
- ### `<switch>` — exact-string routing
233
-
234
- `<switch id="..." value="...">` compares one context reference against ordered string cases and runs exactly one route.
235
-
236
- ```xml
237
- <switch id="route" value="{{context.steps.classify.intent}}">
238
- <case value="bug">
239
- <step id="sendBugs">
240
- <script>return { routedTo: 'bugs' };</script>
241
- </step>
242
- <result value="{{context.steps.sendBugs}}" />
243
- </case>
244
- <default>
245
- <step id="dropNoise">
246
- <script>return { dropped: true };</script>
247
- </step>
248
- <result value="{{context.steps.dropNoise}}" />
249
- </default>
250
- </switch>
251
- ```
252
-
253
- ---
254
-
255
- ## Concurrent steps — `<parallel>`
256
-
257
- `<parallel>` runs its direct child steps concurrently and joins after they finish. A one-step parallel is a valid degenerate fork/join.
258
-
259
- ```xml
260
- <parallel id="fieldData" concurrency="2" on-error="wait-all">
261
- <step id="loadWeather">
262
- <script>return loadWeather(context.payload.fieldId);</script>
263
- </step>
264
- <step id="loadSoil">
265
- <script>return loadSoil(context.payload.fieldId);</script>
266
- </step>
267
- </parallel>
268
- ```
269
-
270
- - `concurrency` caps simultaneous child steps; defaults to the number of children.
271
- - `on-error` is `fail-fast` (default) or `wait-all`. `fail-fast` stops scheduling new children; `wait-all` lets every child reach its terminal outcome first.
272
- - All children see the same context view from immediately before the fork.
273
- - A child cannot reference a sibling's output.
274
-
275
- For multi-step concurrent routes (each branch holds its own sequence of steps), use `<fork>` and `<branch>` instead.
276
-
277
- ---
278
-
279
- ## Concurrent routes — `<fork>` and `<branch>`
280
-
281
- `<fork>` runs multiple multi-step branches concurrently and joins on a chosen set. Each `<branch>` may contain steps, choices, switches, parallel groups, and approvals. Branches remain sequential internally while overlapping through the multiplexed Bun host.
282
-
283
- ```xml
284
- <fork id="distribution" join="all">
285
- <branch id="tiktok">
286
- <step id="formatTikTok">
287
- <script>return { caption: `${context.steps.campaign.title} #automation` };</script>
288
- </step>
289
- <step id="publishTikTok">
290
- <script>return { platform: 'tiktok', caption: context.steps.formatTikTok.caption };</script>
291
- </step>
292
- </branch>
293
- <branch id="instagram">
294
- <step id="formatInstagram">
295
- <script>return { caption: `${context.steps.campaign.title}\n${context.steps.campaign.url}` };</script>
296
- </step>
297
- <step id="publishInstagram">
298
- <script>return { platform: 'instagram', caption: context.steps.formatInstagram.caption };</script>
299
- </step>
300
- </branch>
301
- </fork>
302
- ```
303
-
304
- - `join="all"` (or omitted) waits for every branch.
305
- - `join="none"` waits for none.
306
- - A whitespace-separated branch-ID list waits only for those branches.
307
- - A branch can read context available before the fork and outputs created earlier in that same branch; it cannot read sibling-branch outputs.
308
- - Nested forks inside a fork-owned branch are rejected.
309
- - A workflow whose only terminal structure is a fork is rejected.
310
-
311
- ---
312
-
313
- ## Human approvals — `<approval>`
314
-
315
- `<approval>` is a first-class durable control-flow item. It records that a run is waiting for a decision, optionally fires notifications, suspends the run, and selects exactly one continuation after the decision arrives.
316
-
317
- ```xml
318
- <approval id="contentApproval"
319
- name="Content approval"
320
- description="Ask a moderator to approve or reject"
321
- timeout="24h"
322
- on-timeout="reject">
323
- <notify>
324
- <slack channels="moderators"
325
- bot-token="{{secrets.SLACK_BOT_TOKEN}}"
326
- app-token="{{secrets.SLACK_APP_TOKEN}}" />
327
- </notify>
328
-
329
- <step id="hold" />
330
- <when-approved>
331
- <step id="publish">
332
- <script>return { published: true };</script>
333
- </step>
334
- <result value="{{context.steps.publish}}" />
335
- </when-approved>
336
- <when-rejected>
337
- <step id="archive">
338
- <script>return { archived: true };</script>
339
- </step>
340
- <result value="{{context.steps.archive}}" />
341
- </when-rejected>
342
- </approval>
343
- ```
344
-
345
- - `timeout` caps how long the approval waits; `on-timeout` chooses `approve`, `reject`, or another arm.
346
- - Optional `<notify>` fires built-in Slack/Telegram/Discord/WhatsApp notifications when the approval is armed.
347
- - Exactly one `<when-approved>` or `<when-rejected>` is selected after the decision arrives.
348
-
349
- ---
350
-
351
- ## Notifications — `<notify>`
352
-
353
- `<notify>` is a container for built-in Slack, Telegram, Discord, or WhatsApp deliveries. It is not a standalone step — it is attached to the parent that arms the notification.
354
-
355
- ### Inside lifecycle hooks
356
-
357
- Fire a notification when the run finishes successfully or fails.
358
-
359
- ```xml
360
- <lifecycle>
361
- <on-success>
362
- <notify>
363
- <slack channels="ops"
364
- bot-token="{{secrets.SLACK_BOT_TOKEN}}"
365
- app-token="{{secrets.SLACK_APP_TOKEN}}" />
366
- </notify>
367
- <script>
368
- console.log('Run completed successfully');
369
- </script>
370
- </on-success>
371
- <on-error>
372
- <notify>
373
- <slack channels="oncall"
374
- bot-token="{{secrets.SLACK_BOT_TOKEN}}"
375
- app-token="{{secrets.SLACK_APP_TOKEN}}" />
376
- </notify>
377
- <script>
378
- console.error('Run failed');
379
- </script>
380
- </on-error>
381
- </lifecycle>
382
- ```
383
-
384
- ### Inside an approval
385
-
386
- Fire a notification when the approval is armed so the right moderator sees the decision request (see the `<approval>` example above).
387
-
388
- A `<notify>` contains one or more built-in provider tags — `<slack>`, `<telegram>`, `<discord>`, or `<whatsapp>` — and must not contain anything else.
389
-
390
- ---
391
-
392
- ## Real examples
393
-
394
- ### Local automation — organize a folder by file type
395
-
396
- Run once, sorts every file into the right subfolder. Zero external services, zero API keys.
397
-
398
- ```xml
399
- <woml>
400
- <workflow id="organize" name="Organize a folder by file type" version="1.0.0">
401
- <triggers><manual id="start" /></triggers>
37
+ <workflow
38
+ id="order-router"
39
+ name="Order Router"
40
+ description="Check inventory and risk concurrently, then route the order."
41
+ version="1.0.0"
42
+ >
43
+ <triggers>
44
+ <webhook
45
+ id="newOrder"
46
+ path="/webhooks/orders"
47
+ method="POST"
48
+ auth="none"
49
+ >
50
+ <schema>
51
+ {
52
+ "type": "object",
53
+ "required": ["orderId", "inStock", "riskScore"],
54
+ "properties": {
55
+ "orderId": { "type": "string" },
56
+ "inStock": { "type": "boolean" },
57
+ "riskScore": { "type": "number" }
58
+ },
59
+ "additionalProperties": false
60
+ }
61
+ </schema>
62
+ </webhook>
63
+ </triggers>
402
64
 
403
65
  <steps>
404
- <step id="scan">
405
- <script>
406
- const { promises: fs } = await import('fs');
407
- const path = await import('path');
408
- const folder = context.payload.path ?? '.';
409
- const entries = await fs.readdir(folder, { withFileTypes: true });
410
- return {
411
- folder,
412
- files: entries
413
- .filter(e => e.isFile())
414
- .map(e => ({ name: e.name, ext: path.extname(e.name).toLowerCase() })),
415
- };
416
- </script>
417
- </step>
418
-
419
- <parallel id="moveAll" concurrency="4">
420
- <step id="moveImages">
421
- <script>
422
- const { promises: fs } = await import('fs');
423
- const path = await import('path');
424
- const { folder, files } = context.steps.scan;
425
- for (const f of files.filter(f => ['.jpg','.jpeg','.png','.gif','.webp','.svg'].includes(f.ext))) {
426
- const from = path.join(folder, f.name);
427
- const to = path.join(folder, 'Images', f.name);
428
- await fs.mkdir(path.dirname(to), { recursive: true });
429
- await fs.rename(from, to);
430
- }
431
- return { ok: true };
432
- </script>
433
- </step>
434
- <step id="moveDocs">
66
+ <parallel
67
+ id="orderChecks"
68
+ name="Run order checks"
69
+ description="Check inventory and risk at the same time."
70
+ concurrency="2"
71
+ on-error="wait-all"
72
+ >
73
+ <step id="inventoryCheck" name="Check inventory">
435
74
  <script>
436
- const { promises: fs } = await import('fs');
437
- const path = await import('path');
438
- const { folder, files } = context.steps.scan;
439
- for (const f of files.filter(f => ['.pdf','.doc','.docx','.txt','.md','.rtf'].includes(f.ext))) {
440
- const from = path.join(folder, f.name);
441
- const to = path.join(folder, 'Docs', f.name);
442
- await fs.mkdir(path.dirname(to), { recursive: true });
443
- await fs.rename(from, to);
444
- }
445
- return { ok: true };
75
+ return { available: context.payload.inStock };
446
76
  </script>
447
77
  </step>
448
- <step id="moveVideos">
449
- <script>
450
- const { promises: fs } = await import('fs');
451
- const path = await import('path');
452
- const { folder, files } = context.steps.scan;
453
- for (const f of files.filter(f => ['.mp4','.mov','.avi','.mkv','.webm'].includes(f.ext))) {
454
- const from = path.join(folder, f.name);
455
- const to = path.join(folder, 'Videos', f.name);
456
- await fs.mkdir(path.dirname(to), { recursive: true });
457
- await fs.rename(from, to);
458
- }
459
- return { ok: true };
460
- </script>
461
- </step>
462
- <step id="moveArchives">
78
+
79
+ <step id="riskCheck" name="Check risk">
463
80
  <script>
464
- const { promises: fs } = await import('fs');
465
- const path = await import('path');
466
- const { folder, files } = context.steps.scan;
467
- for (const f of files.filter(f => ['.zip','.tar','.gz','.7z','.rar'].includes(f.ext))) {
468
- const from = path.join(folder, f.name);
469
- const to = path.join(folder, 'Archives', f.name);
470
- await fs.mkdir(path.dirname(to), { recursive: true });
471
- await fs.rename(from, to);
472
- }
473
- return { ok: true };
81
+ return {
82
+ approved: context.payload.riskScore < 70,
83
+ score: context.payload.riskScore
84
+ };
474
85
  </script>
475
86
  </step>
476
87
  </parallel>
477
88
 
478
- <step id="summary">
89
+ <step
90
+ id="canFulfill"
91
+ name="Make decision"
92
+ description="Combine both check results."
93
+ >
479
94
  <script>
480
95
  return {
481
- message: `Organized ${context.steps.scan.files.length} file(s) into Images/, Docs/, Videos/, Archives/.`
96
+ value:
97
+ context.steps.inventoryCheck.available &&
98
+ context.steps.riskCheck.approved
482
99
  };
483
100
  </script>
484
101
  </step>
485
- </steps>
486
- </workflow>
487
- </woml>
488
- ```
489
-
490
- ```bash
491
- woml run organize.woml --payload '{"path":"/path/to/Downloads"}'
492
- ```
493
-
494
- ---
495
-
496
- ### AI-powered — classify Slack messages and route them to the right channel
497
-
498
- Send every incoming Slack message to an LLM, classify intent, and forward to a dedicated channel.
499
-
500
- ```xml
501
- <woml>
502
- <workflow id="slack-router" version="1.0.0">
503
- <triggers>
504
- <slack id="incoming"
505
- events="app-mention,direct-message"
506
- channels="inbox"
507
- bot-token="{{secrets.SLACK_BOT_TOKEN}}"
508
- app-token="{{secrets.SLACK_APP_TOKEN}}" />
509
- </triggers>
510
-
511
- <steps>
512
- <step id="classify">
513
- <script>
514
- const response = await services.http.request({
515
- method: 'POST',
516
- url: 'https://api.openai.com/v1/chat/completions',
517
- headers: { authorization: `Bearer ${secrets.OPENAI_API_KEY}` },
518
- body: {
519
- model: 'gpt-4o-mini',
520
- messages: [
521
- {
522
- role: 'system',
523
- content: 'Classify the message into one of: bug, feature, question, noise. Reply with JSON { "intent": "...", "confidence": 0..1 }.'
524
- },
525
- { role: 'user', content: context.payload.text }
526
- ],
527
- response_format: { type: 'json_object' }
528
- },
529
- timeoutMs: 10000
530
- });
531
- return JSON.parse(response.body.choices[0].message.content);
532
- </script>
533
- </step>
534
102
 
535
- <switch id="route" value="{{context.steps.classify.intent}}">
536
- <case value="bug">
537
- <step id="sendBugs">
103
+ <choose id="orderRoute" name="Route order">
104
+ <when test="{{context.steps.canFulfill.value}}">
105
+ <step id="acceptOrder" name="Accept order">
538
106
  <script>
539
- await services.slack.send({
540
- channel: '#bugs',
541
- text: `🐛 ${context.payload.text}\n> confidence ${context.steps.classify.confidence}`
542
- });
543
- return { routedTo: 'bugs' };
107
+ return {
108
+ orderId: context.payload.orderId,
109
+ status: "accepted",
110
+ message: `Order ${context.payload.orderId} is ready for fulfillment.`
111
+ };
544
112
  </script>
545
113
  </step>
546
- <result value="{{context.steps.sendBugs}}" />
547
- </case>
548
- <case value="feature">
549
- <step id="sendFeatures">
550
- <script>
551
- await services.slack.send({
552
- channel: '#feature-requests',
553
- text: `💡 ${context.payload.text}`
554
- });
555
- return { routedTo: 'feature-requests' };
556
- </script>
557
- </step>
558
- <result value="{{context.steps.sendFeatures}}" />
559
- </case>
560
- <case value="question">
561
- <step id="sendQuestions">
114
+ <result value="{{context.steps.acceptOrder}}" />
115
+ </when>
116
+
117
+ <otherwise>
118
+ <step id="reviewOrder" name="Request review">
562
119
  <script>
563
- await services.slack.send({
564
- channel: '#questions',
565
- text: `❓ ${context.payload.text}`
566
- });
567
- return { routedTo: 'questions' };
120
+ return {
121
+ orderId: context.payload.orderId,
122
+ status: "review",
123
+ message: `Order ${context.payload.orderId} needs review.`
124
+ };
568
125
  </script>
569
126
  </step>
570
- <result value="{{context.steps.sendQuestions}}" />
571
- </case>
572
- <default>
573
- <step id="dropNoise">
574
- <script>return { dropped: true };</script>
575
- </step>
576
- <result value="{{context.steps.dropNoise}}" />
577
- </default>
578
- </switch>
127
+ <result value="{{context.steps.reviewOrder}}" />
128
+ </otherwise>
129
+ </choose>
130
+
131
+ <step id="response" name="Build response">
132
+ <script>
133
+ return context.steps.orderRoute;
134
+ </script>
135
+ </step>
579
136
  </steps>
580
137
  </workflow>
581
138
  </woml>
582
139
  ```
583
140
 
584
- ---
141
+ Check and activate it:
585
142
 
586
- ### Webhook — flag risky orders, alert Slack
587
-
588
- ```xml
589
- <woml>
590
- <workflow id="order-guard" version="1.0.0">
591
- <triggers>
592
- <webhook id="order"
593
- path="/webhooks/orders"
594
- method="POST"
595
- auth="bearer"
596
- secret="{{secrets.ORDER_WEBHOOK_TOKEN}}">
597
- <schema>
598
- {
599
- "type": "object",
600
- "required": ["orderId", "total", "customerId"],
601
- "properties": {
602
- "orderId": { "type": "string" },
603
- "total": { "type": "number" },
604
- "customerId": { "type": "string" }
605
- }
606
- }
607
- </schema>
608
- </webhook>
609
- </triggers>
610
-
611
- <steps>
612
- <step id="risk">
613
- <script>
614
- const customer = await services.http.request({
615
- method: 'GET',
616
- url: `https://internal.api/customers/${context.payload.customerId}`,
617
- timeoutMs: 5000
618
- });
619
- return {
620
- flagged: context.payload.total > 10000 || customer.body.disputes > 0
621
- };
622
- </script>
623
- </step>
143
+ ```bash
144
+ woml check order-router.woml
145
+ woml run order-router.woml
146
+ ```
624
147
 
625
- <step id="isFlagged">
626
- <script>return { value: context.steps.risk.flagged };</script>
627
- </step>
148
+ WOML prints the active webhook URL and a generated `curl` command. Trigger the workflow from another terminal:
628
149
 
629
- <choose id="alertRoute">
630
- <when test="{{context.steps.isFlagged.value}}">
631
- <step id="alert">
632
- <script>
633
- await services.slack.send({
634
- channel: '#fraud',
635
- text: `High-risk order ${context.payload.orderId} ($${context.payload.total}) needs review.`
636
- });
637
- return { alerted: true };
638
- </script>
639
- </step>
640
- <result value="{{context.steps.alert}}" />
641
- </when>
642
- <otherwise>
643
- <step id="logOk">
644
- <script>return { logged: true };</script>
645
- </step>
646
- <result value="{{context.steps.logOk}}" />
647
- </otherwise>
648
- </choose>
649
- </steps>
650
- </workflow>
651
- </woml>
150
+ ```bash
151
+ curl --request POST http://127.0.0.1:3000/webhooks/orders \
152
+ --header 'content-type: application/json' \
153
+ --data '{"orderId":"order-42","inStock":true,"riskScore":18}'
652
154
  ```
653
155
 
654
- ---
156
+ The request becomes `context.payload`. The two checks run concurrently, `<choose>` selects one route, and every step result is recorded under `context.steps.<id>`.
655
157
 
656
- ### Schedule daily sales report at 9am weekdays
158
+ Use `auth="none"` only for local development. Configure authenticated webhooks before exposing an endpoint outside a trusted environment.
657
159
 
658
- ```xml
659
- <woml>
660
- <workflow id="daily-report" version="1.0.0">
661
- <triggers>
662
- <schedule id="weekdays" cron="0 9 * * MON-FRI" timezone="UTC" />
663
- </triggers>
160
+ ## The Workflow at a Glance
664
161
 
665
- <steps>
666
- <step id="totals">
667
- <script>
668
- const rows = await services.database.query({
669
- sql: "SELECT COUNT(*) AS orders, COALESCE(SUM(total), 0) AS revenue FROM orders WHERE created_at >= date('now', '-1 day')",
670
- parameters: []
671
- });
672
- return rows[0];
673
- </script>
674
- </step>
162
+ ```mermaid
163
+ flowchart TD
164
+ trigger[POST /webhooks/orders] --> parallel{Run concurrently}
165
+ parallel --> inventory[Check inventory]
166
+ parallel --> risk[Check risk]
167
+ inventory --> decision[Make decision]
168
+ risk --> decision
169
+ decision --> route{Route order}
170
+ route -->|Accepted| accept[Accept order]
171
+ route -->|Needs review| review[Request review]
172
+ accept --> response[Build response]
173
+ review --> response
174
+ ```
675
175
 
676
- <step id="publish">
677
- <script>
678
- const { orders, revenue } = context.steps.totals;
679
- await services.slack.send({
680
- channel: '#sales',
681
- text: `Daily report ${orders} orders, $${revenue.toFixed(2)} revenue.`
682
- });
683
- return { sent: true };
684
- </script>
685
- </step>
686
- </steps>
687
- </workflow>
688
- </woml>
176
+ ## Why WOML?
177
+
178
+ - **Readable as a document** the workflow structure is visible without tracing API calls or navigating a canvas.
179
+ - **JavaScript when you need it** — use familiar logic inside `<script>` while WOML supervises the workflow around it.
180
+ - **Durable by default** — runs, attempts, waits, decisions, lifecycle events, and outcomes are recorded by the Rust engine.
181
+ - **Git-native** — `.woml` files produce meaningful diffs and fit normal review and deployment workflows.
182
+ - **Self-hosted** — run locally, on a server, in Docker, or through your own infrastructure.
183
+
184
+ ## Key Features
185
+
186
+ - Manual, webhook, schedule, interval, internal-event, Slack, Telegram, Discord, and WhatsApp triggers.
187
+ - Sequential steps, retries, durable bounded item loops, parallel groups, choices, switches, and multi-step forks with explicit joins.
188
+ - Durable human approvals with Slack, Telegram, Discord, WhatsApp, or custom notification providers.
189
+ - Workflow and step lifecycle hooks with scripts and notifications.
190
+ - Managed HTTP, SQLite/PostgreSQL, storage, cache, state, events, workflow-call, and communication capabilities.
191
+ - Native `fetch()` plus Rust-supervised `services.*` operations.
192
+ - Local JavaScript/TypeScript modules, reusable WOML steps, and reusable notification providers.
193
+ - Runtime concurrency, rate-limit, queue, and timeout policies.
194
+ - Foreground and background operation, colored inspection, durable logs, cancellation, backup, restore, and retention.
195
+
196
+ ## Basic WOML API
197
+
198
+ ### Document tags
199
+
200
+ | Tag | Description |
201
+ | --- | --- |
202
+ | `<woml>` | Root of every WOML document. It contains imports, a workflow, or reusable definitions. |
203
+ | `<imports>` | Declares reusable project dependencies before the workflow. |
204
+ | `<module name="..." from="..." />` | Imports a local JavaScript or TypeScript module as `services.<name>`. |
205
+ | `<workflow>` | Defines one executable workflow and its identity, name, description, and version. |
206
+ | `<config>` | Declares workflow concurrency, rate limit, queue, and timeout policies. |
207
+ | `<lifecycle>` | Contains scripts or notifications that observe workflow and step lifecycle events. |
208
+ | `<triggers>` | Contains the trigger definitions that can create workflow runs. |
209
+ | `<steps>` | Contains the workflow's ordered business flow. |
210
+
211
+ ### Trigger tags
212
+
213
+ | Tag | Description |
214
+ | --- | --- |
215
+ | `<manual>` | Keeps the workflow active and creates a run whenever the operator presses Enter. |
216
+ | `<webhook>` | Creates a run from a validated HTTP request at a static route. |
217
+ | `<schedule>` | Creates runs from a five-field cron schedule and optional timezone. |
218
+ | `<interval>` | Creates runs repeatedly using a fixed duration such as `30s` or `5m`. |
219
+ | `<event>` | Subscribes to a durable named event emitted internally or through authenticated HTTP. |
220
+ | `<slack>` | Receives supported Slack Socket Mode events. |
221
+ | `<telegram>` | Receives human text messages through Telegram long polling. |
222
+ | `<discord>` | Receives supported Discord mentions and direct messages. |
223
+ | `<whatsapp>` | Receives signed WhatsApp Cloud API message callbacks. |
224
+
225
+ ### Steps and control-flow tags
226
+
227
+ | Tag | Description |
228
+ | --- | --- |
229
+ | `<step>` | Defines one named executable operation; its return value becomes `context.steps.<id>`. |
230
+ | `<script>` | Runs JavaScript with the current `context`, `services`, `secrets`, and `attempt` bindings. |
231
+ | `<parallel>` | Runs its direct child steps concurrently and waits for them to finish. |
232
+ | `<for-each>` | Runs its body once per array item with durable identity, bounded concurrency, and ordered aggregate results. |
233
+ | `<choose>` | Selects the first true `<when>` route or its final `<otherwise>` route. |
234
+ | `<when>` / `<otherwise>` | Define the conditional routes inside `<choose>`. |
235
+ | `<result>` | Publishes one stable result from a for-each iteration or selected choice, switch, or approval route. |
236
+ | `<switch>` | Selects one exact-string `<case>` or its `<default>` route. |
237
+ | `<fork>` | Starts several independent, concurrent multi-step branches and joins the selected branches. |
238
+ | `<branch>` | Defines one sequential route inside a `<fork>` and may contain several flow items. |
239
+ | `<approval>` | Pauses durably until a human approves, rejects, or the configured timeout settles. |
240
+ | `<notify>` | Sends approval or lifecycle notifications through configured providers. |
241
+ | `<when-approved>` / `<when-rejected>` | Define the two continuations of an approval decision. |
242
+
243
+ ### Lifecycle tags
244
+
245
+ | Tag | Description |
246
+ | --- | --- |
247
+ | `<on-start>` | Runs when a workflow run begins. |
248
+ | `<on-step-start>` | Runs before matching steps begin; its optional `steps` attribute filters step IDs. |
249
+ | `<on-step-success>` | Runs after matching steps succeed. |
250
+ | `<on-step-failure>` | Runs after matching steps fail. |
251
+ | `<on-step-complete>` | Runs after matching steps reach any terminal outcome. |
252
+ | `<on-success>` | Runs after the workflow succeeds. |
253
+ | `<on-error>` | Runs after the workflow fails. |
254
+ | `<on-cancel>` | Runs after the workflow is cancelled. |
255
+ | `<on-complete>` | Runs last after any workflow outcome. |
256
+
257
+ ## Context
258
+
259
+ `context` is the read-only data available to an ordinary workflow script. It is derived from durable run events rather than treated as an authoritative mutable object.
260
+
261
+ | Reference | Description |
262
+ | --- | --- |
263
+ | `context.payload` | The validated input supplied by the trigger or calling workflow. Manual runs currently receive `{}`. |
264
+ | `context.steps.<id>` | The JSON-compatible result returned by a completed step or result-producing control item. |
265
+ | `context.item` | The current item while a script is running inside `<for-each>`; unavailable outside that loop. |
266
+ | `context.iteration.index` | The current `<for-each>` item's stable, zero-based input index. |
267
+ | `context.iteration.total` | The total number of items captured by the current `<for-each>`. |
268
+
269
+ Return only data that later steps genuinely need. Step results become durable workflow context, while local variables and mutations to `context` do not persist.
270
+
271
+ ```javascript
272
+ const order = context.payload;
273
+ const total = context.steps.calculateTotal.total;
274
+
275
+ return {
276
+ orderId: order.orderId,
277
+ total
278
+ };
689
279
  ```
690
280
 
691
- ---
281
+ ## Services
692
282
 
693
- ### Telegram answer mentions in your team chat
283
+ `services` contains WOML's supervised capabilities and aliases imported through `<module>`. Managed operations cross the Bun-to-Rust boundary for durable outcomes, cancellation, limits, and recovery.
694
284
 
695
- ```xml
696
- <woml>
697
- <workflow id="telegram-echo" version="1.0.0">
698
- <triggers>
699
- <telegram id="incoming"
700
- events="message"
701
- bot-token="{{secrets.TELEGRAM_BOT_TOKEN}}" />
702
- </triggers>
285
+ | Service | Description |
286
+ | --- | --- |
287
+ | `services.http.request()` | Makes a managed HTTP request with status policy, timeout, limits, cancellation, and durable operation history. |
288
+ | `services.db()` | Opens a managed SQLite or PostgreSQL handle for queries, writes, and transactions. |
289
+ | `services.storage` | Stores and retrieves larger checksummed objects outside workflow context. |
290
+ | `services.cache` | Keeps reusable, expiring optimization data that may be safely discarded. |
291
+ | `services.state` | Stores small, versioned workflow-owned values that must survive future runs and restarts. |
292
+ | `services.events.emit()` | Publishes a durable named event to every matching active workflow. |
293
+ | `services.workflows.call()` | Starts one workflow and waits for its final JSON result. |
294
+ | `services.workflows.start()` | Starts one workflow in the background and immediately returns its durable run ID. |
295
+ | `services.telegram.send()` | Sends a supervised Telegram message or reply. |
296
+ | `services.discord.send()` | Sends a supervised Discord message or reply. |
297
+ | `services.whatsapp.send()` | Sends an approved WhatsApp Cloud API template message. |
298
+ | `services.<module>` | Exposes named exports from an imported local JavaScript or TypeScript module. |
703
299
 
704
- <steps>
705
- <step id="reply">
706
- <script>
707
- await services.messaging.send({
708
- channel: 'telegram',
709
- conversationId: context.payload.conversationId,
710
- text: `You said: ${context.payload.text}`
711
- });
712
- return { ok: true };
713
- </script>
714
- </step>
715
- </steps>
716
- </workflow>
717
- </woml>
300
+ For standard Web API compatibility and streaming, scripts may also use Bun's native `fetch()`. Prefer `services.http.request()` when the request needs WOML-managed limits, cancellation, operation identity, and durable supervision.
301
+
302
+ ## Common Commands
303
+
304
+ ```bash
305
+ woml check workflows/ # Parse, validate, and compile
306
+ woml run workflows/ # Activate workflows in the foreground
307
+ woml run workflows/ --background # Activate them in the background
308
+ woml inspect # Open the colored runtime inspector
309
+ woml list # List workflows and recent runs
310
+ woml get run_... # Inspect one run and its history
311
+ woml cancel run_... # Cancel a pending or running workflow
312
+ woml workflow-id --logs # Follow logs for a workflow
313
+ woml secrets set API_TOKEN # Store a secret securely
314
+ woml backup backups/latest # Back up the durable state store
315
+ woml prune --before 30d --dry-run # Preview retention cleanup
718
316
  ```
719
317
 
720
- ---
318
+ Read the complete [CLI reference](https://github.com/dali-benothmen/woml/blob/master/docs/cli-reference.md) for every command and option.
721
319
 
722
- ### Event send a confirmation email when another workflow emits `order.created`
320
+ ## Build Workflows with AI
723
321
 
724
- ```xml
725
- <woml>
726
- <workflow id="order-confirmation" version="1.0.0">
727
- <triggers>
728
- <event id="created"
729
- name="order.created"
730
- secret="{{secrets.EVENT_CONTROL_TOKEN}}">
731
- <schema>
732
- {
733
- "type": "object",
734
- "required": ["orderId", "email"],
735
- "properties": {
736
- "orderId": { "type": "string" },
737
- "email": { "type": "string" }
738
- }
739
- }
740
- </schema>
741
- </event>
742
- </triggers>
322
+ The WOML Skill teaches compatible AI coding agents the released language, services, providers, modules, reliability rules, and CLI. Describe the automation you want and the agent can create the `.woml` file, identify required secrets, validate it, and provide the exact command needed to run it.
743
323
 
744
- <steps>
745
- <step id="confirm">
746
- <script>
747
- await services.http.request({
748
- method: 'POST',
749
- url: 'https://api.emailprovider.com/v1/send',
750
- headers: { authorization: `Bearer ${secrets.EMAIL_API_KEY}` },
751
- body: {
752
- to: context.payload.email,
753
- template: 'order-confirmation',
754
- data: { orderId: context.payload.orderId }
755
- },
756
- timeoutMs: 5000
757
- });
758
- return { sentTo: context.payload.email };
759
- </script>
760
- </step>
761
- </steps>
762
- </workflow>
763
- </woml>
324
+ ```text
325
+ $woml Build an order-processing workflow that starts from a webhook, checks
326
+ inventory and fraud risk concurrently, and asks for approval when risk is high.
764
327
  ```
765
328
 
766
- ---
767
-
768
- ## Common commands
329
+ Install it for Claude Code in the current project:
769
330
 
770
331
  ```bash
771
- woml check workflows/ # Validate workflows without running them
772
- woml run workflows/ # Run in the foreground (Ctrl+C to stop)
773
- woml run workflows/ --background # Run in the background, survives Ctrl+C
774
- woml inspect # Show the current state of all runs
775
- woml list # List known workflows and recent runs
776
- woml get run_... # Print the full event history of a run
777
- woml cancel run_... # Cancel a running or pending run
778
- woml backup backups/latest # Snapshot the durable state store to a file
779
- woml prune --before 30d --dry-run # Preview which old runs would be purged
332
+ mkdir -p .claude/skills/woml
333
+ curl -fsSL https://github.com/dali-benothmen/woml/releases/latest/download/woml-skill.tar.gz \
334
+ | tar -xz -C .claude/skills/woml
780
335
  ```
781
336
 
782
- ---
337
+ For Codex and other Agent Skills-compatible tools, copy the repository's complete [`skills/woml`](https://github.com/dali-benothmen/woml/tree/master/skills/woml) directory into `.agents/skills/woml`. Review generated scripts, service calls, and filesystem access before running them, and never place real secret values in prompts or workflow files.
338
+
339
+ ## Documentation
340
+
341
+ - [Getting started](https://github.com/dali-benothmen/woml/blob/master/docs/getting-started.md)
342
+ - [Language reference](https://github.com/dali-benothmen/woml/blob/master/docs/language-reference.md)
343
+ - [CLI reference](https://github.com/dali-benothmen/woml/blob/master/docs/cli-reference.md)
344
+ - [Services and capabilities](https://github.com/dali-benothmen/woml/blob/master/docs/woml-services.md)
345
+ - [Modules](https://github.com/dali-benothmen/woml/blob/master/docs/woml-modules.md)
346
+ - [Communication providers](https://github.com/dali-benothmen/woml/blob/master/docs/woml-communication-providers.md)
347
+ - [Production deployment](https://github.com/dali-benothmen/woml/blob/master/docs/woml-production-deployment.md)
348
+
349
+ ## Support and Security
350
+
351
+ Use [GitHub Discussions](https://github.com/dali-benothmen/woml/discussions) for questions and [GitHub Issues](https://github.com/dali-benothmen/woml/issues) for reproducible bugs. Report vulnerabilities privately according to the [security policy](https://github.com/dali-benothmen/woml/blob/master/SECURITY.md).
783
352
 
784
353
  ## License
785
354
 
786
- Apache-2.0.
355
+ WOML is released under the [Apache License 2.0](https://github.com/dali-benothmen/woml/blob/master/LICENSE).