woml-cli 1.0.2 → 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 CHANGED
@@ -1,48 +1,178 @@
1
- # WOML
1
+ <div align="center">
2
2
 
3
- WOML is a markup-first language and durable runtime for workflow automation.
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
- ## Install
5
+ ![WOML banner](./woml.png)
8
6
 
9
- WOML requires Bun 1.3.14 or later:
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
+ [![npm version](https://img.shields.io/npm/v/woml-cli.svg)](https://www.npmjs.com/package/woml-cli)
12
+ [![GitHub stars](https://img.shields.io/github/stars/dali-benothmen/woml.svg?style=social)](https://github.com/dali-benothmen/woml)
13
+ [![License](https://img.shields.io/badge/license-Apache--2.0-blue.svg)](./LICENSE)
14
+ [![Platforms](https://img.shields.io/badge/platforms-macOS%20%7C%20Linux%20%7C%20Windows-lightgrey.svg)]()
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
- The one `woml` package contains the CLI and compiler and selects the native Rust
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 start
69
+ ## Quick example: organize your Downloads folder
21
70
 
22
- Save this as `hello.woml`:
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="prepare" name="Prepare greeting">
81
+ <step id="scan">
38
82
  <script>
39
- return { name: context.payload.name ?? "World" };
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
- <step id="greet" name="Build message">
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
- return { message: `Hello ${context.steps.prepare.name}` };
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
- Then run:
183
+ Save it as `organize.woml` and run it:
54
184
 
55
185
  ```bash
56
- woml check hello.woml
57
- woml run hello.woml
186
+ woml run organize.woml --payload '{"path":"/path/to/Downloads"}'
58
187
  ```
59
188
 
60
- Press Enter to start a run. The automation remains active for the next trigger;
61
- press Ctrl+C to stop it. `woml test hello.woml` is the one-shot form for tests
62
- and CI.
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
- ## Main commands
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
- ```bash
67
- woml --help
68
- woml check <workflow-or-directory>...
69
- woml run <workflow-or-directory>...
70
- woml run workflows/ --background
71
- woml inspect
72
- woml list
73
- woml get <run-id>
74
- woml cancel <run-id>
75
- woml <run-id-or-workflow-id> --logs
76
- woml secrets set <NAME>
77
- woml backup <directory>
78
- woml prune --before 30d --dry-run
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
- `woml run` activates multiple files and directories atomically. This makes
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
- ## Runtime bindings
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
- Inside scripts:
373
+ **→ [Browse all examples](./examples/)**
89
374
 
90
- - `context.payload` is input from the trigger or parent workflow;
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
- Return JSON-compatible data for downstream steps. WOML records durable events
97
- before advancing the compiled graph and fails closed rather than replaying an
98
- ambiguous external effect.
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
- ## Capabilities
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 v1 includes manual and production triggers, retries, choices, switches,
103
- parallel groups, forked branches, approvals, lifecycle hooks, runtime policies,
104
- HTTP, SQL databases, storage, cache, events, durable state, local modules,
105
- reusable steps/providers, communication adapters, workflow call/start, run
106
- inspection, backup, recovery, and retention.
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
- - [Getting started](https://github.com/dali-benothmen/woml/blob/master/docs/getting-started.md)
111
- - [Language reference](https://github.com/dali-benothmen/woml/blob/master/docs/language-reference.md)
112
- - [CLI reference](https://github.com/dali-benothmen/woml/blob/master/docs/cli-reference.md)
113
- - [Examples](https://github.com/dali-benothmen/woml/tree/master/examples)
114
- - [Production deployment](https://github.com/dali-benothmen/woml/blob/master/docs/woml-production-deployment.md)
115
- - [VS Code extension](https://github.com/dali-benothmen/woml/tree/master/woml-vscode)
116
-
117
- For support, use
118
- [GitHub Discussions](https://github.com/dali-benothmen/woml/discussions) or
119
- open a reproducible [issue](https://github.com/dali-benothmen/woml/issues).
120
- Report vulnerabilities privately according to the
121
- [security policy](https://github.com/dali-benothmen/woml/blob/master/SECURITY.md).
122
-
123
- WOML is licensed under the
124
- [Apache License 2.0](https://github.com/dali-benothmen/woml/blob/master/LICENSE).
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.2",
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=0F42131A3741E94664756E2164756E21
36429
+ //# debugId=9F7FAB952101DD6E64756E2164756E21