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 runhas no snapshot option; resume from code.
For work that genuinely lives outside the process, such as a training job, use a Job: 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.
Resuming a run
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() });
{
"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 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 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
snapshotoption 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 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:
// 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: how a resumed step shows on the trace.
- Step identity: exactly what a cache key covers.