Step identity
A step's id names it within a run, and its cache key records what it asks. Together they decide when a step runs, when it shares another step's result, and when it reuses a snapshot. Three rules cover most of it:
- An id identifies one logical piece of work within a run.
- Derive a repeated step's id from stable input data, such as a file path or a record id.
- Changing what a
<Step>asks changes its cache key, even when its id stays the same, so it runs again instead of reusing an old result. AuseStep's cache key is only thecacheKeyyou give it, so change that yourself when its work changes.
import { openai } from "june/sdk/openai";
import { Agent, Step } from "june";
export default function Summaries({ files }: { files: string[] }) {
return (
<Agent model={openai("gpt-5-mini")}>
{files.map((file) => (
<Step key={file} id={`summary-${file}`} prompt={`Summarize ${file}`} />
))}
</Agent>
);
}
Each file gets its own id, derived from the file, so each summary is its own step.
Choosing ids
- Derive them from data: an issue number, a file path, a topic. Not from an array index, which names whatever item is at that position, and not from
Date.now()or a random value, which never matches a snapshot. - Keep them stable across versions of the workflow unless you want the step to run again.
- Prefix them with what the step does, so
triage-12andsummary-12don't collide.
An index looks harmless until the list changes:
import { Step } from "june";
declare const files: string[];
// Bad: after a file is added at the front, every `summary-${i}` names a
// different file, so none of them matches its snapshot entry and every
// summary runs again.
files.map((file, i) => (
<Step key={i} id={`summary-${i}`} prompt={`Summarize ${file}`} />
));
// Good: the id follows the file wherever it moves in the list, so only the
// new file's summary runs.
files.map((file) => (
<Step key={file} id={`summary-${file}`} prompt={`Summarize ${file}`} />
));
The cache key keeps an index id from reusing the wrong file's result, but it can't make the index useful: the work is identified by the file, so the id should be too.
React's key and June's id have different jobs. key tells React which element is which among siblings, so it can keep their state across renders. id names the step's work for the whole run and across runs: it decides sharing, snapshot reuse, and what steps is keyed by. Derive both from the same data, but they're not interchangeable: a key isn't seen by June, and an id isn't seen by React.
The rules
| When two steps have | June |
|---|---|
| different ids | runs both, even if they're otherwise identical |
| the same id and the same cache key | runs one, and every place it's rendered gets the same result |
| the same id and different cache keys | fails the render: june: step <id> was rendered with two different definitions |
The same applies across runs through a snapshot: a snapshot entry is reused only by a step with its id and its cache key. Anything else runs again.
- Ids are global to the run. They aren't scoped to the component that renders them, so a reusable component should derive its ids from its props.
<Step>,useStep,june/copy's<Write>andjune/train's components share one set of ids. - A render that fails on a duplicate id fails the run, unless a
<Catch>above it handles the error like any other. - A step that failed can render again with a new definition after a
<Catch>retry; the failed attempt doesn't hold its id.
What a <Step>'s cache key covers
June derives a <Step>'s cache key from what it asks the model:
- the
prompt, string or messages - the model's provider and model id, such as
openai.responsesandgpt-5-mini - the agent's other settings that are plain data:
instructions,temperature,providerOptions, itsid, and so on - each tool's name, description, input schema and title
- the kind of
outputand its schema
It doesn't cover:
- functions, which June can't compare: a tool's
execute,stopWhen,prepareStep, callbacks such asonStepFinish - settings that change how a call is made, not what it asks:
maxRetries,timeout,telemetryandexperimental_telemetry - anything about the model besides its provider and id, such as a provider's base URL or API key
- a tool's
needsApproval - the step's
streamprop and itschildren
Inside settings, object keys are compared in sorted order, undefined is skipped, a Date is compared by its ISO string, and other class instances by their toJSON(), or by their class name when they have none.
So a step whose prompt, instructions or output schema changed runs again instead of reusing a snapshot, but one whose tool implementation or stop condition changed doesn't. To make it run again, change its id (summary-v2) or drop its entry from the snapshot.
A step with output or tools suspends once per run, the first time it renders, while the AI SDK resolves their schemas into its key.
A useStep's cache key
June can't see what a useStep function does, so its cache key is exactly the cacheKey option, "" when you don't pass one. Change it when the function changes, so a snapshot from an earlier version isn't reused. Custom steps has examples.
A <Step>'s cache keys look like step:1b0e5c3f9a27d4. Treat them as opaque: compare them, don't parse them.
Next
- Custom steps with useStep: run any async work as a step.
- Snapshots and resume: how ids and cache keys decide what a resumed run reuses.