# june

**Declarative AI workflows.**

June is React for AI workflows: a runtime where the component tree is the program. Nesting creates dependencies, JavaScript controls what renders, independent work can run in parallel, and the rendered tree is the execution trace.

The AI SDK still owns models, tools, structured output and the agent loop. June adds the workflow around them.

## Why June

One model call, or one agent looping over its tools, is already solved: that's the AI SDK. The hard part starts when work depends on other work, branches on a result, fans out over a list, fails, pauses for a person, resumes after a restart, or needs to be observed while it runs.

June is that layer, without another graph DSL to learn:

- **Dependencies** are nesting: a step's children run once it finishes, with its result.
- **Branches** are JavaScript: only what renders runs.
- **Fan-out** is `.map()` inside `<Parallel>`, bounded by the run's concurrency limit.
- **Failures** go to a `<Catch>` boundary, which can fall back or retry.
- **People** answer the AI SDK's tool approvals while the rest of the run keeps going.
- **Restarts** reuse the steps that finished, from a snapshot.
- **Observation** is the trace: the same tree React renders, as JSON.

React isn't the UI for the workflow. **React is the workflow runtime.**

## Install

```sh
bun add june react ai @ai-sdk/openai
```

june runs on [Bun](https://bun.sh) 1.2 or later, and the `june` CLI requires Bun. `react` (19.3+) and `ai` (7.0.137+) are peer dependencies, and models come from any AI SDK provider. The package is ESM only.

`june/sdk/openai`, `june/sdk/anthropic`, `june/sdk/google` and `june/sdk/xai` give you each provider's models, so `import { openai } from "june/sdk/openai"` is all a workflow needs. The provider packages are optional: install the ones you use, like `@ai-sdk/openai` above. Run one you haven't installed with Bun and June's error names the package to add; a bundler reports the missing package when it builds. For anything else a provider offers, such as `openai.chat()` or provider options, import its `@ai-sdk/*` package directly. See [Package surfaces](https://june.dev/docs/reference/packages.md#junesdk).

## Quickstart

```tsx
// review.tsx
import { openai } from "june/sdk/openai";
import { Agent, Parallel, Step } from "june";

const diff = await Bun.$`git diff`.text();

export default function Review() {
  return (
    <Agent
      model={openai("gpt-5-mini")}
      instructions="You are a senior code reviewer."
    >
      <Step id="summary" prompt={`Summarize this diff:\n${diff}`}>
        {(summary) => (
          <Parallel>
            <Step
              id="risks"
              prompt={`List the risks in this change:\n${summary}`}
            />
            <Step
              id="tests"
              prompt={`Suggest tests for this change:\n${summary}`}
            />
          </Parallel>
        )}
      </Step>
    </Agent>
  );
}
```

```sh
june doctor review.tsx   # checks Bun and OPENAI_API_KEY
june run review.tsx      # prints progress, then each step's result
june run review.tsx --json > review.json
```

`summary` runs first. Once it finishes, `risks` and `tests` start together with its result.

### Workflow input

A workflow is a component, so its input is its props. `june run` passes a JSON object as props with `--input`, or reads one from a file with `--input-file` (relative to the current directory):

```tsx
// triage.tsx
export default function Triage({
  repo,
  issues,
}: {
  repo: string;
  issues: number[];
}) {
  return (
    <Agent model={openai("gpt-5-mini")}>
      {issues.map((n) => (
        <Step key={n} id={`triage-${n}`} prompt={`Triage ${repo}#${n}`} />
      ))}
    </Agent>
  );
}
```

```sh
june run triage.tsx --input '{"repo":"johno/june","issues":[12,15]}'
june run triage.tsx --input-file ./triage.json
```

The input must be a JSON object, and June never prints it. From code, render the component with props: `run(<Triage repo="johno/june" issues={[12, 15]} />)`.

## Your program is the workflow

There's no separate graph to define: React and JavaScript already express the control flow. Nesting is a dependency: a step's children render, and run, once it finishes, with its result. The Quickstart's `summary → risks, tests` is that rule and nothing more.

A conditional is a branch, and only the branch that renders runs:

```tsx
<Step id="score" prompt="Score this PR from 1 to 10">
  {(score) =>
    Number(score) < 5 ? (
      <Step id="feedback" prompt="Explain what to improve" />
    ) : (
      <Step id="approve" prompt="Write an approval summary" />
    )
  }
</Step>
```

A `.map()` is dynamic fan-out. Give each step a stable id derived from its data; the run's concurrency limit keeps a long list from starting every model call at once:

```tsx
<Parallel>
  {files.map((file) => (
    <Step key={file} id={`review-${file}`} prompt={`Review ${file}`} />
  ))}
</Parallel>
```

A component is a reusable subworkflow. It holds its own steps, takes props, and composes like any other component:

```tsx
function Research({ topic }: { topic: string }) {
  return (
    <Step id={`search-${topic}`} prompt={`Find sources about ${topic}`}>
      {(sources) => (
        <Step
          id={`brief-${topic}`}
          prompt={`Write a brief from:\n${sources}`}
        />
      )}
    </Step>
  );
}

<Agent model={openai("gpt-5-mini")}>
  <Parallel>
    <Research topic="Suspense" />
    <Research topic="reconcilers" />
  </Parallel>
</Agent>;
```

Any async work can be a step with `useStep`. The component suspends until it resolves, and the result is reused like any other step's:

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

## The API

| Export                          |                                                                                                                                      |
| ------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------ |
| `<Agent>`                       | provides the AI SDK's `ToolLoopAgent` settings to the steps inside it. [Agent and Step](https://june.dev/docs/workflows/agent-and-step.md)          |
| `<Step>`                        | runs the closest agent on a prompt; a function child receives the result. [Agent and Step](https://june.dev/docs/workflows/agent-and-step.md#step) |
| `<Parallel>`                    | marks its children as independent. [Parallelism and concurrency](https://june.dev/docs/workflows/parallelism.md)                                    |
| `<Tool name value>`             | gives the steps in the closest agent an AI SDK tool. [Capabilities](https://june.dev/docs/workflows/capabilities.md)                               |
| `<Catch>`                       | an error boundary for steps, with a fallback and `retry`. [Recovery](https://june.dev/docs/workflows/recovery.md)                                   |
| `useStep(id, fn, { cacheKey })` | runs any async function as a step. [Custom steps](https://june.dev/docs/workflows/use-step.md)                                                      |
| `useAgent()`                    | the closest `<Agent>` as an AI SDK `ToolLoopAgent`. [Agent and Step](https://june.dev/docs/workflows/agent-and-step.md#useagent)                    |
| `run(element, options)`         | renders a workflow and resolves with its results. [Running workflows](https://june.dev/docs/workflows/running.md)                                   |
| `start(element, options)`       | the same, with a handle for approvals, aborting and snapshots. [Running workflows](https://june.dev/docs/workflows/running.md#run-or-start)        |

## Running workflows from code

`run()` resolves with every step's result, the run's token usage, and the final trace tree once nothing is in flight. It rejects with the first error no `<Catch>` handles.

```ts
const { steps, usage, tree } = await run(<Review />, {
  signal, // abort in-flight model calls
  timeout: 120_000, // rejects with a TimeoutError
  concurrency: 8, // the most steps executing at once (the default)
  onUpdate: (tree) => render(tree), // every commit: step status, tool calls, partial text
});
```

Tools with the AI SDK's `needsApproval` pause their step until a person answers, so use `start()`, which returns a handle to approve or deny them. `june run` asks on stdin. See [Approvals](https://june.dev/docs/workflows/approvals.md).

```tsx
const handle = start(<Deploy />);

handle.on("awaitingApproval", ({ requests }) => {
  for (const request of requests) {
    if (confirm(`Run ${request.toolName}?`)) handle.approve(request.id);
    else handle.deny(request.id, "Not now");
  }
});

await handle.done;
```

`snapshot()` returns every finished step's result and cache key as JSON. Pass it back to skip those steps after a crash or restart. Resume is completed-step reuse, not durable execution: a step that hadn't finished runs again from the start. See [Snapshots and resume](https://june.dev/docs/workflows/resume.md).

```ts
await save(handle.snapshot());
// later
await run(<Deploy />, { snapshot: await load() });
```

## Learn the runtime

The Workflows docs go from the first step to the exact semantics:

1. [Agent and Step](https://june.dev/docs/workflows/agent-and-step.md): models, tools, prompts, structured output and streaming.
2. [Capabilities](https://june.dev/docs/workflows/capabilities.md): declaring tools in JSX, scope, and reusable bundles.
3. [Composition and data flow](https://june.dev/docs/workflows/composition.md): results, input, branches and reusable components.
4. [Parallelism and concurrency](https://june.dev/docs/workflows/parallelism.md): `<Parallel>`, fan-out, fan-in and the concurrency limit.
5. [Step identity](https://june.dev/docs/workflows/step-identity.md): ids, cache keys, and when a step runs.
6. [Custom steps](https://june.dev/docs/workflows/use-step.md): `useStep` for any async work, and reporting its model calls.
7. [Running workflows](https://june.dev/docs/workflows/running.md): `run()`, `start()`, cancellation and timeouts.
8. [Recovery](https://june.dev/docs/workflows/recovery.md): `<Catch>`, failure and retry.
9. [Approvals](https://june.dev/docs/workflows/approvals.md): pausing for a person.
10. [Snapshots and resume](https://june.dev/docs/workflows/resume.md): reusing finished steps after a restart.
11. [Traces and usage](https://june.dev/docs/workflows/traces.md): the trace tree, its fields, and token usage.

Runnable versions of their examples, with tests, are in [src/examples/workflows](https://github.com/johno/june/tree/main/packages/june/src/examples/workflows).

## Beyond workflows

The same package has optional subpaths for problems you may have later. Start with the runtime; add these when you need them.

- **[Chat](https://june.dev/docs/chat.md)** (`june/chat`): a workflow runs once and finishes; a chat persists. It turns a model or one of your workflows into a conversation, with feedback on its replies. `june chat chat.ts` opens one in your terminal, and `june ask` scripts it.
- **[Tools](https://june.dev/docs/workflows/tools.md)** (`june/tools`): tools June ships, as plain AI SDK tools. `<Bash />` lets an agent run shell commands, after you approve each one.
- **[Copy and voice](https://june.dev/docs/copy.md)** (`june/copy`): builds a voice from a person's real writing and learns from their edits.
- **[Evals and training](https://june.dev/docs/training.md)** (`june/train`, `june/trainers`): turns feedback into evidence. Build datasets, run evals, compare candidates, and fine-tune through a trainer only when evaluation says you need to.

A chat is a file that default exports one:

```ts
// chat.ts
import { openai } from "june/sdk/openai";
import { createChat, fileStore } from "june/chat";

export default createChat({
  model: openai("gpt-5-mini"),
  instructions: "You are helping build June.",
  store: fileStore(".june/chat"),
});
```

```sh
june chat chat.ts                         # an interactive chat in your terminal
june ask chat.ts "Explain this package"   # one reply on stdout, for scripts
```

See also: the chat guides on [sessions and stores](https://june.dev/docs/chat/sessions.md), [turns](https://june.dev/docs/chat/turns.md), [responders](https://june.dev/docs/chat/responders.md), [context](https://june.dev/docs/chat/context.md) and [feedback](https://june.dev/docs/chat/feedback.md); [learning from chat](https://june.dev/docs/chat/learning.md), which turns sessions into evidence; and [local](https://june.dev/docs/training/local.md) and [managed](https://june.dev/docs/training/managed.md) training with `trl()` and `vertex()`.

## What June doesn't do

June is a small runtime on purpose. Reach for something else when you need:

- **Durable execution.** Resume reuses finished steps but doesn't checkpoint in-flight work, so a crash mid-step repeats that step. If work must survive the process exactly once, use a durable workflow engine, or a [Job](https://june.dev/docs/training/jobs.md) for work a backend owns.
- **Storage, queues, schedules and hosting.** A run lives in one process, and a snapshot is JSON you save yourself. Your app decides where runs happen and when.
- **A visual builder.** Workflows are components, not a graph you draw.
- **Memory, retrieval and MCP abstractions.** Use the AI SDK's tools, or your own.
- **Rate limiting.** The concurrency limit counts steps, not tokens or requests; use the agent's `maxRetries` or your provider's limits.

## Testing

Use `scriptedModel` from `june/test`, so tests don't need API keys or a network. It answers each call with whatever your function returns: text, an object as JSON for a step with an `output`, or tool calls from `callTools()`. The agent runs the tools and calls the model again with their results.

```tsx
import { Agent, Step, Tool, run } from "june";
import { callTools, scriptedModel } from "june/test";

const model = scriptedModel((call) =>
  call.prompt.at(-1)?.role === "user"
    ? callTools({ toolName: "readFile", input: { path: "a.ts" } })
    : "Looks good",
);

const { steps } = await run(
  <Agent model={model}>
    <Tool name="readFile" value={readFile} />
    <Step id="review" prompt="Review a.ts" />
  </Agent>,
);

expect(steps.review).toBe("Looks good");
```

A scripted model is the AI SDK's `MockLanguageModelV4` from `ai/test`, so `model.doGenerateCalls` records every call June made, and `doStreamCalls` those of a step with `stream`. A reply that throws fails the call. For anything it can't script, such as reasoning or provider metadata, build a `MockLanguageModelV4` yourself.
