# Snapshots and resume

A snapshot contains finished steps' results and cache keys, as JSON. A new run can reuse those finished steps instead of executing them again.

**This is completed-step reuse, not durable execution.** June doesn't checkpoint a running model or tool call. Anything that hadn't finished starts again, including its side effects.

## What resume isn't

- **Durable execution.** A run lives in one process. Nothing restarts it, and nothing checkpoints work in flight.
- **Exactly-once.** A step that didn't finish runs again, side effects included.
- **Available from the CLI.** `june run` has no snapshot option; resume from code.

For work that genuinely lives outside the process, such as a training job, use a [Job](https://june.dev/docs/training/jobs.md): a backend owns it, and a later process can find it again. `<FineTune>` can wait on a job it started earlier; see [Training in workflows](https://june.dev/docs/training/workflows.md#durability).

## Resuming a run

```tsx
import { openai } from "june/sdk/openai";
import { Agent, Step, run, start, type RunSnapshot } from "june";

declare function save(snapshot: RunSnapshot): Promise<void>;
declare function load(): Promise<RunSnapshot>;

const workflow = (
  <Agent model={openai("gpt-5-mini")}>
    <Step id="plan" prompt="Plan the deploy">
      {(plan) => <Step id="checklist" prompt={`Write a checklist for:\n${plan}`} />}
    </Step>
  </Agent>
);

const handle = start(workflow);
try {
  await handle.done;
} finally {
  await save(handle.snapshot());
}

// later, or in another process
await run(workflow, { snapshot: await load() });
```

```json
{
  "version": 1,
  "steps": {
    "plan": {
      "cacheKey": "step:1b0e5c3f9a27d4",
      "result": "Deploy web, then api"
    }
  }
}
```

If `checklist` failed the first time, the second run reuses `plan` and runs only `checklist`.

## The resume boundary

The boundary is the step. A step is either finished, and in the snapshot, or it isn't, and it runs again from the start.

**Reused:** a step whose id and [cache key](https://june.dev/docs/workflows/step-identity.md) match a snapshot entry. It doesn't run and takes no concurrency slot; its children render right away with the saved result. Its trace node is `done` with `resumed: true` and its `result`, and has no timing, `model` or `usage`, since it didn't run. Its result is in `steps`, but it adds nothing to the run's `usage`.

**Runs again:**

- a step that was running, queued, or waiting on an [approval](https://june.dev/docs/workflows/approvals.md#approvals-and-resume) when the snapshot was taken
- a step that failed
- a step whose prompt, settings, tools or output changed since, so its cache key no longer matches
- a step the snapshot has no entry for

June doesn't checkpoint inside a step. A model call or tool loop that was in flight starts over, so a tool can run twice across a crash. Make tools with side effects safe to repeat, or put them behind an approval.

## `snapshot()`

`handle.snapshot()` works at any time: while the run is going, after it settles, and after it fails or aborts. It returns:

- every step that has finished in this run, with its result and cache key
- entries from the run's own `snapshot` option that it hasn't rendered, so steps behind a branch the run didn't take aren't lost

An entry whose step renders with a different cache key is dropped, and replaced if the step finishes.

`run()` and `start()` check a snapshot before rendering anything, and throw for one without `version: 1`, or with a step that lacks a `cacheKey` or a `result`.

## Saving snapshots

June has no storage: the snapshot is JSON, and saving it is up to you. Results go in as they are, so return JSON-safe values from [`useStep`](https://june.dev/docs/workflows/use-step.md#results) if you serialize the snapshot. `undefined` is lost in JSON, and a step entry without a `result` is rejected.

To keep finished steps across a crash, save as steps finish. `runResumable` writes the snapshot to a file whenever a step finishes, resumes from that file when it exists, and removes it once the run succeeds:

```ts
// resumable.ts
import type { ReactNode } from "react";
import { start, type RunSnapshot } from "june";

export async function runResumable(workflow: ReactNode, path: string) {
  const file = Bun.file(path);
  const snapshot: RunSnapshot | undefined = (await file.exists())
    ? await file.json()
    : undefined;

  let saved = Object.keys(snapshot?.steps ?? {}).length;
  let saving = Promise.resolve();

  const handle = start(workflow, {
    snapshot,
    onUpdate: () => {
      const latest = handle.snapshot();
      const finished = Object.keys(latest.steps).length;
      if (finished === saved) return;
      saved = finished;
      saving = saving.then(async () => {
        await Bun.write(path, JSON.stringify(latest));
      });
    },
  });

  try {
    const result = await handle.done;
    await saving;
    await file.delete();
    return result;
  } catch (error) {
    await saving;
    await Bun.write(path, JSON.stringify(handle.snapshot()));
    throw error;
  }
}
```

A snapshot's results are used as they are, without running anything. Load snapshots only from storage you trust, like any other input to your workflow.

## Next

- [Traces and usage](https://june.dev/docs/workflows/traces.md): how a resumed step shows on the trace.
- [Step identity](https://june.dev/docs/workflows/step-identity.md): exactly what a cache key covers.
