woml-cli 1.0.6 → 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 +275 -706
- package/dist/cli.js +223 -218
- package/dist/notification-provider-host.js +28 -28
- package/dist/script-host.js +14 -12
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,786 +1,355 @@
|
|
|
1
1
|
# WOML: Workflow Orchestration Markup Language
|
|
2
2
|
|
|
3
|
-
WOML is
|
|
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
|
-
|
|
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
|
|
12
|
+
npm install --global woml-cli
|
|
13
13
|
```
|
|
14
14
|
|
|
15
|
-
Or with Bun:
|
|
16
|
-
|
|
17
15
|
```bash
|
|
18
|
-
bun add
|
|
16
|
+
bun add --global woml-cli
|
|
19
17
|
```
|
|
20
18
|
|
|
21
|
-
Or with pnpm:
|
|
22
|
-
|
|
23
19
|
```bash
|
|
24
|
-
pnpm add
|
|
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
|
-
|
|
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
|
-
##
|
|
31
|
+
## Quick Start: Route an Order
|
|
38
32
|
|
|
39
|
-
|
|
33
|
+
Save this as `order-router.woml`:
|
|
40
34
|
|
|
41
35
|
```xml
|
|
42
36
|
<woml>
|
|
43
|
-
<workflow
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
<triggers
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
<
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
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
|
-
|
|
449
|
-
|
|
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
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
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
|
|
89
|
+
<step
|
|
90
|
+
id="canFulfill"
|
|
91
|
+
name="Make decision"
|
|
92
|
+
description="Combine both check results."
|
|
93
|
+
>
|
|
479
94
|
<script>
|
|
480
95
|
return {
|
|
481
|
-
|
|
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
|
-
<
|
|
536
|
-
<
|
|
537
|
-
<step id="
|
|
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
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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.
|
|
547
|
-
</
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
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
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
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.
|
|
571
|
-
</
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
</
|
|
578
|
-
</
|
|
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
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
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
|
-
|
|
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
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
158
|
+
Use `auth="none"` only for local development. Configure authenticated webhooks before exposing an endpoint outside a trusted environment.
|
|
657
159
|
|
|
658
|
-
|
|
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
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
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
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
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
|
-
|
|
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
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
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
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
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
|
-
|
|
320
|
+
## Build Workflows with AI
|
|
723
321
|
|
|
724
|
-
|
|
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
|
-
|
|
745
|
-
|
|
746
|
-
|
|
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
|
-
|
|
772
|
-
|
|
773
|
-
|
|
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
|
|
355
|
+
WOML is released under the [Apache License 2.0](https://github.com/dali-benothmen/woml/blob/master/LICENSE).
|