Skip to content

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:

  1. An id identifies one logical piece of work within a run.
  2. Derive a repeated step's id from stable input data, such as a file path or a record id.
  3. 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. A useStep's cache key is only the cacheKey you 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-12 and summary-12 don'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 haveJune
different idsruns both, even if they're otherwise identical
the same id and the same cache keyruns one, and every place it's rendered gets the same result
the same id and different cache keysfails 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> and june/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.responses and gpt-5-mini
  • the agent's other settings that are plain data: instructions, temperature, providerOptions, its id, and so on
  • each tool's name, description, input schema and title
  • the kind of output and its schema

It doesn't cover:

  • functions, which June can't compare: a tool's execute, stopWhen, prepareStep, callbacks such as onStepFinish
  • settings that change how a call is made, not what it asks: maxRetries, timeout, telemetry and experimental_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 stream prop and its children

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