workflowskill 0.2.1 → 0.3.0

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/skill/SKILL.md CHANGED
@@ -13,7 +13,7 @@ tags:
13
13
 
14
14
  You are a workflow authoring assistant. When a user describes a task they want to automate, you generate a valid WorkflowSkill YAML definition that a runtime can execute directly. You have full access to Claude Code tools: WebFetch, WebSearch, Read, Write, Bash, and others — use them freely.
15
15
 
16
- **Sections:** Authoring Process | YAML Structure | Step Type Reference | Output Resolution | Expression Language | Iteration Patterns | Patterns | Authoring Rules | Output Format | Validation
16
+ **Sections:** Authoring Process | YAML Structure | Step Type Reference | Output Resolution | Expression Language | Iteration Patterns | Authoring Rules | Output Format | Validation
17
17
 
18
18
  ## How WorkflowSkill Works
19
19
 
@@ -23,15 +23,16 @@ A WorkflowSkill is a declarative workflow definition embedded in a SKILL.md file
23
23
  - **Outputs**: Typed results the workflow produces
24
24
  - **Steps**: An ordered sequence of operations
25
25
 
26
- Each step is one of five types:
26
+ Each step is one of four types:
27
27
 
28
- | Type | Purpose | Tokens |
29
- |------|---------|--------|
30
- | `tool` | Invoke a registered tool (API, function) | 0 |
31
- | `llm` | Call a language model with an explicit prompt | Yes |
32
- | `transform` | Filter, map, or sort data | 0 |
33
- | `conditional` | Branch execution based on a condition | 0 |
34
- | `exit` | Terminate the workflow early with a status | 0 |
28
+ | Type | Purpose |
29
+ |------|---------|
30
+ | `tool` | Invoke a registered tool via the host's ToolAdapter (APIs, functions, LLM calls) |
31
+ | `transform` | Filter, map, or sort data |
32
+ | `conditional` | Branch execution based on a condition |
33
+ | `exit` | Terminate the workflow early with a status |
34
+
35
+ All external calls — including LLM inference — go through `tool` steps. The runtime itself has no LLM dependency. The host registers whatever tools are available in the deployment context.
35
36
 
36
37
  ## Authoring Process
37
38
 
@@ -44,7 +45,7 @@ The user should never have to think about workflow internals. They describe what
44
45
  - If the request is clear, skip directly to Research.
45
46
 
46
47
  ### Phase 2: Research
47
- - **Confirm available tools first.** Before designing tool steps, verify which tools are registered in this deployment. Tool names and availability vary by platform do not assume any specific tool is available without confirmation. If unclear, check the runtime's documentation or ask the user.
48
+ - **Confirm available tools first.** The tools available in `tool` steps are the tools registered in the current runtime context in an interactive agent session, these are the tools listed in your context. No built-in tools are provided by the runtime. All tool names depend on what the host registers. Do not assume any specific tool exists.
48
49
  - If the workflow involves APIs, web services, or web scraping, investigate before generating:
49
50
  1. **WebFetch (primary source)** — Fetch the actual target URL and inspect the raw HTML. This is the ground truth. Look for:
50
51
  - The repeating container element (e.g., `li.result-row`, `div.job-card`)
@@ -59,13 +60,12 @@ The user should never have to think about workflow internals. They describe what
59
60
  ### Phase 3: Generate
60
61
  Design the workflow internally following this checklist, then write the `.md` file:
61
62
 
62
- 1. **Identify data sources** — What tools or APIs are needed? These become `tool` steps.
63
- 2. **Identify judgment points** — Where is LLM reasoning needed? These become `llm` steps. Use the cheapest model that works (haiku for classification, sonnet for complex reasoning).
64
- 3. **Identify data transformations** — What filtering, reshaping, or sorting is needed between steps? These become `transform` steps.
65
- 4. **Identify decision points** — Where does execution branch? These become `conditional` steps.
66
- 5. **Identify exit conditions** — When should the workflow stop early? These become `exit` steps with `condition` guards.
67
- 6. **Wire steps together** — Use `$steps.<id>.output` references to connect outputs to inputs.
68
- 7. **Add error handling** — Mark non-critical steps with `on_error: ignore`. Add `retry` policies for flaky APIs.
63
+ 1. **Identify data sources and operations** — What tools or APIs are needed? These become `tool` steps. All external calls (including LLM inference) are tool steps.
64
+ 2. **Identify data transformations** — What filtering, reshaping, or sorting is needed between steps? These become `transform` steps.
65
+ 3. **Identify decision points** — Where does execution branch? These become `conditional` steps.
66
+ 4. **Identify exit conditions** — When should the workflow stop early? These become `exit` steps with `condition` guards.
67
+ 5. **Wire steps together** — Use `$steps.<id>.output` references to connect outputs to inputs.
68
+ 6. **Add error handling** — Mark non-critical steps with `on_error: ignore`. Add `retry` policies for flaky APIs.
69
69
 
70
70
  Write the workflow `.md` file using the Write tool.
71
71
 
@@ -90,7 +90,7 @@ outputs: # object keyed by name — NOT an array
90
90
 
91
91
  steps:
92
92
  - id: <unique_identifier>
93
- type: tool | llm | transform | conditional | exit
93
+ type: tool | transform | conditional | exit
94
94
  description: <what this step does>
95
95
  # Type-specific fields (see below)
96
96
  inputs: # object keyed by name (the field is "inputs", not "params")
@@ -127,35 +127,18 @@ steps:
127
127
  ```yaml
128
128
  - id: fetch_data
129
129
  type: tool
130
- tool: api.endpoint_name
130
+ tool: api.get_items
131
131
  inputs:
132
- param:
132
+ url:
133
133
  type: string
134
- value: $inputs.query
134
+ value: $inputs.url
135
+ limit:
136
+ type: int
137
+ value: 50
135
138
  outputs:
136
- result:
137
- type: object
138
- value: $result.data # map from raw executor result
139
- ```
140
-
141
- ### LLM Step
142
- ```yaml
143
- - id: summarize
144
- type: llm
145
- model: haiku # optional: haiku, sonnet, opus
146
- prompt: |
147
- Summarize this data in 2-3 sentences.
148
- Data: $steps.fetch_data.output.result
149
-
150
- Write only the summary — no formatting, no preamble.
151
- inputs:
152
- data:
153
- type: object
154
- value: $steps.fetch_data.output.result
155
- outputs:
156
- summary:
157
- type: string
158
- value: $result
139
+ results:
140
+ type: array
141
+ value: $result.items # map from raw executor result
159
142
  ```
160
143
 
161
144
  ### Transform Step
@@ -215,7 +198,7 @@ When you have parallel arrays from different steps that need to be combined into
215
198
  type: array
216
199
  ```
217
200
 
218
- This is a pure data operation — never use an LLM step to merge or zip arrays.
201
+ This is a pure data operation — never use a tool step for merging or zipping arrays when a transform step suffices.
219
202
 
220
203
  ### Transform Step (sort)
221
204
  ```yaml
@@ -309,38 +292,13 @@ outputs:
309
292
 
310
293
  ```yaml
311
294
  outputs:
312
- title:
313
- type: string
314
- value: $result.body.title # maps from raw tool/LLM response
295
+ results:
296
+ type: array
297
+ value: $result.items # maps from the tool's raw response
315
298
  ```
316
299
 
317
300
  This is useful when the raw executor result has a different shape than what downstream steps need. Outputs without `value` pass through from the raw result by key name.
318
301
 
319
- **LLM step outputs require `value`.** LLM steps return the model's raw text (parsed as JSON when valid). Without `value`, downstream `$steps.<id>.output.<key>` references fail for plain text responses.
320
-
321
- **Default: plain text with `value: $result`.** For single-value tasks (summarization, classification, scoring, extraction of one field), instruct the model to return plain text and capture it with `value: $result`:
322
-
323
- ```yaml
324
- - id: classify
325
- type: llm
326
- model: haiku
327
- outputs:
328
- priority:
329
- type: string
330
- value: $result # captures raw text: "high", "medium", or "low"
331
- ```
332
-
333
- **JSON with `value: $result.field` — only when the LLM must generate multiple fields that each require reasoning.** If the output has multiple fields but only one requires LLM judgment, use plain text for the LLM and a `map` transform to zip the LLM output with structural data:
334
-
335
- ```yaml
336
- - id: analyze
337
- type: llm
338
- outputs:
339
- analysis:
340
- type: object
341
- value: $result # parsed JSON object
342
- ```
343
-
344
302
  ## Expression Language
345
303
 
346
304
  Use `$`-prefixed references to wire data between steps:
@@ -389,9 +347,9 @@ inputs:
389
347
 
390
348
  ### Iterating with `each` on Tool Steps
391
349
 
392
- When you need to call an API once per item in a list, use `each` on a tool step. The step runs once per element; `$item` is the current element and `$index` is the 0-based index.
350
+ When you need to call a tool once per item in a list, use `each` on a tool step. The step runs once per element; `$item` is the current element and `$index` is the 0-based index.
393
351
 
394
- **Rate limiting:** The runtime executes iterations sequentially. **Always add `delay` to every `each` loop that calls an external service.** `delay: "1s"` waits 1 second between iterations (not after the last). External APIs rate-limit without warning; a missing `delay` is a latent failure. For tool steps calling HTTP APIs, `delay: "2s"` is a safe default. Always prefer a bulk API endpoint that returns all data in one request. When per-item fetching is unavoidable, add `delay`, a preceding filter step to cap the count (see the `slice_ids` step in the Hacker News example below), and include `retry` with `backoff`.
352
+ **Rate limiting:** The runtime executes iterations sequentially. **Always add `delay` to every `each` loop that calls an external service.** `delay: "1s"` waits 1 second between iterations (not after the last). External APIs rate-limit without warning; a missing `delay` is a latent failure. `delay: "2s"` is a safe default for most APIs. Always prefer a bulk API endpoint that returns all data in one request. When per-item fetching is unavoidable, add `delay`, a preceding filter step to cap the count (see the `slice_items` step in the example below), and include `retry` with `backoff`.
395
353
 
396
354
  **Output collection:** Each iteration's output is collected into an array. If the step declares output `value` mappings using `$result`, the mapping is applied per iteration. The step record's `output` is the array of per-iteration mapped results.
397
355
 
@@ -400,29 +358,31 @@ steps:
400
358
  - id: get_ids
401
359
  type: tool
402
360
  tool: api.list_items
361
+ inputs:
362
+ url: { type: string, value: $inputs.api_url }
403
363
  outputs:
404
- ids:
405
- type: array
364
+ items: { type: array, value: $result.items }
406
365
 
407
366
  - id: fetch_details
408
367
  type: tool
409
- tool: api.get_item # platform-specific tool; use web.scrape for HTML pages
410
- each: $steps.get_ids.output.ids # iterate over ids array
411
- on_error: ignore # skip failed fetches, continue
368
+ tool: api.get_item
369
+ each: $steps.get_ids.output.items # iterate over items array
370
+ delay: "2s" # required: rate limit between calls
371
+ on_error: ignore # skip failed fetches, continue
412
372
  inputs:
413
- url:
373
+ id:
414
374
  type: string
415
- value: "${inputs.base_url}${item}.json"
375
+ value: $item.id # each item's ID from the listing
416
376
  outputs:
417
377
  title:
418
378
  type: string
419
- value: $result.body.title # mapped per iteration via $result
420
- id:
421
- type: int
422
- value: $result.body.id
379
+ value: $result.title # mapped per iteration via $result
380
+ summary:
381
+ type: string
382
+ value: $result.summary
423
383
  ```
424
384
 
425
- After this step, `$steps.fetch_details.output` is an array of `{ title, id }` objects — one per iteration. Use `$steps.fetch_details.output` (the whole array) in downstream steps or workflow outputs.
385
+ After this step, `$steps.fetch_details.output` is an array of `{ title, summary }` objects — one per iteration. Use `$steps.fetch_details.output` (the whole array) in downstream steps or workflow outputs.
426
386
 
427
387
  **Workflow output for each+tool:**
428
388
  ```yaml
@@ -434,16 +394,16 @@ outputs:
434
394
 
435
395
  **Pattern: List → Slice → Fetch Details**
436
396
 
437
- Full example using generic tool names (substitute platform-specific tools as needed):
397
+ Full example fetching a listing then fetching each detail via `each`:
438
398
 
439
399
  ```yaml
440
400
  inputs:
401
+ api_url:
402
+ type: string
403
+ default: "https://api.example.com/items"
441
404
  count:
442
405
  type: int
443
406
  default: 10
444
- base_url:
445
- type: string
446
- default: "https://api.example.com/item/"
447
407
 
448
408
  outputs:
449
409
  items:
@@ -451,231 +411,46 @@ outputs:
451
411
  value: $steps.fetch_details.output
452
412
 
453
413
  steps:
454
- - id: get_ids
414
+ - id: get_listing
455
415
  type: tool
456
416
  tool: api.list_items
457
417
  inputs:
458
- url: { type: string, value: "https://api.example.com/items" }
418
+ url: { type: string, value: $inputs.api_url }
459
419
  outputs:
460
- ids: { type: array, value: $result.body }
420
+ items: { type: array, value: $result.items }
461
421
 
462
- - id: slice_ids
422
+ - id: slice_items
463
423
  type: transform
464
424
  operation: filter
465
425
  where: $index < $inputs.count # cap iteration count to avoid rate limiting
466
426
  inputs:
467
- items: { type: array, value: $steps.get_ids.output.ids }
427
+ items: { type: array, value: $steps.get_listing.output.items }
468
428
  outputs:
469
- ids: { type: array }
429
+ items: { type: array }
470
430
 
471
431
  - id: fetch_details
472
432
  type: tool
473
433
  tool: api.get_item
474
- each: $steps.slice_ids.output.ids
475
- delay: "2s"
476
- retry: { max: 3, delay: "2s", backoff: 1.5 }
477
- on_error: ignore
478
- inputs:
479
- url:
480
- type: string
481
- value: "${inputs.base_url}${item}.json"
482
- outputs:
483
- title: { type: string, value: $result.body.title }
484
- score: { type: int, value: $result.body.score }
485
- url: { type: string, value: $result.body.url }
486
- ```
487
-
488
- ### Iterating with `each` on LLM Steps
489
-
490
- When you have an array of items that each need LLM reasoning (summarization, classification, extraction), use `each` on the LLM step — just like tool steps. **Do not dump the entire array into a single prompt.** Always add `delay` to LLM `each` loops — LLM APIs enforce token-per-minute limits, and `delay: "1s"` is the minimum safe default.
491
-
492
- **Why iterate instead of batch:**
493
- - **Token bounds** — Each call processes one item, so prompt size is predictable and bounded. Batching N items risks hitting context limits when items are large (e.g., HTML content).
494
- - **Error isolation** — If one item produces malformed output, only that item fails. With `on_error: ignore`, the rest succeed. Batching loses *all* results if the model returns one malformed JSON array.
495
- - **Prompt simplicity** — "Summarize this one item" is a trivial prompt. "Parse N items and return an N-element array with exact positional correspondence" is fragile and error-prone.
496
-
497
- **Pattern: `each` + LLM with plain text output + map transform**
498
-
499
- Use plain text output (`value: $result`) for the LLM step, then zip the LLM results with structural data from the source array using a `map` transform:
500
-
501
- ```yaml
502
- steps:
503
- - id: fetch_items
504
- type: tool
505
- tool: api.get_item # platform-specific; use web.scrape for HTML pages
506
- each: $steps.get_ids.output.ids
507
- delay: "2s"
508
- on_error: ignore
509
- inputs:
510
- url:
511
- type: string
512
- value: "${inputs.base_url}${item}.json"
513
- outputs:
514
- title: { type: string, value: $result.body.title }
515
- content: { type: string, value: $result.body.content }
516
-
517
- - id: summarize
518
- type: llm
519
- model: haiku
520
- each: $steps.fetch_items.output # iterate over the collected array
521
- delay: "1s" # rate-limit: 1s pause between iterations
522
- on_error: ignore # skip items that fail
523
- description: Summarize each item individually
524
- prompt: |
525
- Summarize this item in 1-2 sentences.
526
-
527
- Title: $item.title
528
- Content: $item.content
529
-
530
- Write only the summary — no formatting, no preamble.
531
- inputs:
532
- item:
533
- type: object
534
- value: $item
535
- outputs:
536
- summary:
537
- type: string
538
- value: $result # plain text — one summary per iteration
539
-
540
- - id: combine_results
541
- type: transform
542
- operation: map
543
- description: Zip summaries with source data
544
- expression:
545
- title: $item.title
546
- description: $steps.summarize.output[$index].summary
547
- inputs:
548
- items: { type: array, value: $steps.fetch_items.output }
549
- outputs:
550
- items: { type: array }
551
- ```
552
-
553
- After this, `$steps.combine_results.output.items` is an array of `{ title, description }` objects — structural data from the tool step, LLM-generated text from the summarize step.
554
-
555
- **Why plain text over JSON for `each` + LLM:**
556
- - **Fence risk** — Models frequently wrap JSON in markdown fences (`` ```json...``` ``) despite explicit instructions. The runtime parses with `JSON.parse`, which rejects fenced output. Plain text has no parsing step — what the model writes is what you get.
557
- - **Silent failures** — When JSON parsing fails, the output stays as a raw string. `$result.field` on a string returns `undefined`, which propagates as `{}` downstream. No error is thrown — the workflow "succeeds" with empty objects.
558
- - **Structural data doesn't need LLM generation** — Fields like `title`, `id`, `score` already exist in the source data. Only the LLM-generated field (summary, classification, score) needs to come from the model. Use a `map` transform to zip them together.
559
-
560
- **Workflow output for each+LLM:**
561
- ```yaml
562
- outputs:
563
- summaries:
564
- type: array
565
- value: $steps.combine_results.output.items # the zipped array
566
- ```
567
-
568
- **Anti-pattern — JSON output in `each` + LLM:**
569
- ```yaml
570
- # BAD: model may return fenced JSON → parse fails → $result.field returns undefined → silent {}
571
- - id: summarize
572
- type: llm
573
- each: $steps.fetch_items.output
574
- prompt: |
575
- Return a JSON object with "title" and "description" fields.
576
- Respond with raw JSON only — no markdown fences.
577
- outputs:
578
- title: { type: string, value: $result.title } # undefined if fenced
579
- description: { type: string, value: $result.description } # undefined if fenced
580
- ```
581
-
582
- **Anti-pattern — batching all items into one prompt:**
583
- ```yaml
584
- # BAD: unbounded prompt size, all-or-nothing failure, complex output format
585
- - id: summarize
586
- type: llm
587
- prompt: |
588
- Summarize each job below...
589
- Jobs: $steps.fetch_items.output # dumps entire array into prompt
590
- outputs:
591
- roles: { type: array, value: $result } # one malformed response loses everything
592
- ```
593
-
594
- **When bulk IS acceptable:** Use a single LLM call with the full array only when the task requires cross-item reasoning — ranking, deduplication, holistic comparison, or generating a unified summary across all items. If each item can be processed independently, always use `each`.
595
-
596
- ## Web Scraping Pattern
597
-
598
- When a workflow fetches HTML and extracts structured data, follow this recipe:
599
-
600
- ### Step pattern: scrape → guard
601
-
602
- `web.scrape` fetches the URL and applies CSS selectors in one step:
603
-
604
- ```yaml
605
- steps:
606
- - id: scrape_data
607
- type: tool
608
- tool: web.scrape
609
- retry: { max: 3, delay: "2s", backoff: 1.5 }
610
- inputs:
611
- url: { type: string, value: "https://example.com/search" }
612
- headers:
613
- type: object
614
- value: { "User-Agent": "Mozilla/5.0", "Accept": "text/html" }
615
- selector: { type: string, value: "li.result-item" }
616
- fields:
617
- type: object
618
- value:
619
- title: "h3.title"
620
- url: "a.link @href"
621
- id: "@data-pid"
622
- limit: { type: int, value: 50 }
623
- outputs:
624
- items: { type: array, value: $result.results }
625
-
626
- - id: guard_empty
627
- type: exit
628
- condition: $steps.scrape_data.output.items.length == 0
629
- status: success
630
- output: { results: [] }
631
- inputs: {}
632
- outputs: {}
633
- ```
634
-
635
- ### Multi-page scraping with `each`
636
-
637
- When scraping multiple pages, combine `web.scrape` with `each`:
638
-
639
- ```yaml
640
- steps:
641
- - id: get_page_list
642
- type: tool
643
- tool: api.get_sitemap # returns list of page URLs to scrape
644
- outputs:
645
- pages:
646
- type: array
647
-
648
- - id: scrape_pages
649
- type: tool
650
- tool: web.scrape
651
- each: $steps.get_page_list.output.pages
434
+ each: $steps.slice_items.output.items
652
435
  delay: "2s"
653
436
  retry: { max: 3, delay: "2s", backoff: 1.5 }
654
437
  on_error: ignore
655
438
  inputs:
656
- url: { type: string, value: $item }
657
- selector: { type: string, value: "article.post" }
658
- fields:
659
- type: object
660
- value:
661
- title: "h1.title"
662
- body: "div.content"
439
+ id: { type: string, value: $item.id }
663
440
  outputs:
664
- items: { type: array, value: $result.results }
441
+ title: { type: string, value: $result.title }
442
+ summary: { type: string, value: $result.summary }
443
+ score: { type: string, value: $result.score }
665
444
  ```
666
445
 
667
- ### Selector research
668
-
669
- Follow the Research protocol (Authoring Process, Phase 2) before writing selectors. Every selector must be verified against actual fetched HTML.
670
-
671
446
  ## Authoring Rules
672
447
 
673
- 1. **LLM steps are a last resort.** Every LLM step costs tokens. Before reaching for an LLM, ask: can this be done with a tool step, a transform step, an API query parameter, or an exit guard? Filtering, reshaping, sorting, and field extraction are structural operations use tools and transforms. String matching can often be avoided by using categorical fields (department, type, status) with exact equality, or by filtering at the API level. Only use LLM steps for tasks that genuinely require natural language understanding: summarization, classification, sentiment analysis, open-ended generation. If you find yourself using an LLM to match strings, merge arrays, or reshape data — you're doing it wrong.
674
- 2. **Use the cheapest model.** `haiku` for classification/scoring, `sonnet` for complex reasoning.
448
+ 1. **Use tool steps for all external calls.** Every interaction with an API, database, or LLM is a tool step. The runtime dispatches tool steps to whatever tools the host registers — the workflow author should use the exact tool names available in this deployment context. Do not invent tool names.
449
+ 2. **Use transforms for pure data operations.** Filtering, reshaping, sorting, and field extraction are structural operations — use `transform` steps. Do not use tool steps to reshape data that can be expressed as a transform.
675
450
  3. **Always declare inputs and outputs.** They enable validation and composability.
676
451
  4. **Use `value` on workflow outputs** to explicitly map step results to workflow outputs. Use `$steps.<id>.output.<field>` expressions. This is preferred over exit steps for producing output.
677
- 5. **Use `value` on step outputs** to map fields from the raw executor result using `$result`. Required for LLM steps (which return raw text/JSON). Useful for tool steps when the response shape differs from what downstream steps need.
678
- 6. **Use `each` for per-item processing** on both tool and LLM steps. Always include `delay` on every `each` loop that calls an external service — `delay: "2s"` for tool steps (including `web.scrape`), `delay: "1s"` for LLM steps. See *Iteration Patterns*.
452
+ 5. **Use `value` on step outputs** to map fields from the raw executor result using `$result`. Useful when the tool's response shape differs from what downstream steps need.
453
+ 6. **Use `each` for per-item processing** on tool steps. Always include `delay` on every `each` loop that calls an external service — `delay: "2s"` is a safe default. See *Iteration Patterns*.
679
454
  7. **Add `on_error: ignore` for non-critical steps** like notifications.
680
455
  8. **Add `retry` for external API calls** (tool steps that might fail transiently).
681
456
  9. **Use `condition` guards for early exits** rather than letting empty data flow through.
@@ -684,11 +459,9 @@ Follow the Research protocol (Authoring Process, Phase 2) before writing selecto
684
459
  12. **`condition` on a `conditional` step is the branch condition**, not a guard.
685
460
  13. **Use exit steps for conditional early termination only**, not as the default way to produce output. Exit output keys must match the declared workflow output keys.
686
461
  14. **Transform steps are for arrays only.** Never use a transform to extract fields from a single object.
687
- 15. **Use `map` with `$index` for cross-array merging.** When multiple steps produce parallel arrays, use a `map` transform with bracket indexing (`$steps.other.output.field[$index]`) to zip them into structured objects. Never use an LLM step for pure data restructuring.
688
- 16. **When JSON output is used, LLM prompts must say "raw JSON only no markdown fences, no commentary."** Models default to wrapping JSON in ``` fences. The runtime parses the raw text with `JSON.parse`, which rejects fenced output. Every prompt that expects JSON output must explicitly instruct the model to respond with raw JSON. Put this instruction **last** in the prompt, after all data and task description, immediately before the model generates. This exploits recency bias — the last instruction the model sees is the most influential, especially when data references expand to large content that can push earlier instructions out of focus. Also describe the exact expected shape (e.g., "Your entire response must be a valid JSON object starting with { and ending with }").
689
- 17. **Guard expensive steps behind deterministic exits.** Pattern: fetch filter exit guard LLM. Use deterministic expressions (e.g., `$item.department == "Engineering"` or `$item.title contains "Product Manager"`) in `transform filter` steps before any LLM call. See *Patterns*.
690
- 18. **Prefer bulk endpoints over per-item iteration.** When per-item `each` + tool calls (including `web.scrape`) are unavoidable, always add `delay: "2s"` (minimum), cap iteration count, and add `retry` with `backoff`. `delay` is not optional — external sites and APIs rate-limit without warning and delays are free. Same applies to `each` + `llm` steps: always add `delay: "1s"`. See *Iteration Patterns*.
691
- 19. **Prefer plain text LLM output over JSON.** For single-value tasks (summarization, classification, scoring), use `value: $result` and instruct the model to return plain text. Reserve JSON (`value: $result.field`) for multi-field output where every field requires LLM reasoning. In `each` + LLM patterns, always use plain text + a `map` transform to zip LLM output with structural data from the source array. See *Iteration Patterns*.
462
+ 15. **Use `map` with `$index` for cross-array merging.** When multiple steps produce parallel arrays, use a `map` transform with bracket indexing (`$steps.other.output.field[$index]`) to zip them into structured objects.
463
+ 16. **Guard expensive steps behind deterministic exits.** Pattern: fetch filter exit guard expensive tool. Use deterministic expressions (e.g., `$item.department == "Engineering"` or `$item.title contains "Product Manager"`) in `transform filter` steps before any costly tool call. See *Patterns*.
464
+ 17. **Prefer bulk endpoints over per-item iteration.** When per-item `each` + tool calls are unavoidable, always add `delay: "2s"` (minimum), cap iteration count, and add `retry` with `backoff`. `delay` is not optional external APIs rate-limit without warning. See *Iteration Patterns*.
692
465
 
693
466
  ## Output Format
694
467
 
@@ -710,9 +483,9 @@ description: Fetches data and outputs a specific field
710
483
 
711
484
  ` `` `workflow
712
485
  inputs:
713
- id:
486
+ url:
714
487
  type: string
715
- default: "1"
488
+ default: "https://api.example.com/items"
716
489
 
717
490
  outputs:
718
491
  name:
@@ -722,15 +495,15 @@ outputs:
722
495
  steps:
723
496
  - id: fetch
724
497
  type: tool
725
- tool: some.tool
498
+ tool: api.get_item
726
499
  inputs:
727
- id:
500
+ url:
728
501
  type: string
729
- value: $inputs.id
502
+ value: $inputs.url
730
503
  outputs:
731
504
  name:
732
505
  type: string
733
- value: $result.result.name
506
+ value: $result.name
734
507
  ` `` `
735
508
  ```
736
509
 
@@ -745,11 +518,8 @@ After writing the file, always validate it against the runtime. The validation c
745
518
  - [ ] `each` not used on exit or conditional steps
746
519
  - [ ] Workflow outputs have `value` mapping to `$steps` references
747
520
  - [ ] Step output `value` uses `$result` (not `$steps`)
748
- - [ ] LLM step outputs have `value` using `$result`
749
521
  - [ ] All `${}` template references resolve to declared inputs/steps
750
- - [ ] LLM prompts expecting JSON include "raw JSON only no markdown fences" instruction
751
- - [ ] LLM steps with `each` prefer plain text output (`value: $result`) over JSON (`value: $result.field`)
752
- - [ ] Every `each` loop that calls an external service has `delay` (tool steps: `"2s"` minimum; LLM steps: `"1s"` minimum)
753
- - [ ] `each` + `web.scrape` steps are bounded (preceded by a cap) and have `retry` with `backoff`
522
+ - [ ] Every `each` loop that calls an external service has `delay` (`"2s"` minimum)
523
+ - [ ] `each` + tool steps with per-item fetching are bounded (preceded by a cap) and have `retry` with `backoff`
754
524
 
755
525
  If validation fails, fix the errors and revalidate.
@@ -1 +0,0 @@
1
- export { };