@jiggai/recipes 0.4.18 → 0.4.19
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 +4 -1
- package/docs/BUNDLED_RECIPES.md +3 -0
- package/docs/COMMANDS.md +3 -2
- package/docs/MEMORY_SYSTEM.md +304 -0
- package/docs/SWARM_ORCHESTRATOR.md +317 -0
- package/docs/TEAM_WORKFLOW.md +4 -0
- package/docs/WORKFLOW_EXAMPLES.md +292 -0
- package/docs/WORKFLOW_RUNS_FILE_FIRST.md +575 -0
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/src/lib/workflows/workflow-runner.ts +56 -5
|
@@ -2,8 +2,15 @@
|
|
|
2
2
|
|
|
3
3
|
This document explains how ClawRecipes workflows work in practice.
|
|
4
4
|
|
|
5
|
+
If you want a copy-paste cookbook after reading this reference, also see:
|
|
6
|
+
- [WORKFLOW_EXAMPLES.md](WORKFLOW_EXAMPLES.md)
|
|
7
|
+
|
|
5
8
|
If you are trying to answer any of these questions, start here:
|
|
6
9
|
- Where do workflow files live?
|
|
10
|
+
- What node types are available?
|
|
11
|
+
- What do triggers do?
|
|
12
|
+
- What is a run?
|
|
13
|
+
- How do edges work?
|
|
7
14
|
- How do I run one manually?
|
|
8
15
|
- What does the runner do?
|
|
9
16
|
- What does the worker do?
|
|
@@ -44,6 +51,448 @@ Example:
|
|
|
44
51
|
|
|
45
52
|
---
|
|
46
53
|
|
|
54
|
+
## What a workflow file looks like
|
|
55
|
+
|
|
56
|
+
At a high level, a workflow file contains:
|
|
57
|
+
- workflow metadata (`id`, optional `name`)
|
|
58
|
+
- optional `triggers`
|
|
59
|
+
- `nodes`
|
|
60
|
+
- optional `edges`
|
|
61
|
+
|
|
62
|
+
Minimal example:
|
|
63
|
+
|
|
64
|
+
```json
|
|
65
|
+
{
|
|
66
|
+
"id": "demo",
|
|
67
|
+
"name": "Simple demo",
|
|
68
|
+
"nodes": [
|
|
69
|
+
{ "id": "start", "kind": "start" },
|
|
70
|
+
{
|
|
71
|
+
"id": "append_log",
|
|
72
|
+
"kind": "tool",
|
|
73
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
74
|
+
"action": {
|
|
75
|
+
"tool": "fs.append",
|
|
76
|
+
"args": {
|
|
77
|
+
"path": "shared-context/APPEND_LOG.md",
|
|
78
|
+
"content": "- run={{run.id}}\n"
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
},
|
|
82
|
+
{ "id": "end", "kind": "end" }
|
|
83
|
+
],
|
|
84
|
+
"edges": [
|
|
85
|
+
{ "from": "start", "to": "append_log", "on": "success" },
|
|
86
|
+
{ "from": "append_log", "to": "end", "on": "success" }
|
|
87
|
+
]
|
|
88
|
+
}
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Available workflow node types
|
|
94
|
+
|
|
95
|
+
These are the **current first-class node kinds** supported by the runner.
|
|
96
|
+
|
|
97
|
+
### Quick chooser
|
|
98
|
+
|
|
99
|
+
Use this when you are deciding what kind of node to add:
|
|
100
|
+
|
|
101
|
+
- use **`start`** when you want a visible graph entry point
|
|
102
|
+
- use **`end`** when you want a visible graph exit point
|
|
103
|
+
- use **`llm`** when you want the workflow to generate or transform content with an LLM
|
|
104
|
+
- use **`tool`** when you want the workflow to call a tool or side-effecting action
|
|
105
|
+
- use **`human_approval`** when a person must approve before the workflow continues
|
|
106
|
+
- use **`writeback`** when you want to append workflow breadcrumbs/results into team files
|
|
107
|
+
|
|
108
|
+
### `start`
|
|
109
|
+
Purpose:
|
|
110
|
+
- visual or structural start node
|
|
111
|
+
- useful in UI-authored workflows
|
|
112
|
+
- treated as a no-op by the runner
|
|
113
|
+
|
|
114
|
+
What it does at runtime:
|
|
115
|
+
- marks itself successful
|
|
116
|
+
- does not perform work
|
|
117
|
+
|
|
118
|
+
Example:
|
|
119
|
+
|
|
120
|
+
```json
|
|
121
|
+
{ "id": "start", "kind": "start" }
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
### `end`
|
|
125
|
+
Purpose:
|
|
126
|
+
- visual or structural end node
|
|
127
|
+
- used to make the workflow graph readable
|
|
128
|
+
- treated as a no-op by the runner
|
|
129
|
+
|
|
130
|
+
What it does at runtime:
|
|
131
|
+
- marks itself successful
|
|
132
|
+
- does not perform work
|
|
133
|
+
|
|
134
|
+
Example:
|
|
135
|
+
|
|
136
|
+
```json
|
|
137
|
+
{ "id": "end", "kind": "end" }
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
### `llm`
|
|
141
|
+
Purpose:
|
|
142
|
+
- run an LLM step in the workflow
|
|
143
|
+
- generate structured or text-like content and store it as node output JSON
|
|
144
|
+
|
|
145
|
+
Use it when:
|
|
146
|
+
- you want to draft content
|
|
147
|
+
- you want to transform or summarize earlier node output
|
|
148
|
+
- you want a workflow step to reason over previous results before the next action
|
|
149
|
+
|
|
150
|
+
Required pieces:
|
|
151
|
+
- `assignedTo.agentId`
|
|
152
|
+
- either `action.promptTemplatePath` or `action.promptTemplate`
|
|
153
|
+
|
|
154
|
+
What it does:
|
|
155
|
+
- builds a task prompt
|
|
156
|
+
- calls `llm-task-fixed` if present, otherwise falls back to `llm-task`
|
|
157
|
+
- passes upstream node output when available
|
|
158
|
+
- writes result JSON into the run’s `node-outputs/`
|
|
159
|
+
|
|
160
|
+
Example using an inline prompt:
|
|
161
|
+
|
|
162
|
+
```json
|
|
163
|
+
{
|
|
164
|
+
"id": "draft_post",
|
|
165
|
+
"kind": "llm",
|
|
166
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
167
|
+
"action": {
|
|
168
|
+
"promptTemplate": "Write a short product update for X announcing the new workflow runner."
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Example using a prompt template file:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"id": "draft_post",
|
|
178
|
+
"kind": "llm",
|
|
179
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
180
|
+
"action": {
|
|
181
|
+
"promptTemplatePath": "shared-context/prompts/draft-post.md"
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
```
|
|
185
|
+
|
|
186
|
+
Optional output path override:
|
|
187
|
+
|
|
188
|
+
```json
|
|
189
|
+
{
|
|
190
|
+
"id": "draft_post",
|
|
191
|
+
"kind": "llm",
|
|
192
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
193
|
+
"action": {
|
|
194
|
+
"promptTemplate": "Write a short changelog entry."
|
|
195
|
+
},
|
|
196
|
+
"output": {
|
|
197
|
+
"path": "node-outputs/custom-draft.json"
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### `tool`
|
|
203
|
+
Purpose:
|
|
204
|
+
- run a tool action during the workflow
|
|
205
|
+
|
|
206
|
+
Use it when:
|
|
207
|
+
- you want to write a file
|
|
208
|
+
- send a message
|
|
209
|
+
- publish content
|
|
210
|
+
- call another tool after an LLM step
|
|
211
|
+
|
|
212
|
+
Current behavior:
|
|
213
|
+
- supports runner-native special handling for some tools
|
|
214
|
+
- otherwise falls back to normal tool invocation by tool name
|
|
215
|
+
- stores result/error artifacts in `artifacts/`
|
|
216
|
+
|
|
217
|
+
Currently important built-in / special-cased tools:
|
|
218
|
+
|
|
219
|
+
#### `fs.append`
|
|
220
|
+
Use when you want to append text into a file in the team workspace.
|
|
221
|
+
|
|
222
|
+
Required args:
|
|
223
|
+
- `path`
|
|
224
|
+
- `content`
|
|
225
|
+
|
|
226
|
+
Example:
|
|
227
|
+
|
|
228
|
+
```json
|
|
229
|
+
{
|
|
230
|
+
"id": "append_log",
|
|
231
|
+
"kind": "tool",
|
|
232
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
233
|
+
"action": {
|
|
234
|
+
"tool": "fs.append",
|
|
235
|
+
"args": {
|
|
236
|
+
"path": "shared-context/APPEND_LOG.md",
|
|
237
|
+
"content": "- {{date}} run={{run.id}}\n"
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
Notes:
|
|
244
|
+
- path must stay within the team workspace
|
|
245
|
+
- simple template vars are supported in args like `{{date}}`, `{{run.id}}`, `{{workflow.id}}`
|
|
246
|
+
|
|
247
|
+
#### `outbound.post`
|
|
248
|
+
Use when you want a workflow to publish externally through the supported outbound posting service.
|
|
249
|
+
|
|
250
|
+
Required args:
|
|
251
|
+
- `platform`
|
|
252
|
+
- `text`
|
|
253
|
+
- `idempotencyKey`
|
|
254
|
+
|
|
255
|
+
Required plugin config:
|
|
256
|
+
- `outbound.baseUrl`
|
|
257
|
+
- `outbound.apiKey`
|
|
258
|
+
|
|
259
|
+
Example:
|
|
260
|
+
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"id": "publish_x",
|
|
264
|
+
"kind": "tool",
|
|
265
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
266
|
+
"action": {
|
|
267
|
+
"tool": "outbound.post",
|
|
268
|
+
"args": {
|
|
269
|
+
"platform": "x",
|
|
270
|
+
"text": "Hello from ClawRecipes",
|
|
271
|
+
"idempotencyKey": "{{run.id}}:publish_x",
|
|
272
|
+
"runContext": {
|
|
273
|
+
"teamId": "development-team",
|
|
274
|
+
"workflowId": "marketing",
|
|
275
|
+
"workflowRunId": "{{run.id}}",
|
|
276
|
+
"nodeId": "publish_x"
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
For full posting details, see [OUTBOUND_POSTING.md](OUTBOUND_POSTING.md).
|
|
284
|
+
|
|
285
|
+
#### Other tool names
|
|
286
|
+
If a tool node references some other tool name, the runner will try to invoke that tool by name.
|
|
287
|
+
|
|
288
|
+
Example:
|
|
289
|
+
|
|
290
|
+
```json
|
|
291
|
+
{
|
|
292
|
+
"id": "some_tool_step",
|
|
293
|
+
"kind": "tool",
|
|
294
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
295
|
+
"action": {
|
|
296
|
+
"tool": "message",
|
|
297
|
+
"args": {
|
|
298
|
+
"action": "send",
|
|
299
|
+
"channel": "telegram",
|
|
300
|
+
"target": "123456",
|
|
301
|
+
"message": "Workflow says hello"
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
Whether that works depends on your actual runtime/tool exposure.
|
|
308
|
+
|
|
309
|
+
### `human_approval`
|
|
310
|
+
Purpose:
|
|
311
|
+
- pause a workflow until a human approves or rejects
|
|
312
|
+
|
|
313
|
+
Use it when:
|
|
314
|
+
- the workflow might publish externally
|
|
315
|
+
- a human needs to review copy or generated content
|
|
316
|
+
- the next step is risky enough that you do not want it to happen automatically
|
|
317
|
+
|
|
318
|
+
Required pieces:
|
|
319
|
+
- `assignedTo.agentId`
|
|
320
|
+
- `action.approvalBindingId` (or workflow-level fallback metadata that resolves to one)
|
|
321
|
+
|
|
322
|
+
What it does:
|
|
323
|
+
- writes `approvals/approval.json`
|
|
324
|
+
- sends an approval request message through the bound messaging target
|
|
325
|
+
- changes run status to `awaiting_approval`
|
|
326
|
+
- waits until you record a decision
|
|
327
|
+
|
|
328
|
+
Example:
|
|
329
|
+
|
|
330
|
+
```json
|
|
331
|
+
{
|
|
332
|
+
"id": "approval",
|
|
333
|
+
"kind": "human_approval",
|
|
334
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
335
|
+
"action": {
|
|
336
|
+
"approvalBindingId": "marketing-approval"
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### `writeback`
|
|
342
|
+
Purpose:
|
|
343
|
+
- append workflow run information back into one or more workspace files
|
|
344
|
+
|
|
345
|
+
Use it when:
|
|
346
|
+
- you want a durable audit note in `notes/` or `shared-context/`
|
|
347
|
+
- you want workflow output to leave a breadcrumb for humans
|
|
348
|
+
- you want the run to update a team file after the main work finishes
|
|
349
|
+
|
|
350
|
+
Required pieces:
|
|
351
|
+
- `assignedTo.agentId`
|
|
352
|
+
- `action.writebackPaths[]`
|
|
353
|
+
|
|
354
|
+
What it does:
|
|
355
|
+
- appends a stamped workflow note with run/ticket context into the target file(s)
|
|
356
|
+
|
|
357
|
+
Example:
|
|
358
|
+
|
|
359
|
+
```json
|
|
360
|
+
{
|
|
361
|
+
"id": "write_summary",
|
|
362
|
+
"kind": "writeback",
|
|
363
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
364
|
+
"action": {
|
|
365
|
+
"writebackPaths": [
|
|
366
|
+
"notes/status.md",
|
|
367
|
+
"shared-context/last-run.md"
|
|
368
|
+
]
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
```
|
|
372
|
+
|
|
373
|
+
---
|
|
374
|
+
|
|
375
|
+
## What is **not** currently a first-class built-in node type?
|
|
376
|
+
|
|
377
|
+
People often expect nodes like:
|
|
378
|
+
- `if`
|
|
379
|
+
- `delay`
|
|
380
|
+
- `switch`
|
|
381
|
+
- `loop`
|
|
382
|
+
|
|
383
|
+
Those are **not currently first-class built-in node kinds** in the runner code.
|
|
384
|
+
|
|
385
|
+
So if you want branching today, use:
|
|
386
|
+
- edges (`success`, `error`, `always`)
|
|
387
|
+
- multiple nodes
|
|
388
|
+
- approval or tool-mediated control flow
|
|
389
|
+
|
|
390
|
+
If you want waiting/delay behavior today, do it outside the runner with:
|
|
391
|
+
- cron triggers
|
|
392
|
+
- scheduled reruns
|
|
393
|
+
- approval pause/resume
|
|
394
|
+
|
|
395
|
+
I’m calling this out explicitly so we don’t imply features that do not yet exist as first-class nodes.
|
|
396
|
+
|
|
397
|
+
---
|
|
398
|
+
|
|
399
|
+
## Triggers
|
|
400
|
+
|
|
401
|
+
Triggers answer the question:
|
|
402
|
+
|
|
403
|
+
**“What causes a workflow run to be created?”**
|
|
404
|
+
|
|
405
|
+
In the current schema, a workflow may include `triggers[]`.
|
|
406
|
+
|
|
407
|
+
Current workflow type shape:
|
|
408
|
+
|
|
409
|
+
```json
|
|
410
|
+
{
|
|
411
|
+
"kind": "cron",
|
|
412
|
+
"cron": "0 14 * * 1-5",
|
|
413
|
+
"tz": "America/New_York"
|
|
414
|
+
}
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
### What is currently supported in the type layer?
|
|
418
|
+
- `cron` is the clearly documented trigger kind in the current workflow types
|
|
419
|
+
- manual runs are also supported operationally via CLI, even though that is a run-time invocation mode rather than a trigger stored in the workflow file
|
|
420
|
+
|
|
421
|
+
### Example trigger block
|
|
422
|
+
|
|
423
|
+
```json
|
|
424
|
+
{
|
|
425
|
+
"id": "weekday-summary",
|
|
426
|
+
"triggers": [
|
|
427
|
+
{
|
|
428
|
+
"kind": "cron",
|
|
429
|
+
"cron": "0 14 * * 1-5",
|
|
430
|
+
"tz": "America/New_York"
|
|
431
|
+
}
|
|
432
|
+
],
|
|
433
|
+
"nodes": [
|
|
434
|
+
{ "id": "start", "kind": "start" },
|
|
435
|
+
{ "id": "end", "kind": "end" }
|
|
436
|
+
]
|
|
437
|
+
}
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
Human translation of that example:
|
|
441
|
+
- run on weekdays
|
|
442
|
+
- at 2:00 PM
|
|
443
|
+
- in New York time
|
|
444
|
+
|
|
445
|
+
### Manual trigger example
|
|
446
|
+
You can always create a run manually:
|
|
447
|
+
|
|
448
|
+
```bash
|
|
449
|
+
openclaw recipes workflows run \
|
|
450
|
+
--team-id development-team \
|
|
451
|
+
--workflow-file weekday-summary.workflow.json
|
|
452
|
+
```
|
|
453
|
+
|
|
454
|
+
---
|
|
455
|
+
|
|
456
|
+
## Runs
|
|
457
|
+
|
|
458
|
+
A **run** is one concrete execution of a workflow.
|
|
459
|
+
|
|
460
|
+
If your workflow is the recipe, the run is the actual cooking session.
|
|
461
|
+
|
|
462
|
+
### What a run records
|
|
463
|
+
A run stores:
|
|
464
|
+
- workflow id/name/file
|
|
465
|
+
- team id
|
|
466
|
+
- run id
|
|
467
|
+
- trigger information
|
|
468
|
+
- current run status
|
|
469
|
+
- node states
|
|
470
|
+
- event history
|
|
471
|
+
- node results
|
|
472
|
+
- ticket/lane context when used
|
|
473
|
+
|
|
474
|
+
### Common run statuses you will see
|
|
475
|
+
Examples include:
|
|
476
|
+
- `queued` — the run exists and is waiting to be claimed
|
|
477
|
+
- `running` — the runner/worker is actively moving it forward
|
|
478
|
+
- `awaiting_approval` — the run is paused for human review
|
|
479
|
+
- `completed` — the run finished successfully
|
|
480
|
+
- `rejected` — approval was rejected and the run stopped there
|
|
481
|
+
- `needs_revision` — a rejection pushed the run back into a revise-and-try-again path
|
|
482
|
+
|
|
483
|
+
### Where a run lives
|
|
484
|
+
|
|
485
|
+
```text
|
|
486
|
+
shared-context/workflow-runs/<runId>/run.json
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
### What else is inside the run folder?
|
|
490
|
+
- `node-outputs/` — output from nodes
|
|
491
|
+
- `artifacts/` — tool result payloads
|
|
492
|
+
- `approvals/approval.json` — approval state when needed
|
|
493
|
+
|
|
494
|
+
---
|
|
495
|
+
|
|
47
496
|
## Where workflow runs live
|
|
48
497
|
|
|
49
498
|
Every workflow run gets its own folder under:
|
|
@@ -76,6 +525,75 @@ Important files:
|
|
|
76
525
|
|
|
77
526
|
---
|
|
78
527
|
|
|
528
|
+
## Edges
|
|
529
|
+
|
|
530
|
+
Edges define how the workflow graph moves from one node to another.
|
|
531
|
+
|
|
532
|
+
Current edge shape:
|
|
533
|
+
|
|
534
|
+
```json
|
|
535
|
+
{
|
|
536
|
+
"from": "draft_post",
|
|
537
|
+
"to": "approval",
|
|
538
|
+
"on": "success"
|
|
539
|
+
}
|
|
540
|
+
```
|
|
541
|
+
|
|
542
|
+
### Supported `on` values
|
|
543
|
+
- `success`
|
|
544
|
+
- `error`
|
|
545
|
+
- `always`
|
|
546
|
+
|
|
547
|
+
### What they mean
|
|
548
|
+
#### `success`
|
|
549
|
+
Move to the target node if the source node completed successfully.
|
|
550
|
+
|
|
551
|
+
```json
|
|
552
|
+
{ "from": "draft_post", "to": "approval", "on": "success" }
|
|
553
|
+
```
|
|
554
|
+
|
|
555
|
+
#### `error`
|
|
556
|
+
Move to the target node if the source node failed.
|
|
557
|
+
|
|
558
|
+
```json
|
|
559
|
+
{ "from": "publish_x", "to": "notify_failure", "on": "error" }
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
#### `always`
|
|
563
|
+
Move to the target node whether the source node succeeded or failed.
|
|
564
|
+
|
|
565
|
+
```json
|
|
566
|
+
{ "from": "publish_x", "to": "write_summary", "on": "always" }
|
|
567
|
+
```
|
|
568
|
+
|
|
569
|
+
### Important current semantics
|
|
570
|
+
The current runner behavior is intentionally simple:
|
|
571
|
+
|
|
572
|
+
- if a node has explicit `input.from`, those dependencies behave like **AND**
|
|
573
|
+
- if a node has incoming edges, edge satisfaction behaves like **OR**
|
|
574
|
+
- meaning: if **any** incoming edge condition is satisfied, the node can run
|
|
575
|
+
|
|
576
|
+
Human translation:
|
|
577
|
+
- `input.from` is "wait for all of these upstream node outputs"
|
|
578
|
+
- incoming edges are "if any of these paths became valid, go ahead"
|
|
579
|
+
|
|
580
|
+
That is important. Do not assume complex boolean graph semantics unless you have built them explicitly.
|
|
581
|
+
|
|
582
|
+
### Example: success path + failure path
|
|
583
|
+
|
|
584
|
+
```json
|
|
585
|
+
{
|
|
586
|
+
"edges": [
|
|
587
|
+
{ "from": "draft_post", "to": "approval", "on": "success" },
|
|
588
|
+
{ "from": "draft_post", "to": "notify_failure", "on": "error" },
|
|
589
|
+
{ "from": "approval", "to": "publish_x", "on": "success" },
|
|
590
|
+
{ "from": "publish_x", "to": "write_summary", "on": "always" }
|
|
591
|
+
]
|
|
592
|
+
}
|
|
593
|
+
```
|
|
594
|
+
|
|
595
|
+
---
|
|
596
|
+
|
|
79
597
|
## The two moving parts: runner and worker
|
|
80
598
|
|
|
81
599
|
ClawRecipes splits workflow execution into two roles.
|
|
@@ -255,6 +773,8 @@ So if a workflow runs but does not actually post, check your posting path before
|
|
|
255
773
|
|
|
256
774
|
## Typical end-to-end example
|
|
257
775
|
|
|
776
|
+
This is the operational sequence most people mean when they say, "run the workflow":
|
|
777
|
+
|
|
258
778
|
```bash
|
|
259
779
|
# 1) trigger a run
|
|
260
780
|
openclaw recipes workflows run \
|
|
@@ -280,3 +800,58 @@ openclaw recipes workflows resume \
|
|
|
280
800
|
--team-id development-team \
|
|
281
801
|
--run-id <runId>
|
|
282
802
|
```
|
|
803
|
+
|
|
804
|
+
---
|
|
805
|
+
|
|
806
|
+
## End-to-end example: draft → approve → publish
|
|
807
|
+
|
|
808
|
+
Use this pattern when you want:
|
|
809
|
+
- an LLM to draft content
|
|
810
|
+
- a human to approve it
|
|
811
|
+
- a tool step to publish it only after approval
|
|
812
|
+
|
|
813
|
+
```json
|
|
814
|
+
{
|
|
815
|
+
"id": "marketing-demo",
|
|
816
|
+
"name": "Marketing demo",
|
|
817
|
+
"nodes": [
|
|
818
|
+
{ "id": "start", "kind": "start" },
|
|
819
|
+
{
|
|
820
|
+
"id": "draft_post",
|
|
821
|
+
"kind": "llm",
|
|
822
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
823
|
+
"action": {
|
|
824
|
+
"promptTemplate": "Write a short X post announcing our new workflow system."
|
|
825
|
+
}
|
|
826
|
+
},
|
|
827
|
+
{
|
|
828
|
+
"id": "approval",
|
|
829
|
+
"kind": "human_approval",
|
|
830
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
831
|
+
"action": {
|
|
832
|
+
"approvalBindingId": "marketing-approval"
|
|
833
|
+
}
|
|
834
|
+
},
|
|
835
|
+
{
|
|
836
|
+
"id": "publish_x",
|
|
837
|
+
"kind": "tool",
|
|
838
|
+
"assignedTo": { "agentId": "development-team-lead" },
|
|
839
|
+
"action": {
|
|
840
|
+
"tool": "outbound.post",
|
|
841
|
+
"args": {
|
|
842
|
+
"platform": "x",
|
|
843
|
+
"text": "Hello from ClawRecipes",
|
|
844
|
+
"idempotencyKey": "demo:publish_x"
|
|
845
|
+
}
|
|
846
|
+
}
|
|
847
|
+
},
|
|
848
|
+
{ "id": "end", "kind": "end" }
|
|
849
|
+
],
|
|
850
|
+
"edges": [
|
|
851
|
+
{ "from": "start", "to": "draft_post", "on": "success" },
|
|
852
|
+
{ "from": "draft_post", "to": "approval", "on": "success" },
|
|
853
|
+
{ "from": "approval", "to": "publish_x", "on": "success" },
|
|
854
|
+
{ "from": "publish_x", "to": "end", "on": "success" }
|
|
855
|
+
]
|
|
856
|
+
}
|
|
857
|
+
```
|
package/openclaw.plugin.json
CHANGED
package/package.json
CHANGED
|
@@ -2069,11 +2069,62 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
|
|
|
2069
2069
|
|
|
2070
2070
|
|
|
2071
2071
|
} else if (toolName === 'marketing.post_all') {
|
|
2072
|
-
//
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2072
|
+
// Local-only controller patch for RJ: re-enable X posting via xurl. Supports args.dryRun=true.
|
|
2073
|
+
const { execFile } = await import('node:child_process');
|
|
2074
|
+
const { promisify } = await import('node:util');
|
|
2075
|
+
const execFileP = promisify(execFile);
|
|
2076
|
+
|
|
2077
|
+
const platforms = Array.isArray((toolArgs as Record<string, unknown>)?.['platforms'])
|
|
2078
|
+
? (((toolArgs as Record<string, unknown>)['platforms'] as unknown[]) ?? []).map(String)
|
|
2079
|
+
: ['x'];
|
|
2080
|
+
if (!platforms.includes('x')) {
|
|
2081
|
+
throw new Error('marketing.post_all currently supports X-only on this controller.');
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
const nodeOutputsDir = path.join(runDir, 'node-outputs');
|
|
2085
|
+
const draftsFromNode = String((toolArgs as Record<string, unknown>)?.['draftsFromNode'] ?? '').trim() || 'qc_brand';
|
|
2086
|
+
let text = await loadProposedPostTextFromPriorNode({ runDir, nodeOutputsDir, priorNodeId: draftsFromNode });
|
|
2087
|
+
if (!text?.trim()) throw new Error('marketing.post_all: missing draft text');
|
|
2088
|
+
|
|
2089
|
+
const dryRun = Boolean((toolArgs as Record<string, unknown>)?.['dryRun']);
|
|
2090
|
+
|
|
2091
|
+
// Never publish internal draft/instruction/disclaimer copy.
|
|
2092
|
+
text = text
|
|
2093
|
+
.split(/\r?\n/)
|
|
2094
|
+
.filter((line) => !/draft\s*only/i.test(line))
|
|
2095
|
+
.filter((line) => !/do\s+not\s+post\s+without\s+approval/i.test(line))
|
|
2096
|
+
.filter((line) => !/clawrecipes\s+before\s+openclaw/i.test(line))
|
|
2097
|
+
.filter((line) => !/nothing\s+posts\s+without\s+approval/i.test(line))
|
|
2098
|
+
.join('\n')
|
|
2099
|
+
.trim();
|
|
2100
|
+
|
|
2101
|
+
if (dryRun) {
|
|
2102
|
+
const logRel = path.join('shared-context', 'marketing', 'POST_LOG.md');
|
|
2103
|
+
const logAbs = path.join(teamDir, logRel);
|
|
2104
|
+
await ensureDir(path.dirname(logAbs));
|
|
2105
|
+
await fs.appendFile(
|
|
2106
|
+
logAbs,
|
|
2107
|
+
`- ${new Date().toISOString()} [DRY_RUN] run=${runId} node=${node.id} tool=${toolName} platforms=x\n - text: ${JSON.stringify(text)}\n`,
|
|
2108
|
+
'utf8',
|
|
2109
|
+
);
|
|
2110
|
+
|
|
2111
|
+
const result = { dryRun: true, platformsWouldPost: ['x'], draftText: text, logPath: logRel };
|
|
2112
|
+
await fs.writeFile(artifactPath, JSON.stringify({ ok: true, tool: toolName, args: toolArgs, result }, null, 2) + '\n', 'utf8');
|
|
2113
|
+
} else {
|
|
2114
|
+
const who = await execFileP('xurl', ['whoami'], { timeout: 60_000, env: { ...process.env, NO_COLOR: '1' } });
|
|
2115
|
+
const whoJson = JSON.parse(String((who as { stdout?: string }).stdout ?? ''));
|
|
2116
|
+
const username = String((whoJson as { data?: { username?: string } })?.data?.username ?? '').trim();
|
|
2117
|
+
if (!username) throw new Error('marketing.post_all: could not resolve X username');
|
|
2118
|
+
|
|
2119
|
+
const postRes = await execFileP('xurl', ['post', text], { timeout: 60_000, env: { ...process.env, NO_COLOR: '1' } });
|
|
2120
|
+
const postJson = JSON.parse(String((postRes as { stdout?: string }).stdout ?? ''));
|
|
2121
|
+
const postId = String((postJson as { data?: { id?: string } })?.data?.id ?? '').trim();
|
|
2122
|
+
if (!postId) throw new Error('marketing.post_all: xurl post did not return an id');
|
|
2123
|
+
|
|
2124
|
+
const url = `https://x.com/${username}/status/${postId}`;
|
|
2125
|
+
const result = { platformsPosted: ['x'], x: { postId, url, username } };
|
|
2126
|
+
await fs.writeFile(artifactPath, JSON.stringify({ ok: true, tool: toolName, args: toolArgs, result }, null, 2) + '\n', 'utf8');
|
|
2127
|
+
}
|
|
2077
2128
|
} else {
|
|
2078
2129
|
const toolRes = await toolsInvoke<unknown>(api, {
|
|
2079
2130
|
tool: toolName,
|