Skip to content

Durability ​

A run of coding agents takes minutes to hours, and things go wrong partway through: an agent gives no answer, you press Ctrl-C, the machine reboots, or you fix a bug in the workflow and want to go on. This page says what is still there afterwards and what a continue does with it, so you know what runs again. Runs explains ids, attempts and the commands.

The short version: a run keeps the value of each stage that succeeded. Whatever ends an attempt, the next one reuses those values and picks up at the first stage without one.

An example ​

Three stages, each one agent's turn:

ts
export default defineWorkflow({
  meta: {
    name: "fix-issue",
    description: "Plan, build and review a fix.",
    version: "1.0.0",
  },
  async run(wf, args: { issue: string }) {
    const plan = await wf.stage("plan", { result: PLAN }, async () => {
      const planner = await wf.agents.open({
        key: "planner",
        runtime: "claude",
      });
      const { outcome } = await planner.run({
        prompt: `Plan a fix for ${args.issue}. Name the branch you will work on.`,
        schema: PLAN,
      });
      if (!outcome.answered) return wf.stop(`no plan: ${outcome.reason}`);
      return outcome.value;
    });

    const built = await wf.stage("implement", { result: BUILT }, async () => {
      const builder = await wf.agents.open({
        key: "builder",
        runtime: "claude",
      });
      const { outcome } = await builder.run({
        prompt: [
          `On ${plan.branch}, carry out:`,
          ...plan.steps,
          "Commit, and give the commit.",
        ].join("\n"),
        schema: BUILT,
      });
      if (!outcome.answered) return wf.stop(`not built: ${outcome.reason}`);
      return outcome.value;
    });

    return wf.stage("review", { result: VERDICT }, async () => {
      const reviewer = await wf.agents.open({
        key: "reviewer",
        runtime: "codex",
      });
      // A continue's agents are new: the prompt names everything the reviewer needs.
      const { outcome } = await reviewer.run({
        prompt: `Review commit ${built.commit} on ${plan.branch} as a fix for ${args.issue}.`,
        schema: VERDICT,
      });
      if (!outcome.answered) return wf.stop(`no review: ${outcome.reason}`);
      return outcome.value;
    });
  },
  prepare: ({ argv }) => ({ issue: argv.join(" ") }),
});

You start it with an id, so you can come back to it:

sh
awf run fix-issue.ts --id 212 -- "#212: dates before 1970 parse as null"

Say the reviewer never answers. The attempt stops in review, with plan and implement recorded. You go on:

sh
awf run fix-issue.ts --id 212 --continue

plan and implement are reused: their work isn't called, so no agent is opened or asked anything, and each wf.stage(…) returns the value recorded the first time. Only review runs again. A test shows it, with no agents and no cost:

ts
test("a continue reuses plan and implement, and only the review runs again", async () => {
  const run = await testWorkflow(fixIssue, {
    args,
    earlier: { stages: { plan, implement: built } }, // what attempt 1 recorded
    agents: { reviewer: answer(VERDICT, { approved: true, findings: [] }) },
  });
  expect(run.value).toEqual({ approved: true, findings: [] });
  // Reused stages open no agent.
  expect(run.agents.map((agent) => agent.key)).toEqual(["reviewer"]);
  expect(run.turnsOf("reviewer")[0]!.prompt).toContain("4be1c07 on fix/212");
});

What survives what ​

What happensThe attempt endsWhat is keptWhat a continue does
The work in a stage throwsfailedevery stage that succeeded; this one recorded failedredoes this stage, then the rest
wf.stop(reason) inside a stagestoppedas above; this one recorded stoppedredoes this stage
wf.stop(reason) between stagesstoppedevery stage record, unchangedruns the code between stages again, so the check checks again
--timeout runs outtimed-outas for a throw; the stage it was in is recorded failedredoes that stage, with no deadline unless you give one again
Ctrl-C, kill, or the terminal closingcancelledas for a throw; the open stage is recorded failedredoes that stage
A crash: kill -9, a second Ctrl-C before the attempt has ended, the power, a rebootno ending: the run reads interruptedevery stage that ended before it; the one in flight has no recordstarts at the stage in flight
The laptop sleepsnothing, until it wakeseverythingdepends on how it then ends (below)
You change the code between attemptsevery stage recordruns the new code, reusing the records that still fit it (below)
The run completedcompletedeverythingrefused, unless --from-stage names a stage to redo

In a run without events, a crash inside the stage --from-stage named leaves that redo to the next continue: it starts at that stage again, and says so. It doesn't carry the crashed attempt's --values, so a stage before it with no record to reuse stops for its value.

A stage is the unit: nothing finer is kept. Two agent turns in one stage are redone together, so a step worth keeping on its own is a stage of its own. A workflow with no stages keeps nothing to reuse, and a continue runs all of it again.

A sleeping laptop ​

Sleep pauses awf and its agents, but not the clock. A deadline is a time of day, so the time asleep counts against --timeout and every other deadline, and a run past its deadline ends timed-out soon after the machine wakes. A sleep that ends in a shutdown is a crash.

What a continue reuses and what it redoes ​

As the workflow enters each stage, the continue decides:

  • Before the start point, each stage with a succeeded record is reused. Its value is checked against the stage's result schema as it is now, and handed back; its work is never called.
  • The start point is the first stage entered with no succeeded record, or the stage --from-stage names. From there, every stage runs, even those that succeeded before: a review of an implementation that is being redone is stale. Their old records stay in the run's run.jsonl; nothing is deleted.
  • Agents start fresh. A continue opens its agents anew, and they remember nothing of the stages it reused. That is why the review prompt names the commit and the branch.

The progress view marks each reused stage ↺, with the attempt that ran it, and its age once it is over an hour old, so a stale record doesn't slip past you.

When the code changes ​

Continuing after a fix is the main use, so a code change is expected. The run belongs to the workflow's meta.name, not its file: you can continue from a moved or copied file. awf checks what it reuses, from coarse to fine:

What changedWhat happens
prepare no longer accepts the run's recorded argumentsthe continue is refused: start a new run
meta.version's major (under 0.x, its minor), when both the record and the code have onethe attempt stops at the first stage recorded by the other version
A stage's result schema rejects its recorded value, or the stage gained or lost a resultthe attempt stops at that stage, saying why
A stage was added or renamedit has no record, so a plain continue starts there; after --from-stage, the attempt stops, listing the recorded stages it never reached
A stage was removedits record is never asked for, and stays in run.jsonl
A prompt, a model or anything elserecords are reused as they are

Each stop names the stage, and the command it prints goes on with --from-stage {stage}, which redoes it and everything after. A minor version, a reworded prompt or a new model redoes nothing: whether this run should redo a stage after a fix is your call, made with --from-stage. An optional field added to a result keeps old records valid; a required one means redoing that stage.

After a crash ​

An attempt with no ending whose process is gone reads interrupted in awf runs and awf logs, and awf logs prints the command that goes on. The next attempt names each interrupted one and the Herdr workspace its panes were in, the one named awf fix-issue 212 #1. Those panes stay open until the next run that opens a TUI agent, which closes the workspaces of runs whose process is gone. The agents' turns up to the crash are in the run's event log. What it used is read from its agents' sessions by the next attempt, as it starts, and counted in the run's cost.

Two continues of one run at once can't both run: the second is refused, naming the attempt still running and its process.

Runs that wait for events ​

A workflow with events parks between events, with no process left running, so a crash or a closed laptop costs it nothing. Its stages work differently: one that succeeded is reused for the rest of the run, whatever meta.version the code has, so the table above doesn't apply to it. Events covers these runs.

Catches ​

  • A crash can leave an agent working. Each agent's harness runs in a process group of its own, so killing awf doesn't kill it, and it may still be editing your files. Check that none from the crashed attempt is still running before you continue.
  • A stage runs its work, then writes its record. A crash between the two runs the work again on the next attempt. A push or a post that must not happen twice checks first whether it already did.
  • A check on a stage's value belongs inside that stage. Written between stages, a continue reuses the value and stops again. awf notices a continue that ran no stage and stopped where the last attempt did, and says which stage to redo.
  • A renamed stage has no record. A plain continue runs it and every stage after it.
  • The runs live in .awf/runs under the folder the run works in, which awf keeps out of git (the record). Deleting it, as git clean -xfd does, deletes everything a continue would reuse.

See also ​