woml-cli 1.0.1 → 1.0.4
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 +384 -76
- package/dist/cli.js +2 -2
- package/dist/cli.js.map +1 -1
- package/package.json +7 -7
package/README.md
CHANGED
|
@@ -1,48 +1,178 @@
|
|
|
1
|
-
|
|
1
|
+
<div align="center">
|
|
2
2
|
|
|
3
|
-
WOML
|
|
4
|
-
Define triggers, steps, control flow, approvals, lifecycle behavior, and runtime
|
|
5
|
-
policy in readable `.woml` files; write ordinary JavaScript inside `<script>`.
|
|
3
|
+
# WOML: Workflow Orchestration Markup Language
|
|
6
4
|
|
|
7
|
-
|
|
5
|
+

|
|
8
6
|
|
|
9
|
-
WOML
|
|
7
|
+
### If you can read HTML, you can use WOML to automate anything, literally anything.
|
|
8
|
+
|
|
9
|
+
<!-- WOML banner image placeholder: ./docs/assets/banner.png -->
|
|
10
|
+
|
|
11
|
+
[](https://www.npmjs.com/package/woml-cli)
|
|
12
|
+
[](https://github.com/dali-benothmen/woml)
|
|
13
|
+
[](./LICENSE)
|
|
14
|
+
[]()
|
|
15
|
+
|
|
16
|
+
</div>
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
WOML is a markup language for building and running workflow automation. A workflow written in WOML is a document you can read top to bottom, its triggers, steps, control flow, approvals, and lifecycle all expressed as clear, HTML-inspired structure instead of tangled code or an unreadable diagram.
|
|
21
|
+
|
|
22
|
+
When a step needs real logic, JavaScript is always available inside `<script>`, so there is no ceiling on what a workflow can do. WOML handles everything _around_ that code: execution order, retries, concurrency, human approvals, external services, and a durable, inspectable history of every run. The result is automation that scales without becoming spaghetti, that your whole team can read, and that you can actually trust in production.
|
|
23
|
+
|
|
24
|
+
## Why another automation tool?
|
|
25
|
+
|
|
26
|
+
Five nodes in n8n or Zapier feels like magic. Twenty nodes feels like a crime scene.
|
|
27
|
+
|
|
28
|
+
The canvas turns into spaghetti, a single run takes a lifetime, and the moment the built-in integrations fall short you end up stuffing JavaScript into a tiny textbox in a browser UI, no version control, no code review, no idea what changed last Tuesday.
|
|
29
|
+
|
|
30
|
+
WOML takes a different bet: your workflow is **a file**. It reads like HTML, so anyone on the team can follow it. Every step can run real JavaScript with any npm package, so you never hit a wall. It lives in git, so every change is a diff and a review. And the engine underneath is Rust, so it stays fast when your workflows get big, because big workflows are exactly what WOML is built for.
|
|
31
|
+
|
|
32
|
+
## Why WOML
|
|
33
|
+
|
|
34
|
+
- **Readable as a document.** A workflow is structure you can read, diff, and review, not a canvas of boxes and wires that turns into spaghetti as it grows.
|
|
35
|
+
- **No ceiling.** Common actions are clean tags; when you need real logic, drop into `<script>` with full JavaScript and any npm package. Automate anything, literally anything.
|
|
36
|
+
- **Triggers for everything.** Manual, webhook, schedule, interval, event, Slack, Telegram, Discord, and WhatsApp, and you can build your own providers for anything else.
|
|
37
|
+
- **Built to run in production.** Durable run history, retries, concurrency, lifecycle hooks, and a fold-from-events core mean you can always see what happened, track down errors, and replay it.
|
|
38
|
+
- **Human-in-the-loop.** Pause a workflow for approvals with real notifications and durable waiting.
|
|
39
|
+
- **Modular.** Reusable step definitions, local modules, and workflows that call other workflows.
|
|
40
|
+
- **Fast core.** The execution engine is written in Rust for speed and reliability under load.
|
|
41
|
+
- **Free and runs anywhere.** Open source, self-hosted, runs on macOS, Linux, and Windows.
|
|
42
|
+
|
|
43
|
+
## Installation
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
npm i -g woml-cli
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
Or with Bun:
|
|
10
50
|
|
|
11
51
|
```bash
|
|
12
52
|
bun add --global woml-cli
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Or with pnpm:
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pnpm add -g woml-cli
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
This installs the `woml` command:
|
|
62
|
+
|
|
63
|
+
```bash
|
|
13
64
|
woml --version
|
|
14
65
|
```
|
|
15
66
|
|
|
16
|
-
|
|
17
|
-
engine for the current supported platform. Users do not install a compiler or
|
|
18
|
-
native package separately.
|
|
67
|
+
**Requirements:** macOS (x64, arm64), Linux (x64, glibc), or Windows (x64, arm64). No database to set up, the default state store is bundled.
|
|
19
68
|
|
|
20
|
-
## Quick
|
|
69
|
+
## Quick example: organize your Downloads folder
|
|
21
70
|
|
|
22
|
-
|
|
71
|
+
Run this once and it sorts every file in a folder into the right place — images, documents, videos, archives — using conditional logic, a real filesystem loop, and zero external services.
|
|
23
72
|
|
|
24
73
|
```xml
|
|
25
74
|
<woml>
|
|
26
|
-
<workflow
|
|
27
|
-
id="hello"
|
|
28
|
-
name="Hello WOML"
|
|
29
|
-
description="Build a greeting from two durable steps."
|
|
30
|
-
version="1.0.0"
|
|
31
|
-
>
|
|
75
|
+
<workflow id="organize" name="Organize a folder by file type">
|
|
32
76
|
<triggers>
|
|
33
77
|
<manual id="start" />
|
|
34
78
|
</triggers>
|
|
35
79
|
|
|
36
80
|
<steps>
|
|
37
|
-
<step id="
|
|
81
|
+
<step id="scan">
|
|
38
82
|
<script>
|
|
39
|
-
|
|
83
|
+
const { promises: fs } = await import('fs');
|
|
84
|
+
const path = await import('path');
|
|
85
|
+
const folder = context.payload.path ?? '.';
|
|
86
|
+
const entries = await fs.readdir(folder, { withFileTypes: true });
|
|
87
|
+
return {
|
|
88
|
+
folder,
|
|
89
|
+
files: entries
|
|
90
|
+
.filter(e => e.isFile())
|
|
91
|
+
.map(e => ({ name: e.name, ext: path.extname(e.name).toLowerCase() })),
|
|
92
|
+
};
|
|
40
93
|
</script>
|
|
41
94
|
</step>
|
|
42
95
|
|
|
43
|
-
<
|
|
96
|
+
<for-each id="organize" source="{{context.steps.scan.files}}">
|
|
97
|
+
<step id="classify">
|
|
98
|
+
<script>
|
|
99
|
+
return { ext: context.item.ext };
|
|
100
|
+
</script>
|
|
101
|
+
</step>
|
|
102
|
+
|
|
103
|
+
<choose>
|
|
104
|
+
<when test="{{context.steps.classify.ext in ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg']}}">
|
|
105
|
+
<step id="move-image">
|
|
106
|
+
<script>
|
|
107
|
+
const { promises: fs } = await import('fs');
|
|
108
|
+
const path = await import('path');
|
|
109
|
+
const from = path.join(context.steps.scan.folder, context.item.name);
|
|
110
|
+
const to = path.join(context.steps.scan.folder, 'Images', context.item.name);
|
|
111
|
+
await fs.mkdir(path.dirname(to), { recursive: true });
|
|
112
|
+
await fs.rename(from, to);
|
|
113
|
+
return { movedTo: 'Images' };
|
|
114
|
+
</script>
|
|
115
|
+
</step>
|
|
116
|
+
</when>
|
|
117
|
+
<when test="{{context.steps.classify.ext in ['.pdf', '.doc', '.docx', '.txt', '.md', '.rtf']}}">
|
|
118
|
+
<step id="move-doc">
|
|
119
|
+
<script>
|
|
120
|
+
const { promises: fs } = await import('fs');
|
|
121
|
+
const path = await import('path');
|
|
122
|
+
const from = path.join(context.steps.scan.folder, context.item.name);
|
|
123
|
+
const to = path.join(context.steps.scan.folder, 'Docs', context.item.name);
|
|
124
|
+
await fs.mkdir(path.dirname(to), { recursive: true });
|
|
125
|
+
await fs.rename(from, to);
|
|
126
|
+
return { movedTo: 'Docs' };
|
|
127
|
+
</script>
|
|
128
|
+
</step>
|
|
129
|
+
</when>
|
|
130
|
+
<when test="{{context.steps.classify.ext in ['.mp4', '.mov', '.avi', '.mkv', '.webm']}}">
|
|
131
|
+
<step id="move-video">
|
|
132
|
+
<script>
|
|
133
|
+
const { promises: fs } = await import('fs');
|
|
134
|
+
const path = await import('path');
|
|
135
|
+
const from = path.join(context.steps.scan.folder, context.item.name);
|
|
136
|
+
const to = path.join(context.steps.scan.folder, 'Videos', context.item.name);
|
|
137
|
+
await fs.mkdir(path.dirname(to), { recursive: true });
|
|
138
|
+
await fs.rename(from, to);
|
|
139
|
+
return { movedTo: 'Videos' };
|
|
140
|
+
</script>
|
|
141
|
+
</step>
|
|
142
|
+
</when>
|
|
143
|
+
<when test="{{context.steps.classify.ext in ['.zip', '.tar', '.gz', '.7z', '.rar', '.dmg']}}">
|
|
144
|
+
<step id="move-archive">
|
|
145
|
+
<script>
|
|
146
|
+
const { promises: fs } = await import('fs');
|
|
147
|
+
const path = await import('path');
|
|
148
|
+
const from = path.join(context.steps.scan.folder, context.item.name);
|
|
149
|
+
const to = path.join(context.steps.scan.folder, 'Archives', context.item.name);
|
|
150
|
+
await fs.mkdir(path.dirname(to), { recursive: true });
|
|
151
|
+
await fs.rename(from, to);
|
|
152
|
+
return { movedTo: 'Archives' };
|
|
153
|
+
</script>
|
|
154
|
+
</step>
|
|
155
|
+
</when>
|
|
156
|
+
<otherwise>
|
|
157
|
+
<step id="move-misc">
|
|
158
|
+
<script>
|
|
159
|
+
const { promises: fs } = await import('fs');
|
|
160
|
+
const path = await import('path');
|
|
161
|
+
const from = path.join(context.steps.scan.folder, context.item.name);
|
|
162
|
+
const to = path.join(context.steps.scan.folder, 'Misc', context.item.name);
|
|
163
|
+
await fs.mkdir(path.dirname(to), { recursive: true });
|
|
164
|
+
await fs.rename(from, to);
|
|
165
|
+
return { movedTo: 'Misc' };
|
|
166
|
+
</script>
|
|
167
|
+
</step>
|
|
168
|
+
</otherwise>
|
|
169
|
+
</choose>
|
|
170
|
+
</for-each>
|
|
171
|
+
|
|
172
|
+
<step id="summary">
|
|
44
173
|
<script>
|
|
45
|
-
|
|
174
|
+
const total = context.steps.scan.files.length;
|
|
175
|
+
return { message: `Organized ${total} file(s) into Images/, Docs/, Videos/, Archives/, Misc/.` };
|
|
46
176
|
</script>
|
|
47
177
|
</step>
|
|
48
178
|
</steps>
|
|
@@ -50,75 +180,253 @@ Save this as `hello.woml`:
|
|
|
50
180
|
</woml>
|
|
51
181
|
```
|
|
52
182
|
|
|
53
|
-
|
|
183
|
+
Save it as `organize.woml` and run it:
|
|
54
184
|
|
|
55
185
|
```bash
|
|
56
|
-
woml
|
|
57
|
-
woml run hello.woml
|
|
186
|
+
woml run organize.woml --payload '{"path":"/path/to/Downloads"}'
|
|
58
187
|
```
|
|
59
188
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
189
|
+
WOML scans the folder, loops over every file with `<for-each>`, routes each one through a `<choose>` decision by extension, and moves it into the right subfolder with `fs.rename`. Press Ctrl+C to stop the run.
|
|
190
|
+
|
|
191
|
+
```mermaid
|
|
192
|
+
graph TD
|
|
193
|
+
A[Manual trigger] --> B[Scan folder for files]
|
|
194
|
+
B --> C[For each file]
|
|
195
|
+
C --> D{File extension?}
|
|
196
|
+
D -->|jpg, png, gif, webp, svg| E[Move to Images/]
|
|
197
|
+
D -->|pdf, doc, txt, md| F[Move to Docs/]
|
|
198
|
+
D -->|mp4, mov, avi, mkv| G[Move to Videos/]
|
|
199
|
+
D -->|zip, tar, gz, dmg| H[Move to Archives/]
|
|
200
|
+
D -->|anything else| I[Move to Misc/]
|
|
201
|
+
E --> J[Summary: organized N files]
|
|
202
|
+
F --> J
|
|
203
|
+
G --> J
|
|
204
|
+
H --> J
|
|
205
|
+
I --> J
|
|
206
|
+
```
|
|
63
207
|
|
|
64
|
-
|
|
208
|
+
This is one WOML file. No setup, no cloud, no API keys. It runs locally, pulls in real npm packages (`fs`, `path`) on demand, and exercises the same primitives that make n8n workflows become spaghetti and Zapier workflows impossible: `<choose>` for conditional routing, `<for-each>` for iteration, and `<script>` for the parts that need real code.
|
|
65
209
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
woml
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
210
|
+
## Real workflows in WOML
|
|
211
|
+
|
|
212
|
+
Four triggers, four real automations. Each is a complete, runnable workflow.
|
|
213
|
+
|
|
214
|
+
**Webhook — flag risky orders and alert Slack:**
|
|
215
|
+
|
|
216
|
+
```xml
|
|
217
|
+
<woml>
|
|
218
|
+
<workflow id="order-guard">
|
|
219
|
+
<triggers>
|
|
220
|
+
<webhook id="order" path="/webhooks/orders" method="POST"
|
|
221
|
+
secret="{{secrets.ORDER_WEBHOOK_TOKEN}}">
|
|
222
|
+
<schema>
|
|
223
|
+
{
|
|
224
|
+
"type": "object",
|
|
225
|
+
"required": ["orderId", "total", "customerId"],
|
|
226
|
+
"properties": {
|
|
227
|
+
"orderId": { "type": "string" },
|
|
228
|
+
"total": { "type": "number" },
|
|
229
|
+
"customerId": { "type": "string" }
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
</schema>
|
|
233
|
+
</webhook>
|
|
234
|
+
</triggers>
|
|
235
|
+
<steps>
|
|
236
|
+
<step id="risk">
|
|
237
|
+
<script>
|
|
238
|
+
const customer = await services.http.request({
|
|
239
|
+
method: 'GET',
|
|
240
|
+
url: `https://internal.api/customers/${context.payload.customerId}`,
|
|
241
|
+
timeoutMs: 5000
|
|
242
|
+
});
|
|
243
|
+
return { flagged: context.payload.total > 10000 || customer.body.disputes > 0 };
|
|
244
|
+
</script>
|
|
245
|
+
</step>
|
|
246
|
+
</steps>
|
|
247
|
+
<choose>
|
|
248
|
+
<when test="context.steps.risk.flagged">
|
|
249
|
+
<step id="alert">
|
|
250
|
+
<script>
|
|
251
|
+
await services.slack.send({
|
|
252
|
+
channel: '#fraud',
|
|
253
|
+
text: `High-risk order ${context.payload.orderId} ($${context.payload.total}) needs review.`
|
|
254
|
+
});
|
|
255
|
+
return { alerted: true };
|
|
256
|
+
</script>
|
|
257
|
+
</step>
|
|
258
|
+
</when>
|
|
259
|
+
</choose>
|
|
260
|
+
</workflow>
|
|
261
|
+
</woml>
|
|
262
|
+
```
|
|
263
|
+
|
|
264
|
+
**Schedule — a daily sales report, every weekday at 9am:**
|
|
265
|
+
|
|
266
|
+
```xml
|
|
267
|
+
<woml>
|
|
268
|
+
<workflow id="daily-report">
|
|
269
|
+
<triggers>
|
|
270
|
+
<schedule id="weekdays" cron="0 9 * * MON-FRI" timezone="UTC" />
|
|
271
|
+
</triggers>
|
|
272
|
+
<steps>
|
|
273
|
+
<step id="totals">
|
|
274
|
+
<script>
|
|
275
|
+
const rows = await services.database.query({
|
|
276
|
+
sql: "SELECT COUNT(*) AS orders, COALESCE(SUM(total), 0) AS revenue FROM orders WHERE created_at >= date('now', '-1 day')",
|
|
277
|
+
parameters: []
|
|
278
|
+
});
|
|
279
|
+
return rows[0];
|
|
280
|
+
</script>
|
|
281
|
+
</step>
|
|
282
|
+
<step id="publish">
|
|
283
|
+
<script>
|
|
284
|
+
const { orders, revenue } = context.steps.totals;
|
|
285
|
+
await services.slack.send({
|
|
286
|
+
channel: '#sales',
|
|
287
|
+
text: `Daily report — ${orders} orders, $${revenue.toFixed(2)} revenue.`
|
|
288
|
+
});
|
|
289
|
+
return { sent: true };
|
|
290
|
+
</script>
|
|
291
|
+
</step>
|
|
292
|
+
</steps>
|
|
293
|
+
</workflow>
|
|
294
|
+
</woml>
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
**Telegram — answer `/sales` from your team chat:**
|
|
298
|
+
|
|
299
|
+
```xml
|
|
300
|
+
<woml>
|
|
301
|
+
<workflow id="telegram-sales">
|
|
302
|
+
<triggers>
|
|
303
|
+
<telegram id="ask" events="message" commands="/sales"
|
|
304
|
+
bot-token="{{secrets.TELEGRAM_BOT_TOKEN}}" />
|
|
305
|
+
</triggers>
|
|
306
|
+
<steps>
|
|
307
|
+
<step id="lookup">
|
|
308
|
+
<script>
|
|
309
|
+
const rows = await services.database.query({
|
|
310
|
+
sql: "SELECT COUNT(*) AS today FROM orders WHERE created_at >= date('now')",
|
|
311
|
+
parameters: []
|
|
312
|
+
});
|
|
313
|
+
return { today: rows[0].today };
|
|
314
|
+
</script>
|
|
315
|
+
</step>
|
|
316
|
+
<step id="notify">
|
|
317
|
+
<script>
|
|
318
|
+
await services.slack.send({
|
|
319
|
+
channel: '#sales',
|
|
320
|
+
text: `Today's numbers were requested on Telegram: ${context.steps.lookup.today} orders so far.`
|
|
321
|
+
});
|
|
322
|
+
return { ok: true };
|
|
323
|
+
</script>
|
|
324
|
+
</step>
|
|
325
|
+
</steps>
|
|
326
|
+
</workflow>
|
|
327
|
+
</woml>
|
|
79
328
|
```
|
|
80
329
|
|
|
81
|
-
|
|
82
|
-
call-only workflows available to `services.workflows.call()` and
|
|
83
|
-
`services.workflows.start()` in the same runtime. Webhook startup prints its URL
|
|
84
|
-
and a generated `curl`; manual triggers print the keyboard instruction.
|
|
330
|
+
**Event — send a confirmation email whenever another workflow emits `order.created`:**
|
|
85
331
|
|
|
86
|
-
|
|
332
|
+
```xml
|
|
333
|
+
<woml>
|
|
334
|
+
<workflow id="order-confirmation">
|
|
335
|
+
<triggers>
|
|
336
|
+
<event id="created" name="order.created" secret="{{secrets.EVENT_CONTROL_TOKEN}}">
|
|
337
|
+
<schema>
|
|
338
|
+
{
|
|
339
|
+
"type": "object",
|
|
340
|
+
"required": ["orderId", "email"],
|
|
341
|
+
"properties": {
|
|
342
|
+
"orderId": { "type": "string" },
|
|
343
|
+
"email": { "type": "string" }
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
</schema>
|
|
347
|
+
</event>
|
|
348
|
+
</triggers>
|
|
349
|
+
<steps>
|
|
350
|
+
<step id="confirm">
|
|
351
|
+
<script>
|
|
352
|
+
await services.http.request({
|
|
353
|
+
method: 'POST',
|
|
354
|
+
url: 'https://api.emailprovider.com/v1/send',
|
|
355
|
+
headers: { authorization: `Bearer ${secrets.EMAIL_API_KEY}` },
|
|
356
|
+
body: {
|
|
357
|
+
to: context.payload.email,
|
|
358
|
+
template: 'order-confirmation',
|
|
359
|
+
data: { orderId: context.payload.orderId }
|
|
360
|
+
},
|
|
361
|
+
timeoutMs: 5000
|
|
362
|
+
});
|
|
363
|
+
return { sentTo: context.payload.email };
|
|
364
|
+
</script>
|
|
365
|
+
</step>
|
|
366
|
+
</steps>
|
|
367
|
+
</workflow>
|
|
368
|
+
</woml>
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
Every script gets explicit runtime bindings — `context.payload` (trigger input), `context.steps.<id>` (earlier step output), `services.*` (supervised capabilities like HTTP, database, Slack, storage, cache), and `secrets.*` (only the secrets proven necessary at compile time). Scripts return JSON-compatible values; the Rust engine records every outcome durably.
|
|
87
372
|
|
|
88
|
-
|
|
373
|
+
**→ [Browse all examples](./examples/)**
|
|
89
374
|
|
|
90
|
-
|
|
91
|
-
- `context.steps.<id>` is a visible successful step/control-flow result;
|
|
92
|
-
- `services` exposes supervised capabilities and imported modules;
|
|
93
|
-
- `secrets.NAME` resolves only declared secrets; and
|
|
94
|
-
- `attempt` describes the current durable retry attempt.
|
|
375
|
+
## WOML vs alternatives
|
|
95
376
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
377
|
+
| Tool | How you write it | Readable by the whole team | Self-hosted | No ceiling |
|
|
378
|
+
| -------------- | ------------------- | ----------------------------- | ----------- | ---------- |
|
|
379
|
+
| **WOML** | Markup + JavaScript | ✅ | ✅ | ✅ |
|
|
380
|
+
| n8n | Visual canvas | ⚠️ Until it becomes spaghetti | ✅ | ❌ |
|
|
381
|
+
| Zapier | Visual canvas | ⚠️ Until it becomes spaghetti | ❌ | ❌ |
|
|
382
|
+
| Temporal | Code (TS/Go/Java) | ❌ | ✅ | ✅ |
|
|
383
|
+
| Step Functions | JSON (ASL) | ❌ | ❌ | ⚠️ |
|
|
384
|
+
| Airflow | Python DAGs | ❌ | ✅ | ✅ |
|
|
99
385
|
|
|
100
|
-
|
|
386
|
+
WOML is the only one that combines all three: readable as markup, free and self-hosted, and unlimited in what it can express.
|
|
101
387
|
|
|
102
|
-
WOML
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
388
|
+
## What WOML includes
|
|
389
|
+
|
|
390
|
+
- Manual, webhook, schedule, interval, event, Slack, Telegram, Discord, and WhatsApp triggers.
|
|
391
|
+
- Sequential steps, retries, parallel groups, choices, switches, and forked multi-step branches.
|
|
392
|
+
- Durable approvals with provider notifications and shared decisions.
|
|
393
|
+
- Workflow and step lifecycle hooks.
|
|
394
|
+
- Built-in HTTP, SQL database, storage, cache, event, durable-state, messaging, and workflow call/start services.
|
|
395
|
+
- Local JavaScript/TypeScript modules, reusable WOML steps, and custom notification providers.
|
|
396
|
+
- Runtime concurrency, rate-limit, queue, and timeout policies.
|
|
397
|
+
- Foreground and background operation, run inspection, log following, backup, recovery, and retention.
|
|
398
|
+
- A VS Code extension with HTML-style markup and embedded JavaScript syntax.
|
|
399
|
+
|
|
400
|
+
## Common commands
|
|
401
|
+
|
|
402
|
+
```bash
|
|
403
|
+
woml check workflows/ # Validate workflows without running them
|
|
404
|
+
woml run workflows/ # Run in the foreground (Ctrl+C to stop)
|
|
405
|
+
woml run workflows/ --background # Run in the background, survives Ctrl+C
|
|
406
|
+
woml inspect # Show the current state of all runs
|
|
407
|
+
woml list # List known workflows and recent runs
|
|
408
|
+
woml get run_... # Print the full event history of a run
|
|
409
|
+
woml cancel run_... # Cancel a running or pending run
|
|
410
|
+
woml backup backups/latest # Snapshot the durable state store to a file
|
|
411
|
+
woml prune --before 30d --dry-run # Preview which old runs would be purged
|
|
412
|
+
```
|
|
413
|
+
|
|
414
|
+
See the [CLI reference](docs/cli-reference.md) for every command and option.
|
|
107
415
|
|
|
108
416
|
## Documentation
|
|
109
417
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
[Apache License 2.0](
|
|
418
|
+
Full documentation — language reference, all tags, triggers, control flow, services, modules, and production deployment — lives in the docs.
|
|
419
|
+
|
|
420
|
+
**→ [Read the full documentation](./docs/README.md)**
|
|
421
|
+
|
|
422
|
+
## Support and security
|
|
423
|
+
|
|
424
|
+
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. Please report vulnerabilities privately according to the [security policy](SECURITY.md).
|
|
425
|
+
|
|
426
|
+
## Contributing
|
|
427
|
+
|
|
428
|
+
Contributions are welcome — bug reports, feature ideas, docs, or a custom provider. See [CONTRIBUTING.md](./CONTRIBUTING.md) to get started.
|
|
429
|
+
|
|
430
|
+
## License
|
|
431
|
+
|
|
432
|
+
WOML is released under the [Apache License 2.0](./LICENSE).
|
package/dist/cli.js
CHANGED
|
@@ -6870,7 +6870,7 @@ import { dirname as dirname7, extname as extname2, join as join5, resolve as res
|
|
|
6870
6870
|
// package.json
|
|
6871
6871
|
var package_default = {
|
|
6872
6872
|
name: "woml-cli",
|
|
6873
|
-
version: "1.0.
|
|
6873
|
+
version: "1.0.4",
|
|
6874
6874
|
private: false,
|
|
6875
6875
|
description: "WOML workflow automation runtime and command-line interface",
|
|
6876
6876
|
author: "Mohamed Ali Ben Othmen",
|
|
@@ -36426,4 +36426,4 @@ export {
|
|
|
36426
36426
|
activationIdentity
|
|
36427
36427
|
};
|
|
36428
36428
|
|
|
36429
|
-
//# debugId=
|
|
36429
|
+
//# debugId=9F7FAB952101DD6E64756E2164756E21
|