matthew-h-cromer

workflow-author

Author, validate, and run workflows. Use when the user wants to create, automate, build, write, or schedule a workflow or recurring task.

matthew-h-cromer 0 Updated 3mo ago
GitHub

Install

npx skillscat add matthew-h-cromer/workflowskill/workflow-author

Install via the SkillsCat registry.

SKILL.md

WorkflowSkill Author

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.

How WorkflowSkill Works

A WorkflowSkill is a declarative workflow definition embedded in a SKILL.md file as a fenced workflow code block. It defines inputs, outputs, and an ordered sequence of steps.

YAML Structure

inputs: # object keyed by name — NOT an array
  <name>:
    type: string | int | float | boolean | array | object # required
    default: <literal> # optional — fallback value when input not provided

outputs: # object keyed by name — NOT an array
  <name>:
    type: string | int | float | boolean | array | object # required
    value: <$expression> # optional — resolves from $steps or $inputs after all steps complete

steps:
  - id: <unique_identifier> # required
    type: tool | transform | conditional | exit # required
    description: <string> # optional
    # Type-specific fields (see Step Types)
    inputs: # object keyed by name
      <name>:
        type: <type> # required
        value: <$expression or literal> # optional — expression ($-prefixed) or literal
    outputs:
      <name>:
        type: <type> # required
        value: <$result expression> # optional — maps $result fields from raw executor result
    # Optional common fields:
    condition: <$expression> # guard: skip if false
    each: <$expression> # iterate over array
    delay: "<duration>" # inter-iteration pause (requires each). e.g., "1s", "500ms"
    on_error: fail | ignore # default: fail
    retry:
      max: <int> # retry ATTEMPTS, not total tries (total = 1 + max)
      delay: "<duration>" # base delay — all three fields required
      backoff: <float> # multiplier per attempt (e.g., 2.0 → 1s, 2s, 4s)

retry requires all three fields together. Only tool errors are retriable — expression and validation errors fail immediately.

Authoring Process

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 following phases are executed by you atomically with the final output being an executable WorkflowSkill for the user.

Phase 1: Understand

Deeply understand the intent of the user.

  • Read the request carefully. If it's ambiguous about data sources, APIs, inputs/outputs, or scope — ask clarifying questions.
  • Ask at most 2-3 focused questions at a time. Offer specific options. Bad: "What do you want to do?" Good: "Should results be filtered by date, category, or both?"
  • If the request is clear, skip directly to Research.

Phase 2: Research

Perform research to clarify how you should build the workflow.

  • Confirm available tools first. The tools available in tool steps are the tools registered in the current runtime 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. Check your context for the exact names available.
  • Search for official documentation — Use only official API documentation. 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.
  • Fetch the target data yourself — When your workflow involves any kind of data fetching, always fetch it yourself and inspect the response. This is the ultimate source of truth to guide your implementation.

Phase 3: Generate

Design the workflow internally following this checklist, then write the .md file:

  • Identify data sources and operations — What data is needed, and what operations must be performed? These become tool steps.
  • Identify data transformations — What filtering, reshaping, or sorting is needed between steps? These become transform steps.
  • Identify decision points — Where does execution branch? These become conditional steps.
  • Identify exit conditions — When should the workflow stop early? These become exit steps with condition guards.
  • Wire steps together — Use $steps.<id>.output references to connect outputs to inputs.
  • Add error handling — Configure on_error. Use retry policies to protect from transient errors.

Phase 4: Validate & Test

  • Validate the workflow against the runtime. If validation fails, fix the errors and revalidate.
  • Run the workflow to verify it works end-to-end.
  • Test beyond just happy path. Vary inputs. For workflows with conditional exits, test both execution paths (e.g., "results found" vs. "no results").
  • If the test reveals issues (malformed LLM output, wrong field mappings, broken expressions), fix the workflow and re-test. Repeat until the workflow accomplishes the original intent. Solutions should always follow best practices. No workarounds.

YAML Reference

Workflow Inputs

Each input is an object keyed by name. Two fields:

  • type (required): string, int, float, boolean, array, or object
  • default (optional): literal fallback used when the input is not provided at runtime
inputs:
  url:
    type: string
    default: "https://example.com/items"
  count:
    type: int
    default: 10
  verbose:
    type: boolean
    default: false

Workflow Outputs

Each output is an object keyed by name. Two fields:

  • type (required): string, int, float, boolean, array, or object
  • value (optional): a $steps expression resolved after all steps complete
outputs:
  items:
    type: array
    value: $steps.fetch_details.output
  title:
    type: string
    value: $steps.fetch.output.title

Use $steps.<id>.output when the step returns a plain value (string, array, object) with no named step outputs declared. Use $steps.<id>.output.<field> only when the step declares named outputs and you need a specific field. Never add a field selector for a step whose result is a plain string — it will always resolve to nothing.

Workflow Steps

id

A unique string identifier. Downstream steps reference it as $steps.<id>.output. Declare steps in dependency order — a step can only reference steps declared before it.

type

One of tool, transform, conditional, or exit.

tool — Invokes a registered tool via the host's ToolAdapter. Use for all external calls: APIs, databases, LLM inference. Requires a tool field naming the registered tool. Only use tools actually available to you in your context. Do not make up tools.

- id: fetch
  type: tool
  tool: web_fetch
  inputs:
    url: { type: string, value: $inputs.url }
  outputs:
    content: { type: string, value: $result.content }

transform — Filters, maps, or sorts an array in-process. Arrays only — never use on a single object. Requires an operation field: filter, map, or sort.

  • filter — keep items where where is true
  • map — reshape each item using an expression object
  • sort — order items by field and direction (asc or desc)
- id: filter_items
  type: transform
  operation: filter
  where: $item.score >= $inputs.threshold
  inputs:
    items: { type: array, value: $steps.previous.output.items }
  outputs:
    items: { type: array }

- id: reshape
  type: transform
  operation: map
  expression:
    name: $item.full_name
    score: $item.metrics.score
  inputs:
    items: { type: array, value: $steps.previous.output.items }
  outputs:
    items: { type: array }

- id: sort_results
  type: transform
  operation: sort
  field: score
  direction: desc
  inputs:
    items: { type: array, value: $steps.previous.output.items }
  outputs:
    items: { type: array }

conditional — Branches execution. condition is the branch predicate: true runs then step IDs, false runs else step IDs. Branch step IDs must match steps declared later in the list — they are skipped during sequential execution and only run when selected. inputs: {} and outputs: {} are required even when empty.

- id: route
  type: conditional
  condition: $steps.check.output.items.length > 0
  then: [handle_found]
  else: [handle_empty]
  inputs: {}
  outputs: {}

exit — Terminates the workflow early. Use only for conditional early termination — not to produce normal output. status is success or failed. output keys must match declared workflow output keys. inputs: {} and outputs: {} are required.

- id: guard_empty
  type: exit
  condition: $steps.filter.output.items.length == 0
  status: success
  output:
    items: []
  inputs: {}
  outputs: {}

description

Documents the intent of the step for readers.

Step inputs

Typed input fields — objects with type and value. A bare scalar is invalid.

  • type (required): string, int, float, boolean, array, or object
  • value (optional): expression, template string, or literal

Step outputs

Typed output fields — objects with type and optional value. Use $result to map fields from the raw executor result:

outputs:
  content: { type: string, value: $result.content }
  items: { type: array, value: $result.items }

Outputs without value pass through from the raw result by key name. $result is only valid in step output value — not in workflow outputs.

Only declare step outputs when you need to extract and rename specific fields from a structured result. If downstream steps reference $steps.<id>.output directly — without a field selector — omit the outputs block entirely; the full result is always available that way regardless.

When a tool returns a plain string rather than a JSON object, there are no fields to extract. Declaring named outputs with $result.<field> will always resolve to nothing. Reference the string directly as $steps.<id>.output in workflow outputs or downstream step inputs.

condition

A boolean expression. When false, the step is skipped and its output is null. Valid on tool, transform, and exit steps as a guard. On a conditional step, condition is the branch predicate — not a guard.

- id: notify
  type: tool
  tool: slack.post_message
  condition: $steps.filter.output.items.length > 0
  inputs:
    text: { type: string, value: "Items found" }
  outputs:
    sent: { type: boolean, value: $result.ok }

Operators: ==, !=, >, <, >=, <=, &&, ||, !, contains

contains tests substring ($item.title contains "Manager", case-insensitive) or array membership ($item.tags contains "urgent", exact primitive equality). Use it in where clauses and condition guards to match text without an LLM.

No function calls, no ternary expressions, no regex. &&/|| are boolean — they return true/false, not the operand value.

each

Runs the step once per element in the target array. $item is the current element; $index is the 0-based position. Each iteration's output is collected into an array, with $result mappings applied per iteration. Valid only on tool and transform steps.

With on_error: ignore, failed iterations produce null in the results array while successful iterations keep their output — the step continues through all items.

- id: fetch_details
  type: tool
  tool: web_fetch
  each: $steps.slice_items.output.items
  delay: "2s"
  on_error: ignore
  inputs:
    url: { type: string, value: $item.url }
  outputs:
    content: { type: string, value: $result.content }

delay

Pauses between iterations. Requires each. Not applied after the last iteration. Examples: "2s", "500ms".

Always add delay to every each loop that calls an external service — APIs rate-limit without warning. Minimum: "500ms" for LLM calls, "2s" for external APIs.

on_error

fail (default) — the workflow stops on the first error. ignore — failed iterations produce null in the results array and the workflow continues.

Use ignore for per-item tool calls where one failure should not abort the batch.

retry

Retries a failed tool step. All three fields are required:

retry:
  max: 3 # retry attempts — total tries = 1 + max
  delay: "2s" # base delay before first retry
  backoff: 1.5 # delay multiplier per attempt (2s → 3s → 4.5s)

Only tool errors are retriable — expression and validation errors fail immediately.

Expression — a $-prefixed reference to a workflow input or earlier step output:

url: { type: string, value: $inputs.url }
items: { type: array, value: $steps.extract.output.items }

Expression reference table:

Reference Resolves to
$inputs.name Workflow input parameter
$steps.<id>.output A step's full output object
$steps.<id>.output.field A specific field from a step's output
$steps.<id>.output.field[0] First element of an array field
$item Current element in each or transform iteration
$index 0-based position in iteration
$result Raw executor result (step output value only)

Template — a string with ${ref} blocks. References inside ${...} omit the leading $:

url: { type: string, value: "https://api.example.com/items/${item.id}" }
path: { type: string, value: "${inputs.base_url}${item}.json" }

Literal — any static value:

method: { type: string, value: "GET" }
channel: { type: string, value: "#alerts" }

To use a literal string starting with $, escape it: "$$100""$100".

Design Rules

LLM vs. Deterministic Steps

If the operation has a single correct answer derivable from the data, use a deterministic step. LLMs cost money, add latency, and introduce variability — use them only when no deterministic alternative exists.

Step costs:

Step type Cost
transform, conditional, exit Free — pure in-process
tool (API) Latency + rate limits
tool (LLM) Expensive — billed per token

Use an LLM ONLY for:

  • Interpreting ambiguous or unstructured text where the meaning is context-dependent
  • Extracting structured data from prose where field boundaries aren't predictable
  • Generating natural language where wording quality matters
  • Classification that can't be reduced to substring or numeric comparison

NEVER use an LLM for:

Operation Alternative
Merging parallel arrays transform map + $index
Filtering by substring transform filter + contains
Filtering by numeric threshold transform filter + comparison operators
Grouping by boolean condition Two transform filter steps with complementary where
Sorting transform sort
Counting .length
Formatting strings Template interpolation in step value inputs, exit outputs, or workflow outputs (not inside transform map expressions — those only resolve $-prefixed references)
Branching conditional step or condition guard
Reshaping/renaming fields transform map + expression object
String equality check == operator
Array membership contains operator

If you find yourself writing a prompt that says "filter", "merge", "group by", "sort", or "format each item" — stop. These are deterministic operations.

Cost control for unavoidable LLM steps:

  • Filter and cap with transform steps before passing data to any tool step.
  • Use each on LLM steps that process a list. Never pass the whole collection in one bulk prompt — per-item calls are cheaper, higher quality, and easier to debug.
  • Always precede an each + LLM step with a transform filter using $index < $inputs.count to bound cost.
  • Keep LLM output schemas minimal — only request fields you actually use downstream.
  • Always provide a system prompt. A focused system prompt improves output quality and reduces token use.
  • Use on_error: ignore on per-item LLM steps so one failed generation doesn't abort the batch.