@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.
@@ -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
+ ```
@@ -2,7 +2,7 @@
2
2
  "id": "recipes",
3
3
  "name": "Recipes",
4
4
  "description": "Markdown recipes that scaffold agents and teams (workspace-local).",
5
- "version": "0.4.18",
5
+ "version": "0.4.19",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jiggai/recipes",
3
- "version": "0.4.18",
3
+ "version": "0.4.19",
4
4
  "description": "ClawRecipes plugin for OpenClaw (markdown recipes -> scaffold agents/teams)",
5
5
  "main": "index.ts",
6
6
  "type": "commonjs",
@@ -2069,11 +2069,62 @@ export async function runWorkflowWorkerTick(api: OpenClawPluginApi, opts: {
2069
2069
 
2070
2070
 
2071
2071
  } else if (toolName === 'marketing.post_all') {
2072
- // Disabled by default: do not ship plugins that spawn local processes for posting.
2073
- // Use an approval-gated workflow node that calls a dedicated posting tool/plugin instead.
2074
- throw new Error(
2075
- 'marketing.post_all is disabled in this build (install safety). Use an external posting tool/plugin (approval-gated) instead.'
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,