Running workflows
run() and start() both execute a workflow from code. Pick by whether your code has to act while the workflow runs.
run()when you only need the outcome. It renders the workflow and resolves with every step's result, the run's usage and the final trace.start()when your code has to act while the workflow runs: answer tool approvals, abort it, take a snapshot, or react to its status. It hands you a live handle to the run.
import { run } from "june";
import type { ReactNode } from "react";
declare const workflow: ReactNode;
declare const signal: AbortSignal;
const { steps, tree, usage } = await run(workflow, {
signal, // abort the run and its in-flight steps
timeout: 120_000, // reject with a TimeoutError after two minutes
concurrency: 8, // the most steps executing at once (the default)
onUpdate: (tree) => console.log(tree), // every committed trace tree
});
From the terminal, june run <file> does this for a file's default export; the CLI reference has its options.
run() or start()
Use run() when nothing needs to happen while the workflow runs. It can't answer tool approvals, so a tool that asks for one makes it reject with june: <tool> needs approval, which run() can't give. Use start() to approve or deny tool calls.
Use start() for approvals, for aborting from your own code, and for snapshots. It returns a handle:
| Member | Is |
|---|---|
done | a promise that resolves or rejects like run() |
status | running, awaitingApproval, settled, failed or aborted |
requests | the tool calls waiting for an answer |
approve(id) | approves a request |
deny(id, reason?) | denies a request; the model is told it was denied, with the reason |
abort(reason?) | aborts the run |
snapshot() | every finished step's result and cache key, as plain JSON |
tree() | a copy of the trace as last committed; once the run ends, the tree it ended with |
on(status, fn) | calls fn with the handle each time the run enters status; returns a function that unsubscribes |
on("awaitingApproval", fn) also calls fn whenever the pending requests change while the run is already waiting: when more arrive, or when one is answered or withdrawn and others remain. on doesn't replay a status the run reached before you subscribed, so subscribe right after start().
settled, failed and aborted are final. A run is failed when an error rejected it and aborted when its signal, its timeout or abort() did.
start() throws, and run() rejects, before rendering anything when concurrency or snapshot is invalid.
Options
run() and start() take the same options:
| Option | |
|---|---|
onUpdate | called with every committed trace tree: the whole tree each time, not a progress event |
signal | aborts the run when it aborts |
timeout | aborts the run with a TimeoutError after this many milliseconds |
concurrency | the most steps executing at once: 8 by default, or Infinity; see Concurrency |
snapshot | an earlier run's snapshot(), to resume from |
When a run ends
A run settles once React has committed everything it can render and no step is in flight. It resolves with:
steps: each finished step's result, keyed by id, including steps resumed from a snapshot and steps a<Catch>fallback has since replacedtree: the final traceusage: the tokens its steps reported, summed, orundefinedwhen no step reported any. A step a<Catch>aborted or retried drops what it had reported, and a model call that throws reports nothing, so a run with failures can use more than this
It rejects with the first error no <Catch> handles, exactly as it was thrown: a step's error, or an error thrown while rendering. Steps still running are aborted first.
A failed or aborted run has no result, but handle.tree() still says what it reached: the tree as it was when the run ended, with each step's latest state. The step that failed shows as failed with its error, the steps that finished before it as done, and the steps it cut off as running. It holds what React had committed, so a step whose node never rendered before the run ended isn't in it:
import { start } from "june";
import type { ReactNode } from "react";
declare const workflow: ReactNode;
declare function save(record: object): Promise<void>;
const handle = start(workflow);
try {
await handle.done;
} catch (error) {
await save({ error, tree: handle.tree(), snapshot: handle.snapshot() });
throw error;
}
A timer or promise that isn't a step doesn't keep a run open. Waiting belongs in a useStep.
Cancellation and timeouts
A run aborts when its signal aborts, when its timeout passes, or when you call handle.abort(). Then:
donerejects with the abort reason: the signal's reason, aTimeoutError(The operation timed out.) for a timeout, or the reason you passed toabort().- Every in-flight step's signal aborts. A
<Step>passes it to the AI SDK, which stops its model call; auseStepgets it asfn's first argument. Work that ignores the signal keeps going, but its result is thrown away. - Queued steps never start, and pending approval requests are withdrawn.
Steps can hold the rejection briefly to clean up: a <FineTune> cancels its training job, and the run waits for that cancel to be sent, for up to 30 seconds. See Training in workflows.
Every step's signal also aborts once the run ends for any reason, so nothing a step started outlives it.
june run aborts on Ctrl-C (and exits at once on a second Ctrl-C), and takes a timeout with --timeout <ms>.
Resources
A resource is a live value a run owns, such as a client with a connection to close. useResource(key, create) suspends until create returns { value, dispose }, then returns value:
import { useResource } from "june";
async function connect(signal: AbortSignal) {
return { query: async (sql: string) => sql, close: async () => {} };
}
function Report() {
const db = useResource("db", async (signal) => {
const client = await connect(signal);
return { value: client, dispose: () => client.close() };
});
return <div>{db.query.name}</div>;
}
- Components that use the same key share one value, and concurrent callers share one creation. The first
createfor a key defines it. - The run stays open while a resource is being created, and nothing is created until a component asks.
- Before
donesettles, whether the run succeeded, failed, aborted or timed out, June disposes every resource once, in reverse order of creation, waiting up to 30 seconds. A disposer that fails doesn't stop the rest. - If the run failed or aborted,
donerejects with that error and a cleanup error is dropped. If the run succeeded, a cleanup error rejectsdone(anAggregateErrorwhen several failed). - A resource whose creation finishes after the run began closing is disposed at once. Its
creategets a signal that aborts when the run closes. - Resources are never in a snapshot. A resumed run creates fresh ones for the work it runs.
Where a run lives
A run lives in one process, in memory. There's no server, queue or database behind it: if the process exits, the run is gone, along with any step that hadn't finished. June isn't a durable orchestrator.
Two things reach past one process. A snapshot lets a later run reuse the steps that finished. A Job represents work a backend owns, such as a training job, that a later process can find again.