Resumable hierarchical workflow tracking for Claude Code — a Goal->Phases->Tasks tree with first-class forks that spawn sub-workflows and auto-resume the parent. State persists per-project so any session can pick up cold.
SaferSkills independently audited workflow-builder (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Real builds aren't linear. They look like trees: a goal at the root, with phases that decompose into tasks, that occasionally fork into entirely new sub-workflows (when a parent task discovers it needs a tool/dataset/process that doesn't exist yet).
>
Running example used throughout this skill: "Ship a public `/reports` REST API." Midway through building the endpoint you discover there's no rate-limiter — and you can't expose a public API without one. That's a fork: you spawn a rate-limiter sub-workflow, build it, return to the API work, and later realise the rate-limiter is reusable and graduate it into a shared platform skill.>
This skill makes that tree explicit and persistent.
Three failure modes this skill prevents:
/reports API; it's actually shared middleware every endpoint needs. The skill captures this graduation explicitly.Not everything is a workflow. There are four distinct shapes of work, with different state-keeping needs. Pick the right shape before scaffolding anything:
| Shape | Lifecycle | State location | Examples |
|---|---|---|---|
| One-shot task | Atomic — start to finish in one shot, no decomposition | Just chat / no file state | Reply to an email, fix a typo, ad-hoc query, kill a stray process |
| Workflow | Finite — has a "done" state, may fork, can graduate | <project>/workflow-state/ (this skill) | The /reports API build, the rate-limiter sub-workflow (during its build), a single feature ship |
| Operation | Recurring — same shape each cycle, runs on cadence | <project>/runs/<YYYY-MM>/workflow-state/ (one instance per run) | A monthly release, a nightly data-sync, a weekly report pipeline |
| Development | Open-ended — product that keeps evolving, never "done" | Conventional project folder (CONTEXT.md + PROGRESS.md + code) | The main app, a long-lived service, the SaaS product itself |
Graduation ladder (work moves UP as it matures):
One-shot task → Workflow → Operation → Development
atomic finite recurring open-ended
no state per-build per-instance continuousCrucial: an Operation IS a workflow that runs on a cadence. Same tree shape, instantiated each cycle. Phases under an operation can themselves be sub-workflows (graduated from earlier one-time builds) OR atomic tasks (a human clicking "approve" once a cycle) OR nested operations.
When does work move up the ladder?
Things rarely move DOWN. A retired development becomes an operation (just run, no enhance); an unused operation becomes a one-shot fallback.
These are orthogonal. Don't conflate them.
| Skill | Workflow | |
|---|---|---|
| Grammatical shape | Noun (a capability) | Verb (an execution) |
| Where it lives | .claude/skills/<name>/ (versioned alongside code) | <project>/workflow-state/ (per-project state) |
| Lifecycle | Stable — refined over months, same across every invocation | Has a start, has a "done", may fork, may merge |
| Reusable? | Yes — invoke from any project, any session | No — this specific instance is one-time |
| Source of truth for | "How do we do X?" | "Where are we in this particular X?" |
| Analogy | Recipe in a cookbook | Tonight's dinner (with its own "garlic ✓, no olive oil" state) |
They interact like this: workflows USE skills. A single workflow can invoke many skills as it progresses — the /reports API build pulls in a testing skill for the handlers and a UI skill for the docs page. The workflow-builder skill orchestrates STATE; domain skills do WORK.
Skills are horizontal (capabilities — same across projects). Workflows are vertical (one specific delivery — pulls horizontal skills as needed).
The extraction pattern (how the skill library grows):
Workflow runs once → done, archive the state folder
Workflow recurs → EXTRACT the pattern into a skill (Phase 5 resume step covers this)
Skill matures → other workflows discover and use itThe rate-limiter is the proof case. Started as a fork off the /reports API. Finished. Realised every public endpoint needs it. We extracted the pattern into a shared middleware skill. The original workflow state lives on as historical record; the skill is the reusable capability.
When you finish a workflow, ask: "will this recur?" If yes → extract into a skill before closing.
blocked-byPer workflow, state goes in <project>/workflow-state/:
<project-folder>/
├── CONTEXT.md ← human-readable project overview (existing)
└── workflow-state/
├── workflow.yaml ← the tree (single source of truth)
├── current-pointer.md ← short "you are here" note (auto-updated)
└── log.jsonl ← append-only history of node transitionsThis is intentional — the workflow state travels with the project, not with the skill. The skill is the grammar; each project's workflow.yaml is a sentence.
| # | Phase | When | See |
|---|---|---|---|
| 1 | Define goal | First touch on a new workflow | phases/01-define-goal.md |
| 2 | Decompose | After goal is locked, before any execution | phases/02-decompose.md |
| 2.5 | Pseudocode the leaf (optional) | Between Decompose and Execute when the leaf has non-trivial control flow — write the language-neutral logic before touching code | (no per-phase doc; see notes below) |
| 3 | Execute node | Most of the time — work one leaf at a time | phases/03-execute-node.md |
| 4 | Detect & spawn fork | When a node can't progress without something the workflow doesn't have | phases/04-detect-fork.md |
| 5 | Resume after sub-completes | When a sub-workflow finishes — return to parent | phases/05-resume.md |
Every phase is a CONTRACT. Three fixed sections — Inputs / Process / Outputs — so anyone (or any future session) can read it in 30 seconds and know what loads, what runs, what comes out.
- id: phase-2-build-endpoint
title: Build the /reports REST handlers + auth check
status: planned
inputs:
- source: L3:reference # factory — stable across runs
file: docs/architecture.md
section: "## API conventions" # ← selective section routing
why: routing + error-envelope pattern every endpoint follows
- source: L4:prev-phase # product — this run's artifacts
file: ../phase-1-schema/output/reports-schema.md
section: full file
why: the response shape the handlers serialise
process: |
1. Add GET /reports + GET /reports/{id}
2. Wire the auth middleware + error envelope
3. Add handler tests, run them green
outputs:
- artifact: endpoint handlers
location: src/api/reports.py
format: python
- artifact: test report
location: workflow-state/phase-2-execute.md
format: markdownExisting workflows using notes: | prose for phases still work — notes remains an optional field. New workflows (and refactors of stuck workflows) should use the structured triple. When restructuring an old workflow, the notes: prose typically splits cleanly: lines that name files become inputs:, numbered steps become process:, "Done when:" criteria become outputs:.
Workflow phases declare contracts in YAML. The scripts that execute those phases should declare the SAME contract in their module docstring — so when you open the file directly (without the workflow.yaml in view) you can still see in 5 seconds what loads, what runs, what writes.
The shape — first thing in the docstring, before anything else:
"""Phase N — <one-line goal>.
<2-3 sentence summary of what this pass does and why it exists.>
Reads:
<relative path> # L3 (reference) — what it provides
<relative path> # L4 (working artifact) — which prior phase produced it
Writes:
<relative path> — <one-line description>
Methodology:
- <key decision 1>
- <key decision 2>
- <gotchas, threshold values, fallback chains>
"""Worked example (a generic ETL leaf):
"""Phase 3 — Build the feature matrix.
Join raw events with the customer dimension, derive per-customer aggregates,
and write a single parquet the model phase consumes.
Reads:
data/raw/events.parquet # L4 (Phase 2 output — cleaned events)
data/dim/customers.csv # L3 (stable: customer dimension)
config/feature_spec.yaml # L3 (stable: which aggregates to compute)
Writes:
data/output/feature_matrix.parquet — one row per customer, model-ready
workflow-state/phase-3-execute.md — row counts + null-rate report
Methodology:
- Left-join on customer_id; unmatched events dropped (logged count)
- Aggregates: 7/30/90-day windows; missing → 0 with a presence flag
- Fails loudly if null-rate on any key feature > 5%
"""Why this matters. Three failure modes it prevents:
Path("...") calls scattered through 300 lines.When is a script too small to need this? If it's <50 lines AND only reads/writes one file each AND is not part of a multi-phase pipeline, a prose docstring is fine. Everything in a scripts/ folder that's part of a numbered or named phase: structured contract.
workflow.yaml schema)goal:
id: <short-slug>
title: <one-line>
status: planning | executing | blocked | completed
why: <one-paragraph why-this-matters>
phases:
- id: <slug>
title: <one-line>
status: planned | in-progress | blocked | done | skipped
completed_at: <iso-date or null>
# Stage contract — see "Stage Contracts" section above
inputs: # what to load
- source: L3:reference | L4:prev-phase | L4:user-upload
file: <path>
section: <"full file" | specific section header>
why: <one line>
process: | # ordered steps
1. ...
2. ...
outputs: # what this produces + where it lands
- artifact: <name>
location: <path>
format: <markdown | yaml | json | code | ...>
notes: <free text> # optional — only for stuff that doesn't fit above
tasks:
- id: <slug>
status: ...
# tasks may be leaves OR may have sub-tasks (nested same shape)
forks:
- id: <slug> # the fork's local id within the parent
spawned_at: <iso-date>
spawned_from: <parent-node-id> # which node triggered this
reason: <why we forked>
sub_workflow: <path> # link to the sub-workflow's own state folder
status: open | merged | abandoned
return_to: <parent-node-id> # where we resume when sub-workflow closes
current_node: <node-id> # where execution should pick up
last_updated: <iso-date>forks:, not silently in a task. Every fork must list its return_to.done only when all tasks under it are done or skipped. The skill auto-rolls this up — never set manually.abandoned instead.When a sub-workflow finishes AND turns out to be reusable (recurring monthly, recurring per-feature, etc.), don't leave it as a one-off in the parent's forks: list. Graduate it into a longer-running workflow or a skill:
*-workflow skill, or extract a brand-new skill)merged → graduated_to: <host>/phase-NThe rate-limiter IS this. It started as a fork off the /reports API; it's now a shared middleware skill every endpoint pulls in.
When you finish a workflow, ask: "will this recur?" If yes → extract into a skill before closing.
workflow-state/, write skeleton workflow.yaml, ask the 4 clarifying questions (goal / why / output / first 3 phases)current_nodeworkflow.yaml, render the tree, point at current_nodemerged, restore parent current_nodeThe skill NEVER executes the underlying work itself. It just keeps the tree honest.
The skill's invocation is complete when:
<project>/workflow-state/workflow.yaml exists with a valid tree (goal → phases → tasks)current_node points at a SPECIFIC leaf id (not "Phase 3, somewhere")current-pointer.md is a one-paragraph human note that says "you are here, doing X, next is Y"log.jsonl has at least one transition line for the current sessionin-progress task that hasn't been touched in days)The workflow itself is complete when:
done or skippedmerged or abandoned (no open forks left)If a workflow current_node hasn't moved in 14 days, it's stuck — surface it, don't let it rot silently.
return_to pinned. Don't silently switch to building the missing thing inside the parent — the parent's current_node ends up lying about what's actually happening.current_node, EITHER move the pointer first OR explicitly note "deferring current_node to do X". Silent drift makes "where are we?" answers wrong.blocked, the next session won't know to pick it up.workflow-state/. Use the shape taxonomy honestly.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.