Create and edit checkpointflow workflow YAML files. Use this skill whenever the user wants to create a workflow, automate a process as a YAML pipeline, define steps that mix CLI commands with human or agent checkpoints, turn a conversation into a reusable runbook, or asks about writing cpf/checkpointflow workflows. Also use it when you see keywords like "workflow", "runbook", "approval flow", "pause and resume", or "await event" in the context of automation.
From cpfnpx claudepluginhub thomasrohde/marketplace --plugin cpfThis skill uses the workspace's default tool permissions.
references/patterns.mdProvides demand forecasting, safety stock optimization, replenishment planning, and promotional lift estimation for multi-location retailers managing 300-800 SKUs.
Creates consistent pitch decks, one-pagers, investor memos, financial models, accelerator apps, and fundraising materials from a single source of truth.
Provides expertise on electricity/gas procurement, tariff optimization, demand charge management, renewable PPA evaluation, hedging, load profiling, and multi-facility energy strategies.
Create checkpointflow workflow YAML files that are valid, idiomatic, and ready to run with cpf run.
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.
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?
Design the step sequence. Map each action to a step kind:
cliawait_eventendDecide where to put the file. If the user specifies a path, use it. Otherwise, prompt the user to choose:
.checkpointflow/<id>.yaml) — scoped to the current project, checked into the repo~/.checkpointflow/<id>.yaml) — available everywhere via cpf flowsexamples/<id>.yaml) — only offer this if inside the checkpointflow repo itselfSkip 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).
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.
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.
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/v1workflow.id must be present, snake_case, and uniqueworkflow.inputs uses JSON Schema to define what the caller must provideidThe 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:
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.
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.
All step kinds below are fully supported at runtime.
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"
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.
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).
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.
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).
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.
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.
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 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
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.
schema_version: checkpointflow/v1 at the top level — every file needs it.id must be unique across the entire workflow.next value must match an actual step id.${} in if conditions — if takes a bare expression, not wrapped in ${}.input_schema on await_event — it's required, not optional.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.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.cli, await_event, end, switch, api, foreach, parallel, and workflow. Any other kind fails at runtime with exit code 80.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"}'