workflowskill 0.1.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/LICENSE +21 -0
- package/README.md +242 -0
- package/dist/cli/index.d.mts +1 -0
- package/dist/cli/index.mjs +187 -0
- package/dist/cli/index.mjs.map +1 -0
- package/dist/gmail-CYBJ6Dxa.mjs +209 -0
- package/dist/gmail-CYBJ6Dxa.mjs.map +1 -0
- package/dist/html-select-BZPYAr6k.mjs +104 -0
- package/dist/html-select-BZPYAr6k.mjs.map +1 -0
- package/dist/http-request-k9bp5joL.mjs +91 -0
- package/dist/http-request-k9bp5joL.mjs.map +1 -0
- package/dist/index.d.mts +637 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +61 -0
- package/dist/index.mjs.map +1 -0
- package/dist/runtime-BY1CFnew.mjs +2003 -0
- package/dist/runtime-BY1CFnew.mjs.map +1 -0
- package/dist/sheets-CGy8JvVz.mjs +177 -0
- package/dist/sheets-CGy8JvVz.mjs.map +1 -0
- package/package.json +75 -0
- package/skill/SKILL.md +725 -0
package/skill/SKILL.md
ADDED
|
@@ -0,0 +1,725 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: workflow-author
|
|
3
|
+
description: Generate valid WorkflowSkill YAML from natural language descriptions. Teaches Claude Code to author executable workflow definitions.
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
tags:
|
|
6
|
+
- workflow
|
|
7
|
+
- automation
|
|
8
|
+
- authoring
|
|
9
|
+
- code-generation
|
|
10
|
+
---
|
|
11
|
+
|
|
12
|
+
# WorkflowSkill Author
|
|
13
|
+
|
|
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
|
+
|
|
16
|
+
**Sections:** Authoring Process | YAML Structure | Step Type Reference | Output Resolution | Expression Language | Iteration Patterns | Patterns | Authoring Rules | Output Format | Validation
|
|
17
|
+
|
|
18
|
+
## How WorkflowSkill Works
|
|
19
|
+
|
|
20
|
+
A WorkflowSkill is a declarative workflow definition embedded in a SKILL.md file as a fenced `workflow` code block. It defines:
|
|
21
|
+
|
|
22
|
+
- **Inputs**: Typed parameters the workflow accepts
|
|
23
|
+
- **Outputs**: Typed results the workflow produces
|
|
24
|
+
- **Steps**: An ordered sequence of operations
|
|
25
|
+
|
|
26
|
+
Each step is one of five types:
|
|
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 |
|
|
35
|
+
|
|
36
|
+
## Authoring Process
|
|
37
|
+
|
|
38
|
+
The user should never have to think about workflow internals. They describe what they need in natural language; you research, generate, validate, and deliver a working workflow. No proposal step, no asking for confirmation mid-flow. The output should feel like magic.
|
|
39
|
+
|
|
40
|
+
### Phase 1: Understand
|
|
41
|
+
- Read the request carefully. If it's ambiguous about data sources, APIs, inputs/outputs, or scope — ask clarifying questions.
|
|
42
|
+
- Ask at most 2-3 focused questions at a time. Offer specific options.
|
|
43
|
+
- Bad: "What do you want to do?" Good: "Should results be filtered by date, category, or both?"
|
|
44
|
+
- If the request is clear, skip directly to Research.
|
|
45
|
+
|
|
46
|
+
### 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
|
+
- If the workflow involves APIs, web services, or web scraping, investigate before generating:
|
|
49
|
+
1. **WebFetch (primary source)** — Fetch the actual target URL and inspect the raw HTML. This is the ground truth. Look for:
|
|
50
|
+
- The repeating container element (e.g., `li.result-row`, `div.job-card`)
|
|
51
|
+
- CSS classes on child elements that hold the data you need (title, price, URL, etc.)
|
|
52
|
+
- Whether data lives in element text, attributes (`href`, `data-*`), or both
|
|
53
|
+
2. **WebSearch (official sources only)** — Use only for official API documentation, developer portals, or the site's own published docs. Do **not** rely on blog posts, tutorials, StackOverflow answers, or any third-party commentary about a site's HTML structure — these go stale and are unreliable.
|
|
54
|
+
3. **Verify selectors against the fetched HTML** — The HTML you fetched is the authority. Confirm every selector you plan to use appears in the actual markup.
|
|
55
|
+
4. **Prefer bulk endpoints over per-item fetching** — Before designing a workflow that iterates over items and fetches each one individually, check whether the API provides a bulk alternative: list endpoints with include/expand parameters (e.g., `?content=true`, `?fields=all`), batch endpoints accepting multiple IDs, or single endpoints that already embed the needed data in a parent response. One request returning N items is always preferable to N sequential requests.
|
|
56
|
+
- Summarize what you found: the container selector, the field selectors, and which are text vs. attributes. Note whether the API has bulk/batch endpoints that eliminate per-item fetching.
|
|
57
|
+
- **Do not guess selectors.** If you cannot verify the HTML structure, tell the user what you need.
|
|
58
|
+
|
|
59
|
+
### Phase 3: Generate
|
|
60
|
+
Design the workflow internally following this checklist, then write the `.md` file:
|
|
61
|
+
|
|
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.
|
|
69
|
+
|
|
70
|
+
Write the workflow `.md` file using the Write tool.
|
|
71
|
+
|
|
72
|
+
### Phase 4: Validate & Test
|
|
73
|
+
- Validate the workflow against the runtime. If validation fails, fix the errors and revalidate.
|
|
74
|
+
- Run the workflow to verify it works end-to-end.
|
|
75
|
+
- For workflows with conditional exits, test both execution paths (e.g., "results found" vs. "no results"). If the primary path targets data that might currently be empty, test with known data to verify the non-empty path works.
|
|
76
|
+
- If the test reveals issues (malformed LLM output, wrong field mappings, broken expressions), fix the workflow and re-test.
|
|
77
|
+
|
|
78
|
+
## YAML Structure
|
|
79
|
+
|
|
80
|
+
```yaml
|
|
81
|
+
inputs: # object keyed by name — NOT an array
|
|
82
|
+
<name>:
|
|
83
|
+
type: string | int | float | boolean | array | object
|
|
84
|
+
default: <optional> # default value for optional inputs
|
|
85
|
+
|
|
86
|
+
outputs: # object keyed by name — NOT an array
|
|
87
|
+
<name>:
|
|
88
|
+
type: string | int | float | boolean | array | object
|
|
89
|
+
value: <$expression> # optional — resolves from $steps context after all steps
|
|
90
|
+
|
|
91
|
+
steps:
|
|
92
|
+
- id: <unique_identifier>
|
|
93
|
+
type: tool | llm | transform | conditional | exit
|
|
94
|
+
description: <what this step does>
|
|
95
|
+
# Type-specific fields (see below)
|
|
96
|
+
inputs: # object keyed by name (the field is "inputs", not "params")
|
|
97
|
+
<name>:
|
|
98
|
+
type: <type> # required
|
|
99
|
+
value: <$expression or literal> # the value: expression ($-prefixed) or literal
|
|
100
|
+
outputs:
|
|
101
|
+
<name>:
|
|
102
|
+
type: <type>
|
|
103
|
+
value: <$expression> # optional — maps from $result (raw executor result)
|
|
104
|
+
# Optional common fields:
|
|
105
|
+
condition: <expression> # guard: skip if false
|
|
106
|
+
each: <expression> # iterate over array
|
|
107
|
+
delay: "<duration>" # inter-iteration pause (requires each). e.g., "1s", "500ms"
|
|
108
|
+
on_error: fail | ignore # default: fail
|
|
109
|
+
retry:
|
|
110
|
+
max: <int> # not "max_attempts"
|
|
111
|
+
delay: "<duration>" # e.g., "1s", "500ms" — not "backoff_ms"
|
|
112
|
+
backoff: <float>
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
**Step input rules:**
|
|
116
|
+
- Every step input requires `type`.
|
|
117
|
+
- Use `value` for both expressions and literals. Strings starting with `$` are auto-detected as expressions.
|
|
118
|
+
- Expressions: `value: $inputs.query`, `value: $steps.prev.output.field`
|
|
119
|
+
- Templates: `value: "https://example.com?q=${inputs.query}"`, `value: "${inputs.base_url}${item}.json"`
|
|
120
|
+
- Literals: `value: "https://example.com"`, `value: "GET"`
|
|
121
|
+
- To use a literal string starting with `$`, escape with `$$`: `value: "$$100"` → `"$100"`
|
|
122
|
+
- A bare value like `url: "https://example.com"` is invalid — it must be an object with `type`.
|
|
123
|
+
|
|
124
|
+
## Step Type Reference
|
|
125
|
+
|
|
126
|
+
### Tool Step
|
|
127
|
+
```yaml
|
|
128
|
+
- id: fetch_data
|
|
129
|
+
type: tool
|
|
130
|
+
tool: api.endpoint_name
|
|
131
|
+
inputs:
|
|
132
|
+
param:
|
|
133
|
+
type: string
|
|
134
|
+
value: $inputs.query
|
|
135
|
+
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
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
### Transform Step
|
|
162
|
+
|
|
163
|
+
Transform steps operate on **arrays only** (filter, map, sort). They require an `items` input of type `array` and always output an `items` array. Do NOT use transform steps to extract fields from a single object — use an exit step with `$`-references for that.
|
|
164
|
+
|
|
165
|
+
**filter:**
|
|
166
|
+
```yaml
|
|
167
|
+
- id: filter_items
|
|
168
|
+
type: transform
|
|
169
|
+
operation: filter
|
|
170
|
+
where: $item.score >= $inputs.threshold
|
|
171
|
+
inputs:
|
|
172
|
+
items:
|
|
173
|
+
type: array
|
|
174
|
+
value: $steps.previous.output.items
|
|
175
|
+
outputs:
|
|
176
|
+
items:
|
|
177
|
+
type: array
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Transform Step (map)
|
|
181
|
+
```yaml
|
|
182
|
+
- id: reshape
|
|
183
|
+
type: transform
|
|
184
|
+
operation: map
|
|
185
|
+
expression:
|
|
186
|
+
name: $item.full_name
|
|
187
|
+
email: $item.contact.email
|
|
188
|
+
inputs:
|
|
189
|
+
items:
|
|
190
|
+
type: array
|
|
191
|
+
value: $steps.previous.output.items
|
|
192
|
+
outputs:
|
|
193
|
+
items:
|
|
194
|
+
type: array
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
### Transform Step (map — cross-array zip)
|
|
198
|
+
|
|
199
|
+
When you have parallel arrays from different steps that need to be combined into an array of objects, use `map` with `$index` bracket indexing. Iterate over one array and pull corresponding elements from the others:
|
|
200
|
+
|
|
201
|
+
```yaml
|
|
202
|
+
- id: zip_results
|
|
203
|
+
type: transform
|
|
204
|
+
operation: map
|
|
205
|
+
expression:
|
|
206
|
+
title: $item
|
|
207
|
+
company: $steps.extract_companies.output.companies[$index]
|
|
208
|
+
location: $steps.extract_locations.output.locations[$index]
|
|
209
|
+
inputs:
|
|
210
|
+
items:
|
|
211
|
+
type: array
|
|
212
|
+
value: $steps.extract_titles.output.titles
|
|
213
|
+
outputs:
|
|
214
|
+
items:
|
|
215
|
+
type: array
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
This is a pure data operation — never use an LLM step to merge or zip arrays.
|
|
219
|
+
|
|
220
|
+
### Transform Step (sort)
|
|
221
|
+
```yaml
|
|
222
|
+
- id: sort_results
|
|
223
|
+
type: transform
|
|
224
|
+
operation: sort
|
|
225
|
+
field: score
|
|
226
|
+
direction: desc # or asc (default)
|
|
227
|
+
inputs:
|
|
228
|
+
items:
|
|
229
|
+
type: array
|
|
230
|
+
value: $steps.previous.output.items
|
|
231
|
+
outputs:
|
|
232
|
+
items:
|
|
233
|
+
type: array
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Conditional Step
|
|
237
|
+
|
|
238
|
+
`condition` is the branch predicate — evaluates to true (execute `then` steps) or false (execute `else` steps). Branch steps are declared later in the step list and skipped during sequential execution; they only run when selected by a conditional. `inputs: {}` and `outputs: {}` are required even though they're empty.
|
|
239
|
+
|
|
240
|
+
```yaml
|
|
241
|
+
- id: route
|
|
242
|
+
type: conditional
|
|
243
|
+
condition: $steps.check.output.items.length > 0
|
|
244
|
+
then:
|
|
245
|
+
- handle_items
|
|
246
|
+
else:
|
|
247
|
+
- handle_empty
|
|
248
|
+
inputs: {}
|
|
249
|
+
outputs: {}
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
### Exit Step
|
|
253
|
+
|
|
254
|
+
Use exit steps for **conditional early termination** — to stop the workflow when a condition is met.
|
|
255
|
+
|
|
256
|
+
`status` must be `success` or `failed` — those are the only valid values.
|
|
257
|
+
|
|
258
|
+
Early exit on empty result (success):
|
|
259
|
+
```yaml
|
|
260
|
+
- id: early_exit
|
|
261
|
+
type: exit
|
|
262
|
+
condition: $steps.filter.output.items.length == 0
|
|
263
|
+
status: success
|
|
264
|
+
output:
|
|
265
|
+
count: 0
|
|
266
|
+
items: []
|
|
267
|
+
inputs: {}
|
|
268
|
+
outputs: {}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
Early exit on error condition (failed):
|
|
272
|
+
```yaml
|
|
273
|
+
- id: guard_empty
|
|
274
|
+
type: exit
|
|
275
|
+
condition: $steps.fetch.output.data.length == 0
|
|
276
|
+
status: failed
|
|
277
|
+
output:
|
|
278
|
+
error: "No data returned from API"
|
|
279
|
+
inputs: {}
|
|
280
|
+
outputs: {}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
For normal workflow output, prefer `value` on workflow outputs instead of a trailing exit step.
|
|
284
|
+
|
|
285
|
+
## Output Resolution
|
|
286
|
+
|
|
287
|
+
| Context | Reference | When resolved |
|
|
288
|
+
|---------|-----------|---------------|
|
|
289
|
+
| Step output `value` | `$result` | Immediately after step executes |
|
|
290
|
+
| Workflow output `value` | `$steps.<id>.output` | After all steps complete |
|
|
291
|
+
|
|
292
|
+
Workflow outputs use `value` to map data from step results:
|
|
293
|
+
|
|
294
|
+
```yaml
|
|
295
|
+
outputs:
|
|
296
|
+
title:
|
|
297
|
+
type: string
|
|
298
|
+
value: $steps.fetch.output.title # resolved after all steps complete
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Resolution rules:**
|
|
302
|
+
1. **Normal completion** — each workflow output with `value` (an expression) is resolved from the final runtime context using `$steps` references.
|
|
303
|
+
2. **Exit step fires** — the exit step's `output` takes precedence. Its keys are matched against the declared workflow output keys.
|
|
304
|
+
3. **No value, no exit** — outputs are matched by key name against the last executed step's output (legacy behavior).
|
|
305
|
+
|
|
306
|
+
**Use `value` on workflow outputs** to explicitly declare where each output comes from. This eliminates the need for a trailing exit step just to produce outputs. Reserve exit steps for conditional early termination.
|
|
307
|
+
|
|
308
|
+
**Step output `value`** maps fields from the raw executor result using `$result`:
|
|
309
|
+
|
|
310
|
+
```yaml
|
|
311
|
+
outputs:
|
|
312
|
+
title:
|
|
313
|
+
type: string
|
|
314
|
+
value: $result.body.title # maps from raw tool/LLM response
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
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
|
+
|
|
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
|
+
## Expression Language
|
|
345
|
+
|
|
346
|
+
Use `$`-prefixed references to wire data between steps:
|
|
347
|
+
|
|
348
|
+
| Reference | Resolves To |
|
|
349
|
+
|-----------|-------------|
|
|
350
|
+
| `$inputs.name` | Workflow input parameter |
|
|
351
|
+
| `$steps.<id>.output` | A step's full output |
|
|
352
|
+
| `$steps.<id>.output.field` | A specific field from output |
|
|
353
|
+
| `$item` | Current item in `each` or transform iteration |
|
|
354
|
+
| `$index` | Current index in iteration |
|
|
355
|
+
| `$result` | Raw executor result (only valid in step output `value`) |
|
|
356
|
+
| `$steps.<id>.output.field[0]` | First element of an array field |
|
|
357
|
+
| `$item[$index]` | Nested array element at computed index (only valid inside `each`) |
|
|
358
|
+
|
|
359
|
+
Operators: `==`, `!=`, `>`, `<`, `>=`, `<=`, `&&`, `||`, `!`, `contains`
|
|
360
|
+
|
|
361
|
+
`contains` tests for substring or membership: `$item.title contains "Manager"` (string substring, case-sensitive); `$item.tags contains "urgent"` (array membership for primitives). Use in `transform filter` `where` clauses and `condition` guards to match text without an LLM.
|
|
362
|
+
|
|
363
|
+
Bracket indexing: `[0]`, `[$index]`, or any expression inside `[]` for array element access.
|
|
364
|
+
|
|
365
|
+
**Expression language limitations:** No function calls, no ternary expressions, no regex. Use `contains` for substring and array membership tests. Use `${}` template interpolation to build computed strings.
|
|
366
|
+
|
|
367
|
+
### Template Interpolation and Dynamic URLs
|
|
368
|
+
|
|
369
|
+
String `value` fields may contain `${ref}` blocks for interpolation. References inside `${...}` omit the leading `$`:
|
|
370
|
+
|
|
371
|
+
- `"${inputs.base_url}${item}.json"` → concatenated string
|
|
372
|
+
- `"https://api.example.com?q=${inputs.query}"` → URL with query param
|
|
373
|
+
- `"${steps.fetch.output.count}"` alone → typed result preserved (not coerced to string)
|
|
374
|
+
- `$${` inside a template → literal `${`
|
|
375
|
+
|
|
376
|
+
Primary use case: constructing per-iteration URLs in `each` + tool patterns.
|
|
377
|
+
|
|
378
|
+
```yaml
|
|
379
|
+
# Dynamic URL using template interpolation
|
|
380
|
+
# If base_url = "https://api.example.com/item/" and item = 101:
|
|
381
|
+
# → "https://api.example.com/item/101.json"
|
|
382
|
+
inputs:
|
|
383
|
+
url:
|
|
384
|
+
type: string
|
|
385
|
+
value: "${inputs.base_url}${item}.json"
|
|
386
|
+
```
|
|
387
|
+
|
|
388
|
+
## Iteration Patterns
|
|
389
|
+
|
|
390
|
+
### Iterating with `each` on Tool Steps
|
|
391
|
+
|
|
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.
|
|
393
|
+
|
|
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`.
|
|
395
|
+
|
|
396
|
+
**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
|
+
|
|
398
|
+
```yaml
|
|
399
|
+
steps:
|
|
400
|
+
- id: get_ids
|
|
401
|
+
type: tool
|
|
402
|
+
tool: api.list_items
|
|
403
|
+
outputs:
|
|
404
|
+
ids:
|
|
405
|
+
type: array
|
|
406
|
+
|
|
407
|
+
- id: fetch_details
|
|
408
|
+
type: tool
|
|
409
|
+
tool: http.request
|
|
410
|
+
each: $steps.get_ids.output.ids # iterate over ids array
|
|
411
|
+
on_error: ignore # skip failed fetches, continue
|
|
412
|
+
inputs:
|
|
413
|
+
url:
|
|
414
|
+
type: string
|
|
415
|
+
value: "${inputs.base_url}${item}.json"
|
|
416
|
+
outputs:
|
|
417
|
+
title:
|
|
418
|
+
type: string
|
|
419
|
+
value: $result.body.title # mapped per iteration via $result
|
|
420
|
+
id:
|
|
421
|
+
type: int
|
|
422
|
+
value: $result.body.id
|
|
423
|
+
```
|
|
424
|
+
|
|
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.
|
|
426
|
+
|
|
427
|
+
**Workflow output for each+tool:**
|
|
428
|
+
```yaml
|
|
429
|
+
outputs:
|
|
430
|
+
details:
|
|
431
|
+
type: array
|
|
432
|
+
value: $steps.fetch_details.output # the collected array of per-iteration results
|
|
433
|
+
```
|
|
434
|
+
|
|
435
|
+
**Pattern: List → Fetch Details → Filter → Summarize**
|
|
436
|
+
|
|
437
|
+
Full example for "fetch Hacker News top stories":
|
|
438
|
+
|
|
439
|
+
```yaml
|
|
440
|
+
inputs:
|
|
441
|
+
count:
|
|
442
|
+
type: int
|
|
443
|
+
default: 10
|
|
444
|
+
base_url:
|
|
445
|
+
type: string
|
|
446
|
+
default: "https://hacker-news.firebaseio.com/v0/item/"
|
|
447
|
+
|
|
448
|
+
outputs:
|
|
449
|
+
stories:
|
|
450
|
+
type: array
|
|
451
|
+
value: $steps.fetch_stories.output
|
|
452
|
+
|
|
453
|
+
steps:
|
|
454
|
+
- id: get_top_ids
|
|
455
|
+
type: tool
|
|
456
|
+
tool: http.request
|
|
457
|
+
inputs:
|
|
458
|
+
url: { type: string, value: "https://hacker-news.firebaseio.com/v0/topstories.json" }
|
|
459
|
+
outputs:
|
|
460
|
+
ids: { type: array, value: $result.body }
|
|
461
|
+
|
|
462
|
+
- id: slice_ids
|
|
463
|
+
type: transform
|
|
464
|
+
operation: filter
|
|
465
|
+
where: $index < $inputs.count # cap iteration count to avoid rate limiting
|
|
466
|
+
inputs:
|
|
467
|
+
items: { type: array, value: $steps.get_top_ids.output.ids }
|
|
468
|
+
outputs:
|
|
469
|
+
ids: { type: array }
|
|
470
|
+
|
|
471
|
+
- id: fetch_stories
|
|
472
|
+
type: tool
|
|
473
|
+
tool: http.request
|
|
474
|
+
each: $steps.slice_ids.output.ids
|
|
475
|
+
on_error: ignore
|
|
476
|
+
inputs:
|
|
477
|
+
url:
|
|
478
|
+
type: string
|
|
479
|
+
value: "${inputs.base_url}${item}.json"
|
|
480
|
+
outputs:
|
|
481
|
+
title: { type: string, value: $result.body.title }
|
|
482
|
+
score: { type: int, value: $result.body.score }
|
|
483
|
+
url: { type: string, value: $result.body.url }
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Iterating with `each` on LLM Steps
|
|
487
|
+
|
|
488
|
+
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.
|
|
489
|
+
|
|
490
|
+
**Why iterate instead of batch:**
|
|
491
|
+
- **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).
|
|
492
|
+
- **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.
|
|
493
|
+
- **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.
|
|
494
|
+
|
|
495
|
+
**Pattern: `each` + LLM with plain text output + map transform**
|
|
496
|
+
|
|
497
|
+
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:
|
|
498
|
+
|
|
499
|
+
```yaml
|
|
500
|
+
steps:
|
|
501
|
+
- id: fetch_items
|
|
502
|
+
type: tool
|
|
503
|
+
tool: http.request
|
|
504
|
+
each: $steps.get_ids.output.ids
|
|
505
|
+
on_error: ignore
|
|
506
|
+
inputs:
|
|
507
|
+
url:
|
|
508
|
+
type: string
|
|
509
|
+
value: "${inputs.base_url}${item}.json"
|
|
510
|
+
outputs:
|
|
511
|
+
title: { type: string, value: $result.body.title }
|
|
512
|
+
content: { type: string, value: $result.body.content }
|
|
513
|
+
|
|
514
|
+
- id: summarize
|
|
515
|
+
type: llm
|
|
516
|
+
model: haiku
|
|
517
|
+
each: $steps.fetch_items.output # iterate over the collected array
|
|
518
|
+
delay: "1s" # rate-limit: 1s pause between iterations
|
|
519
|
+
on_error: ignore # skip items that fail
|
|
520
|
+
description: Summarize each item individually
|
|
521
|
+
prompt: |
|
|
522
|
+
Summarize this item in 1-2 sentences.
|
|
523
|
+
|
|
524
|
+
Title: $item.title
|
|
525
|
+
Content: $item.content
|
|
526
|
+
|
|
527
|
+
Write only the summary — no formatting, no preamble.
|
|
528
|
+
inputs:
|
|
529
|
+
item:
|
|
530
|
+
type: object
|
|
531
|
+
value: $item
|
|
532
|
+
outputs:
|
|
533
|
+
summary:
|
|
534
|
+
type: string
|
|
535
|
+
value: $result # plain text — one summary per iteration
|
|
536
|
+
|
|
537
|
+
- id: combine_results
|
|
538
|
+
type: transform
|
|
539
|
+
operation: map
|
|
540
|
+
description: Zip summaries with source data
|
|
541
|
+
expression:
|
|
542
|
+
title: $item.title
|
|
543
|
+
description: $steps.summarize.output[$index].summary
|
|
544
|
+
inputs:
|
|
545
|
+
items: { type: array, value: $steps.fetch_items.output }
|
|
546
|
+
outputs:
|
|
547
|
+
items: { type: array }
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
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.
|
|
551
|
+
|
|
552
|
+
**Why plain text over JSON for `each` + LLM:**
|
|
553
|
+
- **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.
|
|
554
|
+
- **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.
|
|
555
|
+
- **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.
|
|
556
|
+
|
|
557
|
+
**Workflow output for each+LLM:**
|
|
558
|
+
```yaml
|
|
559
|
+
outputs:
|
|
560
|
+
summaries:
|
|
561
|
+
type: array
|
|
562
|
+
value: $steps.combine_results.output.items # the zipped array
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
**Anti-pattern — JSON output in `each` + LLM:**
|
|
566
|
+
```yaml
|
|
567
|
+
# BAD: model may return fenced JSON → parse fails → $result.field returns undefined → silent {}
|
|
568
|
+
- id: summarize
|
|
569
|
+
type: llm
|
|
570
|
+
each: $steps.fetch_items.output
|
|
571
|
+
prompt: |
|
|
572
|
+
Return a JSON object with "title" and "description" fields.
|
|
573
|
+
Respond with raw JSON only — no markdown fences.
|
|
574
|
+
outputs:
|
|
575
|
+
title: { type: string, value: $result.title } # undefined if fenced
|
|
576
|
+
description: { type: string, value: $result.description } # undefined if fenced
|
|
577
|
+
```
|
|
578
|
+
|
|
579
|
+
**Anti-pattern — batching all items into one prompt:**
|
|
580
|
+
```yaml
|
|
581
|
+
# BAD: unbounded prompt size, all-or-nothing failure, complex output format
|
|
582
|
+
- id: summarize
|
|
583
|
+
type: llm
|
|
584
|
+
prompt: |
|
|
585
|
+
Summarize each job below...
|
|
586
|
+
Jobs: $steps.fetch_items.output # dumps entire array into prompt
|
|
587
|
+
outputs:
|
|
588
|
+
roles: { type: array, value: $result } # one malformed response loses everything
|
|
589
|
+
```
|
|
590
|
+
|
|
591
|
+
**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`.
|
|
592
|
+
|
|
593
|
+
## Web Scraping Pattern
|
|
594
|
+
|
|
595
|
+
When a workflow fetches HTML and extracts structured data, follow this recipe:
|
|
596
|
+
|
|
597
|
+
### Step pattern: fetch → guard → extract
|
|
598
|
+
|
|
599
|
+
```yaml
|
|
600
|
+
steps:
|
|
601
|
+
- id: fetch_page
|
|
602
|
+
type: tool
|
|
603
|
+
tool: http.request
|
|
604
|
+
retry: { max: 3, delay: "2s", backoff: 1.5 }
|
|
605
|
+
inputs:
|
|
606
|
+
url: { type: string, value: "https://example.com/search" }
|
|
607
|
+
method: { type: string, value: "GET" }
|
|
608
|
+
headers:
|
|
609
|
+
type: object
|
|
610
|
+
value: { "User-Agent": "Mozilla/5.0", "Accept": "text/html" }
|
|
611
|
+
outputs:
|
|
612
|
+
html: { type: string, value: $result.body }
|
|
613
|
+
|
|
614
|
+
- id: guard_empty
|
|
615
|
+
type: exit
|
|
616
|
+
condition: $steps.fetch_page.output.html == ""
|
|
617
|
+
status: success
|
|
618
|
+
output: { results: [] }
|
|
619
|
+
|
|
620
|
+
- id: extract_data
|
|
621
|
+
type: tool
|
|
622
|
+
tool: html.select
|
|
623
|
+
inputs:
|
|
624
|
+
html: { type: string, value: $steps.fetch_page.output.html }
|
|
625
|
+
selector: { type: string, value: "li.result-item" }
|
|
626
|
+
fields:
|
|
627
|
+
type: object
|
|
628
|
+
value:
|
|
629
|
+
title: "h3.title"
|
|
630
|
+
url: "a.link @href"
|
|
631
|
+
id: "@data-pid"
|
|
632
|
+
limit: { type: int, value: 50 }
|
|
633
|
+
outputs:
|
|
634
|
+
items: { type: array, value: $result.results }
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
### Selector research
|
|
638
|
+
|
|
639
|
+
Follow the Research protocol (Authoring Process, Phase 2) before writing selectors. Every selector must be verified against actual fetched HTML.
|
|
640
|
+
|
|
641
|
+
## Authoring Rules
|
|
642
|
+
|
|
643
|
+
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.
|
|
644
|
+
2. **Use the cheapest model.** `haiku` for classification/scoring, `sonnet` for complex reasoning.
|
|
645
|
+
3. **Always declare inputs and outputs.** They enable validation and composability.
|
|
646
|
+
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.
|
|
647
|
+
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.
|
|
648
|
+
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 HTTP tool steps, `delay: "1s"` for LLM steps. See *Iteration Patterns*.
|
|
649
|
+
7. **Add `on_error: ignore` for non-critical steps** like notifications.
|
|
650
|
+
8. **Add `retry` for external API calls** (tool steps that might fail transiently).
|
|
651
|
+
9. **Use `condition` guards for early exits** rather than letting empty data flow through.
|
|
652
|
+
10. **Steps execute in declaration order.** A step can only reference steps declared before it.
|
|
653
|
+
11. **`each` is not valid on `exit` or `conditional` steps.**
|
|
654
|
+
12. **`condition` on a `conditional` step is the branch condition**, not a guard.
|
|
655
|
+
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.
|
|
656
|
+
14. **Transform steps are for arrays only.** Never use a transform to extract fields from a single object.
|
|
657
|
+
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.
|
|
658
|
+
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 }").
|
|
659
|
+
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*.
|
|
660
|
+
18. **Prefer bulk endpoints over per-item iteration.** When `each` + `http.request` is unavoidable, always add `delay: "2s"` (minimum), cap iteration count, and add `retry` with `backoff`. `delay` is not optional — external APIs rate-limit without warning and delays are free. Same applies to `each` + `llm` steps: always add `delay: "1s"`. See *Iteration Patterns*.
|
|
661
|
+
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*.
|
|
662
|
+
|
|
663
|
+
## Output Format
|
|
664
|
+
|
|
665
|
+
Write the SKILL.md file using the Write tool. The file structure:
|
|
666
|
+
|
|
667
|
+
1. YAML frontmatter (between `---` delimiters) — the very first line. The `name` field must be lowercase-hyphenated (e.g., `fetch-json-from-api`, not `Fetch JSON from API`).
|
|
668
|
+
2. A markdown heading.
|
|
669
|
+
3. A single `workflow` fenced code block containing the YAML.
|
|
670
|
+
|
|
671
|
+
Example of a complete SKILL.md:
|
|
672
|
+
|
|
673
|
+
```
|
|
674
|
+
---
|
|
675
|
+
name: example-workflow
|
|
676
|
+
description: Fetches data and outputs a specific field
|
|
677
|
+
---
|
|
678
|
+
|
|
679
|
+
# Example Workflow
|
|
680
|
+
|
|
681
|
+
` `` `workflow
|
|
682
|
+
inputs:
|
|
683
|
+
id:
|
|
684
|
+
type: string
|
|
685
|
+
default: "1"
|
|
686
|
+
|
|
687
|
+
outputs:
|
|
688
|
+
name:
|
|
689
|
+
type: string
|
|
690
|
+
value: $steps.fetch.output.name
|
|
691
|
+
|
|
692
|
+
steps:
|
|
693
|
+
- id: fetch
|
|
694
|
+
type: tool
|
|
695
|
+
tool: some.tool
|
|
696
|
+
inputs:
|
|
697
|
+
id:
|
|
698
|
+
type: string
|
|
699
|
+
value: $inputs.id
|
|
700
|
+
outputs:
|
|
701
|
+
name:
|
|
702
|
+
type: string
|
|
703
|
+
value: $result.result.name
|
|
704
|
+
` `` `
|
|
705
|
+
```
|
|
706
|
+
|
|
707
|
+
## Validation
|
|
708
|
+
|
|
709
|
+
After writing the file, always validate it against the runtime. The validation checklist:
|
|
710
|
+
- [ ] All step IDs are unique
|
|
711
|
+
- [ ] All `$steps` references point to earlier steps
|
|
712
|
+
- [ ] All tools referenced are confirmed available in this deployment context
|
|
713
|
+
- [ ] Input/output types are consistent between connected steps
|
|
714
|
+
- [ ] No cycles in step references
|
|
715
|
+
- [ ] `each` not used on exit or conditional steps
|
|
716
|
+
- [ ] Workflow outputs have `value` mapping to `$steps` references
|
|
717
|
+
- [ ] Step output `value` uses `$result` (not `$steps`)
|
|
718
|
+
- [ ] LLM step outputs have `value` using `$result`
|
|
719
|
+
- [ ] All `${}` template references resolve to declared inputs/steps
|
|
720
|
+
- [ ] LLM prompts expecting JSON include "raw JSON only — no markdown fences" instruction
|
|
721
|
+
- [ ] LLM steps with `each` prefer plain text output (`value: $result`) over JSON (`value: $result.field`)
|
|
722
|
+
- [ ] Every `each` loop that calls an external service has `delay` (tool steps: `"2s"` minimum; LLM steps: `"1s"` minimum)
|
|
723
|
+
- [ ] `each` + `http.request` steps are bounded (preceded by a cap) and have `retry` with `backoff`
|
|
724
|
+
|
|
725
|
+
If validation fails, fix the errors and revalidate.
|