Traces and usage
The trace is the tree React rendered, as plain JSON: a node for each agent, step and <Parallel>, with each step's live state in its props.
import { run } from "june";
import type { ReactNode } from "react";
declare const workflow: ReactNode;
const { tree, usage } = await run(workflow, {
onUpdate: (tree) => console.log(JSON.stringify(tree)),
});
A trace is structured execution state, not a log stream. onUpdate gives you the latest tree after each commit, and run() resolves with the settled tree as tree, next to usage, the run's total. From the terminal, june run --json prints steps, usage and tree together.
Reading the trace
Take the workflow from the Quickstart: summary, then risks and tests inside <Parallel>. Midway through, once summary is done, its trace looks like this (abridged):
{
"type": "ROOT",
"props": {},
"children": [
{
"type": "agent",
"props": { "model": "gpt-5-mini", "tools": [] },
"children": [
{
"type": "step",
"props": {
"id": "summary",
"status": "done",
"result": "Adds --json to june run.",
"durationMs": 63,
"usage": { "inputTokens": 10, "outputTokens": 5, "totalTokens": 15 }
},
"children": [
{
"type": "parallel",
"props": {},
"children": [
{
"type": "step",
"props": { "id": "risks", "status": "running" },
"children": []
},
{
"type": "step",
"props": { "id": "tests", "status": "running" },
"children": []
}
]
}
]
}
]
}
]
}
The nesting is the workflow's dependencies: risks and tests are inside summary because they were rendered from its result, and they didn't exist until it finished. The statuses say where the run is: summary is done with its result, timing and usage, and both children are running at once because they're inside <Parallel>. A step past the concurrency limit would show as queued, and a failed one as failed with its error.
summarize walks a trace and prints one line per step. It counts any node with a status as a step, so it covers <Write> and the training components too:
// trace-summary.ts
import type { StepState, TraceNode } from "june";
export function summarize(tree: TraceNode): string[] {
const lines: string[] = [];
const visit = (node: TraceNode) => {
const state = node.props as Partial<StepState> & { id?: string };
if (state.status) {
const parts = [`${state.status} ${state.id}`];
if (state.resumed) parts.push("(resumed)");
if (state.durationMs !== undefined) parts.push(`${state.durationMs}ms`);
if (state.usage?.totalTokens !== undefined) {
parts.push(`${state.usage.totalTokens} tokens`);
}
if (state.error)
parts.push(`${state.error.name}: ${state.error.message}`);
lines.push(parts.join(" "));
}
node.children.forEach(visit);
};
visit(tree);
return lines;
}
On the changes → notes workflow, resumed from a snapshot that already had changes, it prints:
done changes (resumed)
done notes 812ms 412 tokens
- Copy what you keep. The tree
onUpdatereceives is the live tree, changed in place by later commits.run()'s resolvedtreeis a copy. - Expect batching. React batches commits, so
onUpdateisn't called once per change: two steps can finish in one update, and streamed text arrives in chunks. - Read a failed run's tree from its handle. A failure that no
<Catch>handles unmounts the tree, so the last update before the run rejects is an empty tree, andonUpdateisn't called after the run aborts.handle.tree()keeps the tree the run ended with. - Parse JSON, not the CLI's text.
june run's progress output may change in a minor version (Compatibility); read--jsoninstead.
Nodes
Every node is { type, props, children }. The root's type is ROOT.
type | From | Props |
|---|---|---|
agent | <Agent> | id, model (the model id), tools (their names), and capabilities when the agent declares some |
step | <Step> | id, prompt, and the step fields |
parallel | <Parallel> | id |
write | june/copy's <Write> | id, voice, request, and the step fields |
fine-tune | <FineTune> | id, model, trainer, and the step fields |
evaluate | <Evaluate> | id, suite, and the step fields |
- A
useStephas no node. The component that calls it renders whatever it likes. - A string your components render becomes a
TEXTnode with avalue, and any other host element they render becomes a node with its props. - Children are in render order. A step renders its children inside its own node once it finishes, so the tree's nesting is the workflow's dependencies.
- Props that are
undefined, such as an agent without anid, are left out of the JSON. - A step rendered in two places, as in fan-in, has two nodes with the same state.
- When a
<Catch>falls back, its children's nodes are replaced by the fallback's.
Step fields
The fields every step node has, typed as StepState in june. All of them are plain JSON.
| Field | |
|---|---|
status | queued, running, done or failed |
toolCalls | each tool call's toolCallId, toolName, input, and output once it has one |
result | the step's result, once done |
error | { name, message }, once failed |
startedAt, endedAt | ISO 8601 timestamps from when the step first started running to when it settled, not time spent queued before it started |
durationMs | the time between them, from a monotonic clock, including any time spent waiting on an approval |
partialText | text streamed so far, with stream |
model | { provider, modelId } of the last model call |
usage | tokens, summed across every model call the step made |
finishReason | why the last model call stopped, such as stop or length |
calls | model calls a step reported itself |
resumed | true when the result came from a snapshot |
job, progress | a training step's job and progress; see Training in workflows |
status and toolCalls are always there. The rest appear as they apply:
- A
queuedstep has onlystatusandtoolCalls.startedAtis set when it starts running. model,usageandfinishReasonare set on a<Step>each time its agent loop returns, whether it finished or paused for an approval, and on any step each time it reports a call.- A resumed step is
donewith itsresultandresumed: true, and has no timing,modelorusage.
A trace holds your data: prompts, results, and tool inputs and outputs. Treat a saved trace as being as sensitive as the prompts in it. Security and privacy covers the rest.
Usage
usage has the AI SDK's token counts, without the provider's raw usage: inputTokens, outputTokens and totalTokens, plus inputTokenDetails (noCacheTokens, cacheReadTokens, cacheWriteTokens) and outputTokenDetails (textTokens, reasoningTokens). A count the provider doesn't report is undefined.
- A
<Step>'susagecovers its whole tool loop, including the calls an approval splits it into. - A
useStep's covers the calls it reported, and nothing else: June can't see its model calls. - The run's
usageis the sum of its steps', including steps that have no node, such as auseStep's. Resumed steps add nothing, since they didn't run, and neither do steps a<Catch>aborted or failed attempts aretrycleared. It'sundefinedwhen no step reported usage.
Next
- React as the workflow: back to the overview.
- Running workflows:
onUpdateand the other run options.