# Custom steps with useStep

`useStep` makes ordinary async work part of the workflow runtime. It's not another way to call a model.

## When to use it

Use `<Step>` when the unit of work is a model or agent call. Use `useStep` when it's ordinary async code, such as fetching data, running a command, reading a service or waiting on a job, and you want it to take part in the run like a step does: its cancellation, concurrency limit, retries and snapshots.

A `useStep` runs once per id, and the component that calls it suspends until it settles:

```tsx
import { openai } from "june/sdk/openai";
import { Agent, Step, useStep } from "june";

function Issue({ number }: { number: number }) {
  const issue = useStep(
    `issue-${number}`,
    (signal) =>
      fetch(`https://api.github.com/repos/johno/june/issues/${number}`, {
        signal,
      }).then((response) => response.json()),
    { cacheKey: "issue-v1" },
  );
  return <Step id={`triage-${number}`} prompt={`Triage: ${issue.title}`} />;
}

export default function Triage() {
  return (
    <Agent model={openai("gpt-5-mini")}>
      <Issue number={12} />
    </Agent>
  );
}
```

`useStep(id, fn, { cacheKey })` returns `fn`'s result. Until it settles, the component suspends, so nothing after the hook runs and nothing it renders exists yet. The fetch gets the run's `AbortSignal`, so aborting the run stops it, and it takes one of the run's concurrency slots while it's in flight, like a `<Step>`.

For work that runs somewhere else and outlives your process, such as a training job, a step that waits on it isn't enough on its own: see [Jobs](https://june.dev/docs/training/jobs.md).

## What it gets

- **A signal.** `fn`'s first argument is an `AbortSignal` that aborts when the run ends, is aborted or times out, or when a [`<Catch>`](https://june.dev/docs/workflows/recovery.md) fallback replaces the component. Pass it to `fetch`, the AI SDK, or anything else that can stop.
- **One execution per id.** Components that call `useStep` with the same id and cache key share one call to `fn`. See [Step identity](https://june.dev/docs/workflows/step-identity.md).
- **A slot.** It counts toward the run's [concurrency limit](https://june.dev/docs/workflows/parallelism.md#concurrency) while `fn` runs, and is `queued` while it waits for one.
- **A snapshot entry.** Its result is saved in [`snapshot()`](https://june.dev/docs/workflows/resume.md) and reused on resume when the id and cache key match.
- **Retries.** A failure goes to the closest `<Catch>`, whose `retry` runs `fn` again.

A `useStep` has no trace node of its own. The component that calls it renders whatever it likes, such as the `<Step>` above. It still counts toward the run: its result is in `steps`, and the usage it [reports](#reporting-model-calls) is in the run's `usage`.

## Results

Return a value that survives `JSON.stringify` if you save snapshots: a resumed step gets back whatever was saved. `undefined` doesn't survive (a snapshot entry without a `result` is rejected), so return `null` for a step that only has an effect.

## Cache keys

June can't see what `fn` does, so its cache key is the `cacheKey` you give it, `""` by default. Change it when `fn` changes, so a snapshot from an earlier run isn't reused. Rendering the same id with two different keys fails the run.

```text
useStep("issue-12", fetchIssue)                              // key ""
useStep("issue-12", fetchIssue, { cacheKey: "issue-v2" })    // a new version: snapshots of the old one don't match
```

## Waiting

A run ends once no step is in flight and React has nothing left to render, so a timer outside a step doesn't hold it open. Make a wait a `useStep`, as the [retry example](https://june.dev/docs/workflows/recovery.md#retry-with-a-delay) does. A waiting step holds a concurrency slot like any other.

## Reporting model calls

You only need this when `fn` itself calls a model. A `<Step>`'s calls go through June, which records their usage; a call `fn` makes with `generateText` doesn't, so June can't see it, and the run's `usage` would leave it out. Report it, and it counts like a `<Step>`'s: `fn`'s second argument has `reportCall({ label, model, usage, finishReason, durationMs })`. Reported calls are listed in the step's `calls`, and their usage counts toward the step's and the run's `usage`.

```tsx
import { openai } from "june/sdk/openai";
import { generateText } from "ai";
import { Step, useStep } from "june";

function Summary({ text }: { text: string }) {
  const summary = useStep(
    "summary",
    async (signal, step) => {
      const started = performance.now();
      const result = await generateText({
        model: openai("gpt-5-mini"),
        prompt: `Summarize: ${text}`,
        abortSignal: signal,
      });
      step.reportCall({
        label: "summary",
        model: result.finalStep.model,
        usage: result.totalUsage,
        finishReason: result.finishReason,
        durationMs: performance.now() - started,
      });
      return result.text;
    },
    { cacheKey: "summary-v1" },
  );
  return <Step id="title" prompt={`Title this summary: ${summary}`} />;
}
```

[`june/copy`'s `<Write>`](https://june.dev/docs/copy/voices.md#in-a-workflow) reports every call a voice makes this way.

To use the closest `<Agent>`'s settings in `fn`, call [`useAgent()`](https://june.dev/docs/workflows/agent-and-step.md#useagent) during render and pass the agent in.

## Next

- [Running workflows](https://june.dev/docs/workflows/running.md): start, cancel and time out a run.
- [Recovery](https://june.dev/docs/workflows/recovery.md): what happens when a step fails.
