SuperAPI AI Directory

cpf-workflow-author

Office & productivity

View repository

Updated 10-16 07:15

About this skill

cpf-workflow-author

Create checkpointflow workflow YAML files that are valid, idiomatic, and ready to run with cpf run.

Before you start

Read the checkpointflow guide to get the full reference:

cpf guide

If you need to see existing examples in the project, check examples/*.yaml in the checkpointflow repo.

Authoring process

  1. Understand what the user wants to automate. Ask clarifying questions if needed: What commands or tools are involved? Where does a human or agent need to weigh in? What are the inputs?
  1. Design the step sequence. Map each action to a step kind:

    • A shell command or script invocation -> cli
    • A point where execution must pause for human/agent input -> await_event
    • A terminal outcome -> end
  2. Decide where to put the file. If the user specifies a path, use it. Otherwise, prompt the user to choose:

    • Local (.checkpointflow/<id>.yaml) — scoped to the current project, checked into the repo
    • Global (~/.checkpointflow/<id>.yaml) — available everywhere via cpf flows
    • Examples (examples/<id>.yaml) — only offer this if inside the checkpointflow repo itself

    Skip the question when the location is obvious from context (e.g. "add a global workflow", "create a workflow in this repo", or an explicit file path).

  3. Validate immediately after writing:

    cpf validate -f <path-to-workflow.yaml>
    

    If validation fails, fix the YAML and re-validate before telling the user the workflow is ready.

  4. Offer to run it if the user seems ready. When the workflow hits an await_event step (exit code 40), collect the required input via a structured prompt, then cpf resume to continue.

Workflow file structure

Every workflow file follows this skeleton:

schema_version: checkpointflow/v1
workflow:
  id: snake_case_identifier
  name: Human Readable Name
  version: 0.1.0
  description: >
    A clear description of what this workflow does, why it exists,
    and what the user/agent will experience when running it.

  inputs:
    type: object
    required: [field_name]
    properties:
      field_name:
        type: string
        description: What this field is for and how it will be used
        examples: ["realistic-value-1", "realistic-value-2"]

  steps:
    - id: first_step
      kind: cli
      command: echo "doing something with ${inputs.field_name}"

    - id: done
      kind: end
      result:
        status: completed

Key rules:

  • schema_version is always checkpointflow/v1
  • workflow.id must be present, snake_case, and unique
  • workflow.inputs uses JSON Schema to define what the caller must provide
  • Every step needs a unique id
  • Steps execute sequentially unless a transition jumps elsewhere

Writing great names and descriptions

The name and description fields are emitted in every JSON envelope at runtime. When an agent runs or resumes this workflow, these are the only context it has about what the workflow does — it may not have read the YAML file. Write them as if they are the briefing for an agent that knows nothing else.

name — a short, specific title that tells the agent what process it is driving. Avoid generic names like "Build Pipeline" or "Review Flow". Instead, be specific: "PR Review with Security Gate", "TDD Feature Development for CPF".

description — a 1-3 sentence explanation that covers:

  1. What the workflow does end-to-end (the happy path)
  2. Who is involved (human approvals? agent steps? parallel checks?)
  3. What methodology or constraints it follows (e.g. "TDD: tests first, then implementation")

Bad: description: A workflow for deployments. Good: description: > Deploys a service to production with a two-phase rollout. An agent runs pre-deploy checks (lint, test, canary), then pauses for human approval before promoting to full traffic. Rolls back automatically on health check failure.

Writing great input properties

Input description and examples directly shape the UX when an agent or skill collects input from the user. The runner skill uses examples as selectable options in its prompt UI — without them, the agent has to guess what values look like.

Always include examples on string inputs that don't have an enum. Provide 2-3 realistic values that illustrate the expected format:

properties:
  feature_name:
    type: string
    description: Short kebab-case name for the feature
    examples: ["run-delete", "gui-filter", "batch-resume"]
  target_url:
    type: string
    description: The URL to deploy to
    examples: ["https://staging.example.com", "https://prod.example.com"]

description on input properties should say what the value is for, not just what type it is. "The environment name" is worse than "Target deployment environment — determines which config and secrets are loaded."

For enum inputs, examples are unnecessary — the enum values themselves become the options. But write a clear description explaining what each choice means if it's not obvious from the value names.

Step kinds

All step kinds below are fully supported at runtime.

cli

Runs a shell command. Supports ${inputs.x} and ${steps.<id>.outputs.x} interpolation. Use cli for steps that run real shell commands. Do NOT use cli with echo as a placeholder for agent work — use await_event with audience: agent instead.

command accepts a string or a list of strings. Lists are joined with && at runtime — use lists to avoid YAML multi-line scalar pitfalls:

- id: build
  kind: cli
  cwd: ${inputs.project_dir}
  command:
    - npm run lint
    - npm run build --project ${inputs.project_name}
    - npm test
  timeout_seconds: 120

Use cwd to set the working directory (supports interpolation). Use shell to pick a specific shell (bash, sh, powershell, pwsh), or set defaults.shell at the workflow level:

workflow:
  defaults:
    shell: bash

If the command produces structured JSON on stdout, declare outputs so the runtime validates it and makes it available to later steps:

- id: analyze
  kind: cli
  command: analyze-tool --format json ${inputs.target}
  outputs:
    type: object
    required: [score, report_path]
    properties:
      score: { type: number }
      report_path: { type: string }

Use if for conditional execution (no ${} wrapper needed):

- id: deep_scan
  kind: cli
  command: scan --deep ${inputs.target}
  if: inputs.mode == "full"

await_event

Pauses the workflow and waits for external input. The CLI exits with code 40 and emits a waiting envelope that tells the caller exactly what input is needed and how to resume.

audience: user — The step pauses for a human to make a decision. The prompt presents context and choices. The input_schema defines what the user should provide.

- id: review
  kind: await_event
  audience: user
  event_name: review_decision
  prompt: Review the results and decide whether to proceed.
  input_schema:
    type: object
    required: [decision]
    properties:
      decision:
        type: string
        enum: [approve, reject]
      notes:
        type: string

audience: agent — The step pauses for an AI agent to do real work. The prompt is the work assignment — describe what to analyze, build, or run. The input_schema defines the structured output the agent must deliver. The workflow pauses, the agent does the work, and resumes with results. Use this instead of cli with echo for agent work.

- id: analyze
  kind: await_event
  audience: agent
  event_name: analysis_results
  name: "Agent: Analyze Codebase"
  prompt: |
    Scan src/${inputs.area}/ and identify:
    - Source files and their responsibilities
    - Existing test files
    - Key dependencies
  input_schema:
    type: object
    required: [files_found, notes]
    properties:
      files_found: { type: array, items: { type: string } }
      test_files: { type: array, items: { type: string } }
      notes: { type: string }

Add transitions to branch based on the input received:

  transitions:
    - when: ${event.decision == "approve"}
      next: deploy
    - when: ${event.decision == "reject"}
      next: cancelled

Without transitions, execution continues to the next step in the array.

end

Terminates the workflow with an explicit result. The result can interpolate values from earlier steps:

- id: done
  kind: end
  result:
    status: success
    url: ${steps.deploy.outputs.url}

If no end step is present, the workflow completes implicitly after the last step with no result. Use end when you want to return meaningful data or when you have multiple terminal states (e.g., approved vs. rejected).

switch

Evaluates conditions in order and jumps to the first matching step. Use for data-driven branching without human input.

- id: route
  kind: switch
  cases:
    - when: 'inputs.priority == "critical"'
      next: fast_track
    - when: 'inputs.priority == "low"'
      next: backlog
  default: standard_review

Each case has when (expression) and next (target step ID). default is optional — if no case matches and no default, the step fails.

api

Makes an HTTP request. Response body is parsed as JSON and exposed as step outputs.

- id: fetch_status
  kind: api
  method: GET
  url: ${inputs.api_base}/health
  headers:
    Authorization: Bearer ${inputs.token}

Optional: body (sent as JSON), headers, success.status_codes (default: [200, 201, 204]), timeout_seconds (default: 30).

foreach

Iterates over a list. Within body steps, ${item} is the current element and ${item_index} is the zero-based index.

- id: process_files
  kind: foreach
  items: inputs.files
  body:
    - id: convert
      kind: cli
      command: convert ${item} --output out_${item_index}.pdf

Body steps currently support cli and end kinds.

parallel

Runs multiple steps concurrently. Each branch references a step ID via start_at. All branches must succeed.

- id: checks
  kind: parallel
  branches:
    - start_at: lint
    - start_at: test
    - start_at: typecheck

Branch target steps must be cli or end steps in the same workflow.

workflow

Invokes another workflow YAML as a sub-workflow.

- id: deploy
  kind: workflow
  workflow_ref: ./deploy.yaml
  inputs:
    version: ${steps.build.outputs.version}

Sub-workflow steps currently support cli and end kinds.

Expressions

Expressions use ${...} syntax for interpolation in command strings and transition when clauses:

  • ${inputs.name} — workflow input
  • ${steps.build.outputs.artifact} — output from a previous step
  • ${event.decision} — event data (only in when clauses)

In if conditions, write the expression bare (no ${}):

  • if: inputs.mode == "full"

Operators: ==, !=, and, or

Branching patterns

Skip a step conditionally — use if:

- id: optional_step
  kind: cli
  command: do-extra-work
  if: inputs.thorough == "yes"

Branch on data — use switch for data-driven branching without human input:

- id: route
  kind: switch
  cases:
    - when: 'steps.check.outputs.count == 0'
      next: nothing_to_do
  default: process_items

Branch after human input — use transitions on await_event to jump to different steps based on the response. Make sure every transition target step exists in the steps array:

- id: approval
  kind: await_event
  audience: user
  event_name: approval
  prompt: Approve or reject?
  input_schema:
    type: object
    required: [choice]
    properties:
      choice: { type: string, enum: [approve, reject] }
  transitions:
    - when: ${event.choice == "approve"}
      next: do_work
    - when: ${event.choice == "reject"}
      next: rejected

- id: do_work
  kind: cli
  command: echo "approved"

- id: complete
  kind: end
  result: { outcome: approved }

- id: rejected
  kind: end
  result: { outcome: rejected }

Cover every enum value with a transition — once an await_event has transitions, the runtime does NOT fall through to the next step. If no transition matches, the run fails with "No transition matched." Every possible enum value in the input_schema must have a corresponding when clause:

# WRONG — "ship_it" has no transition, run will fail
input_schema:
  properties:
    decision: { type: string, enum: [ship_it, revise, shelve] }
transitions:
  - when: ${event.decision == "revise"}
    next: revision
  - when: ${event.decision == "shelve"}
    next: shelved
  # Missing: ship_it → approved

# CORRECT — every enum value is covered
transitions:
  - when: ${event.decision == "ship_it"}
    next: approved
  - when: ${event.decision == "revise"}
    next: revision
  - when: ${event.decision == "shelve"}
    next: shelved

The same rule applies to switch cases — if no case matches and no default is set, the run fails.

Be aware that steps between a transition target and the next step will execute sequentially. If do_work transitions to step complete, but rejected comes after complete in the array, the rejected step will NOT be reached from the do_work path — execution flows forward from do_work through complete and stops at the end. Place your branch targets so that sequential fall-through does not hit steps from other branches, or use end steps to terminate each branch.

Common mistakes to avoid

  • Missing schema_version: checkpointflow/v1 at the top level — every file needs it.
  • Duplicate step IDs — each step id must be unique across the entire workflow.
  • Transition target doesn't exist — every next value must match an actual step id.
  • Using ${} in if conditions — if takes a bare expression, not wrapped in ${}.
  • Forgetting input_schema on await_event — it's required, not optional.
  • Incomplete transition coverage — when await_event has transitions, every possible input_schema enum value must have a matching when clause. Uncovered values cause a runtime failure, not a fallthrough.
  • Using cli with echo for agent work — use await_event with audience: agent instead. The prompt describes the work, and input_schema defines the expected output.
  • Step kind typos — supported kinds are cli, await_event, end, switch, api, foreach, parallel, and workflow. Any other kind fails at runtime with exit code 80.

After writing the workflow

Always validate:

cpf validate -f <path>

If the user wants to run it:

cpf run -f <path> --input '{"key": "value"}'

When a run pauses at an await_event (exit code 40), the JSON envelope contains a wait block with the prompt, input schema, and resume command. Gather the input from the user via a structured prompt, then resume:

cpf resume --run-id <id> --event <event_name> --input '{"key": "value"}'