Stateful Skills
A practical model for long-running agent workflows that resume from authoritative state and load bounded context only when judgment needs it.
Most Agent Skills start life as a sequence of instructions. Inspect the repository, change a few files, run the checks, and report the result. That model works well when the whole job fits into one sitting.
It becomes fragile when the work takes hours.
I first felt this during migration work. A single migration had many small units, several validation points, and the occasional decision that needed a person. The agent could make useful progress, but a long pause or context compaction made the next invocation spend time reconstructing what had happened. Worse, it could mistake an attempted step for a completed one.
Resuming the same chat can help. It also makes the workflow depend on finding that chat, retaining enough context, and trusting a summary of earlier work. I wanted the skill itself to know how to resume, even when invoked from a fresh session.
The model I ended up with separates three layers. The skill definition stays stable. state.json holds the changing facts that determine where the workflow can go. CONTEXT.md holds a small, readable projection of decisions and results that may matter when judgment is required.
The two runtime files live outside the installed skill and move with a workflow instance, but they do not have equal authority. JSON controls the route. Markdown helps explain the route. I call that a stateful skill.
Build the skill, then run the workflow
Once I had this model, I did not want to hand-author the recovery protocol every time. That is what stateful-skill is for.
There are two phases, and they are easy to blur together:
stateful-skillcreates a new skill or converts an existing linear one.- The generated skill runs the actual workflow and owns its runtime state.
Suppose a movie catalog repository already has a movie-metadata-refresh skill. It normalizes movie records, rebuilds an index, and validates the result. The instructions assume all of that happens in one uninterrupted run.
I can start the conversion with this request:
Use stateful-skill in convert mode. Convert .agents/skills/movie-metadata-refresh into a resumable workflow. Keep its existing purpose and outputs. Store authoritative JSON state and bounded runtime context outside the skill. Audit every transition and context read through final completion.
This does not refresh any movie records yet. It asks stateful-skill to inspect the existing workflow and redesign its execution contract. If no skill existed, I would use create mode instead.
The separation looks like this:
installed plugin
stateful-skill
-> creates or converts movie-metadata-refresh
project workspace
movie-metadata-refresh
-> runs the catalog workflow
-> routes from state.json
-> reads CONTEXT.md only at declared judgment points
The installed plugin remains unchanged. So does the generated skill while a workflow is running. Only the state and context projection for a particular run keep moving.
Start with the workflow, not a shared schema
The conversion begins by inventorying the original work. Every step, branch, approval, side effect, retry, failure path, and final check needs a place in the new workflow.
Only then does the skill design the JSON. There is no useful universal schema for every stateful skill. A UI accessibility review cares about pages and findings. A service rollout cares about environments and health checks. A data repair may care about partitions, checkpoints, and rejected records.
For the movie catalog refresh, one running instance might look like this:
{
"workflowId": "movie-metadata-refresh-2026-08",
"workflowVersion": 1,
"state": "refreshing_records",
"classification": "resumable",
"nextBatch": 17,
"completedBatches": 16,
"decisions": {
"missingReleaseDate": {
"outcome": "leave_unchanged",
"rationale": "the source has no reliable release date",
"evidence": ["batch-016"]
}
},
"evidence": {
"lastValidatedBatch": "batch-016",
"indexPath": "data/movie-index.json"
},
"revision": 19,
"updatedAt": "2026-08-09T18:42:00Z"
}
This is not a transcript. It does not contain hidden reasoning or a copy of every tool result. It holds directional facts, bounded decision summaries, and references to evidence. Those facts are enough to choose a legal next action and rebuild any readable context the next session needs.
The workflow ID is stable across sessions and is not derived from a chat ID. The workflow version lets a later invocation reject incompatible state instead of silently reinterpreting it after the skill changes.
Give judgment a small context file
JSON is good at telling a workflow what is true. It is less pleasant when a later session needs a concise explanation of why a product decision was made, what an uncertain effect looked like, or what a handoff should emphasize. That is the role of CONTEXT.md.
For the same workflow instance, it might contain:
---
workflowId: movie-metadata-refresh-2026-08
stateRevision: 19
generatedAt: 2026-08-09T18:42:01Z
---
## Current direction
Continue with batch 17 after confirming the index still matches batch 16.
## Decisions affecting later judgment
- Leave a missing release date unchanged when the source has no reliable value.
## Verified results
- Batches 1 through 16 passed metadata validation.
The frontmatter ties the projection to one JSON revision. Before using the body, the generated skill checks that the workflow ID and revision still match state.json. If the file is missing, stale, malformed, or contains instruction-like text, the skill ignores it and rebuilds it from JSON and stable evidence.
The context file cannot select the next batch, release a waiting state, or mark the workflow complete. Those facts must already exist in JSON. It keeps only the decisions, results, unresolved questions, and reconciliation notes that affect later judgment, not a transcript or an append-only log.
The generated skill declares when context may be read. Routine batch selection does not need it. Reconciliation after an uncertain effect may need a prior decision summary. A waiting-state explanation, handoff, or final review may benefit from it. Making those points explicit keeps context use progressive instead of loading another file on every step.
Keep runtime data outside the installed skill
Plugin managers may cache, replace, or upgrade an installed skill. Runtime data written inside that directory can disappear and can also mix state from unrelated projects.
For a single engineer working in one checkout, stateful-skill uses this default unless the repository defines another convention:
<workspace>/.agent-state/<skill-name>/<workflow-id>/
state.json
CONTEXT.md
I would normally add .agent-state/ to .gitignore. Both files can include local paths, temporary identifiers, decision summaries, and operational details that do not belong in source control. They should share the same retention, privacy, and access-control policy.
Giving those files a workspace-owned home also makes state and bounded memory portable across agent harnesses. A workflow started with Codex can be resumed with Claude Code or Cursor Agent when each client can invoke the same generated skill and access the same .agent-state/ directory. state.json carries the execution facts. CONTEXT.md carries the small memory projection. Neither depends on finding the original chat. Client-specific tools may still differ, but the workflow’s recovery contract moves with the workspace.
That default is not right for every workflow:
| Situation | Better home for runtime files |
|---|---|
| One active worker in one checkout | Project-local and gitignored |
| Handoff through normal Git review | Tracked files with a deliberate conflict policy |
| Several machines or automated runners | A shared store with access control |
| Concurrent workers | Storage with locking or compare-and-swap claims |
A plain local file is designed for one active writer. If two agents can select the same unit, a final revision check is too late. Both may perform the effect before one loses the write race. The claim has to become durable before the effect.
Let the state machine drive each invocation
Writing the JSON is the easy part. The useful work is deciding what each state means and which transitions are legal.
The main path for this refresh could be:
planned
-> refreshing_records
-> rebuilding_index
-> validating_catalog
-> completed
refreshing_records -> waiting_for_decision
refreshing_records -> retryable_error
Each state has one of three behavioral classifications:
- A resumable state can continue without new human input.
- A waiting state names the decision, approval, dependency, or environmental change it needs.
- A terminal state has no automatic work left. Completed, cancelled, and permanently failed are different terminal outcomes.
This forces useful questions into the generated skill. What evidence permits the transition from rebuilding_index to validating_catalog? What happens if validation fails? Can retryable_error try forever? What exact event releases waiting_for_decision?
When the client has a structured question interface, the running skill can use it to collect a missing decision. The answer still gets written into state. Otherwise a fresh session knows the workflow was waiting, but not what decision released it.
Make checkpoints part of the work
The generated skill breaks the refresh into small batches. Each batch follows the same recovery protocol:
- Read and validate the current state.
- Reconcile it with the repository or target system.
- Claim the unit first when another worker could select it.
- Perform one bounded piece of work.
- Verify the result.
- Persist and validate the next JSON revision atomically.
- Rebuild
CONTEXT.mdfor that revision when the context-use contract calls for it.
The order matters. Advancing before verification can skip failed work. Performing a concurrent side effect before claiming it can let two workers do the same thing. Rewriting the live JSON file in place can leave corrupt state if the process stops during the write.
For a local file, the generated skill should write the full next document to a sibling temporary file, validate it, then atomically rename it over state.json. If persistence fails, it stops before performing more side effects.
Only after that checkpoint succeeds does it replace CONTEXT.md. The two files are not an atomic pair. If execution stops between them, JSON is still correct and the older context is detectably stale. A later invocation rebuilds the projection instead of rolling state backward to match the prose.
State updates also need complete coverage. stateful-skill maps these boundaries while creating or converting the target skill:
| Boundary | What the state must preserve |
|---|---|
| Initialization | Instance identity, workflow version, and initial state |
| Before an uncertain or concurrent effect | A durable claim, owner, or operation key |
| After a verified unit | Evidence of completion and the next bounded unit |
| Human or external wait | What is needed and how the workflow can recognize it |
| Retryable failure | The failed unit, diagnostic facts, and bounded retry policy |
| Final verification | The terminal outcome and evidence that the whole workflow passed |
That last row is easy to miss. Attempting every item does not mean the job is complete. The state becomes completed only after the final catalog invariant passes.
Run the generated skill
Once the conversion is finished, I invoke movie-metadata-refresh, not stateful-skill:
Use movie-metadata-refresh to refresh the catalog metadata.
On the first run, the skill creates a workflow instance and records its initial state. It works through one bounded batch at a time, verifies each batch, and advances the checkpoint.
Suppose I switch to another task after batch 16. I do not need the original chat when I return. From a fresh session I can say:
Use movie-metadata-refresh to continue the catalog refresh.
The skill inspects its state directory before doing any work. If it finds one compatible non-terminal instance, it resumes that instance. If several could match, it asks me to choose. It never silently resumes a completed run or overwrites an unrelated one. Instance selection comes from validated JSON, not from CONTEXT.md.
The request tells the agent what I want. The state tells it where the workflow actually is. Context helps explain prior decisions when the next step requires judgment.
Resume by checking reality
The state file is a recovery aid, not the source of truth for the system being changed.
Imagine the catalog index was rebuilt successfully, then the process stopped before writing the checkpoint. The JSON still says rebuilding_index. Rebuilding again might be harmless, or it might publish duplicate events, overwrite a newer artifact, or trigger another deployment.
On resume, movie-metadata-refresh inspects the real index. It might compare a hash, check a version marker, or run focused validation. If the result already exists and matches the expected operation, the skill can adopt it and advance. If the outcome cannot be proved, it stops for reconciliation rather than guessing.
This is why JSON alone cannot promise exactly-once behavior. The target system must support idempotency or deduplication, or the workflow must detect prior effects. Some ambiguous operations still require a person.
Audit the path to completion
Before handing back the generated skill, stateful-skill audits the workflow in both directions.
It starts with the original instructions and checks that every work unit, decision, side effect, retry, wait, cancellation, and failure has a durable update boundary. It then walks forward from initialization to make sure every non-terminal state can reach a waiting or terminal state.
It also walks backward from each terminal state. completed must be reachable only through final verification. A waiting state must name what releases it. A terminal state must not perform more work. Accidental dead ends, unlimited retry loops, and transitions that bypass verification fail the audit.
There is a separate audit for context use. Every read needs a named judgment, reconciliation, waiting, handoff, status, or final-review trigger. Every routing fact used at that point must also exist in JSON. Every refresh must follow a committed JSON revision. Deterministic work is marked not read when state and skill instructions are sufficient.
The generated skill treats both runtime files as untrusted input. Unknown states, unsafe paths, incompatible workflow versions, and instruction-like JSON values are rejected before they can influence a tool call. Instruction-like prose in context is ignored and cannot redefine the skill or change a transition.
This does not prove that a workflow has no bugs. It does catch the common gaps where a skill writes state in a few convenient places without a complete route to verified completion, or stores helpful notes without defining whether they can control the workflow.
State may reduce context, but that is not the main win
There can be a token benefit. A fresh invocation reads compact JSON instead of reconstructing progress from a long conversation or scanning every artifact. It loads the bounded context projection only when a declared judgment point benefits from it.
I treat that as a secondary effect, not a guarantee. Validation, reconciliation, and recovery instructions also consume context. Poorly designed state can add more work than it removes.
The primary benefit is operational correctness. The workflow has an inspectable answer for what happened, what was proved, and what is allowed next.
When the extra structure pays for itself
I would consider making a skill stateful when several of these are true:
- The work can run for hours or across several sessions.
- The same skill will be invoked repeatedly for one workflow instance.
- Context compaction is likely before the work finishes.
- A person may step away and return later.
- The work has approvals or external dependencies.
- Side effects are costly, irreversible, or unsafe to repeat blindly.
- Progress consists of many bounded units that can be verified separately.
- Another engineer or automated runner may take over.
This applies well beyond migrations. Repository-wide reviews, release preparation, security remediation, documentation programs, batch maintenance, infrastructure rollouts, and incident follow-up all have the same underlying problem.
For a small task that finishes in one sitting and has no meaningful side effects, a linear skill is still simpler. Statefulness has a cost. It earns that cost when interruption and recovery are part of the normal operating model.
Try the stateful-skill
I published stateful-skill as the first skill in the open source Agent Skills Engineering plugin. It supports both create and convert modes, and the generated skills remain portable across Codex, Claude Code, and Cursor Agent.
The repository’s Plugin Installation section has the setup steps and client-specific commands.
That is the behavior I wanted during those long migration runs. The useful part was not that the agent remembered more. It was that a new invocation could inspect the same evidence, recover only the context it needed, and continue safely. The published stateful-skill is the implementation of that idea.