Skip to content

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 onUpdate receives is the live tree, changed in place by later commits. run()'s resolved tree is a copy.
  • Expect batching. React batches commits, so onUpdate isn'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, and onUpdate isn'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 --json instead.

Nodes

Every node is { type, props, children }. The root's type is ROOT.

typeFromProps
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
writejune/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 useStep has no node. The component that calls it renders whatever it likes.
  • A string your components render becomes a TEXT node with a value, 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 an id, 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
statusqueued, running, done or failed
toolCallseach tool call's toolCallId, toolName, input, and output once it has one
resultthe step's result, once done
error{ name, message }, once failed
startedAt, endedAtISO 8601 timestamps from when the step first started running to when it settled, not time spent queued before it started
durationMsthe time between them, from a monotonic clock, including any time spent waiting on an approval
partialTexttext streamed so far, with stream
model{ provider, modelId } of the last model call
usagetokens, summed across every model call the step made
finishReasonwhy the last model call stopped, such as stop or length
callsmodel calls a step reported itself
resumedtrue when the result came from a snapshot
job, progressa training step's job and progress; see Training in workflows

status and toolCalls are always there. The rest appear as they apply:

  • A queued step has only status and toolCalls. startedAt is set when it starts running.
  • model, usage and finishReason are 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 done with its result and resumed: true, and has no timing, model or usage.

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>'s usage covers 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 usage is the sum of its steps', including steps that have no node, such as a useStep's. Resumed steps add nothing, since they didn't run, and neither do steps a <Catch> aborted or failed attempts a retry cleared. It's undefined when no step reported usage.

Next