Skip to content

Run log — structured execution timeline

A run log is an ordered, timestamped record of everything that happened during a graph run: which nodes ran, how long they took, transitions, and — crucially — the governance events (BudgetPolicy, StatePolicy, ApprovalGate). It answers the question production teams actually have: "what did my agent do, and why?"

It is the data foundation for replay, debugging, and any timeline/observability UI.

Enabling it

Attach a RunLogStore to the graph. The default InMemoryRunLogStore keeps events keyed by run id:

InMemoryRunLogStore log = new InMemoryRunLogStore();

AgentGraph graph = AgentGraph.builder()
        .addNode("triage", triage)
        .addNode("refund", refund)
        .addEdge("triage", "refund")
        .runLog(log)
        .build();

graph.invoke(AgentContext.of("refund my order"), "ticket-4521");

Then read the timeline back by run id:

for (AgentRunEvent e : graph.runLog("ticket-4521")) {
    System.out.println(e.describe());
}
#0 NODE_ENTER node=triage
#1 NODE_EXIT node=triage (412ms)
#2 TRANSITION node=triage — triage→refund
#3 NODE_ENTER node=refund
#4 NODE_EXIT node=refund (1203ms)
#5 GRAPH_COMPLETE — completed

What gets recorded

Event When
NODE_ENTER before a node executes
NODE_EXIT after a node executes (carries durationNanos)
NODE_ERROR a node failed (carries the message)
TRANSITION the graph moved between nodes (detail from→to)
APPROVAL_REQUIRED an ApprovalGate paused the run
BUDGET_EXCEEDED a BudgetPolicy denied a call
STATE_DENIED a StatePolicy blocked a write
GRAPH_COMPLETE the run finished (completed, failed, interrupted)

Each AgentRunEvent is a flat record (runId, sequence, epochMillis, type, node, detail, durationNanos) — trivially serializable to JSON, a log pipeline, or a UI.

Run ids

The runId is a general run identifier. graph.invoke(ctx, runId) uses it for both the run log and checkpointing — and now requires neither store:

  • with a RunLogStore → events are queryable via graph.runLog(runId)
  • with a CheckpointStore → the run is checkpointed/resumable
  • with both → correlated by the same id
  • with neither → behaves like invoke(ctx) but with your chosen id

Plain invoke(ctx) still records events under an internally generated id; use the runId overload when you want to query the log afterward.

Custom sinks

Implement RunLogStore to stream events somewhere durable:

class JsonlRunLogStore implements RunLogStore {
    private final Path file;
    @Override public void append(AgentRunEvent e) {
        Files.writeString(file, toJson(e) + "\n", CREATE, APPEND);
    }
    // events(runId) / runIds() read them back
}

Implementations must be thread-safe — one graph instance can run concurrently on multiple threads, each with its own run id.

Relationship to observability

The run log is intentionally a small, in-process primitive. It is the substrate for:

  • a replay/debugger view (re-read a finished run step by step)
  • a live trace UI (render the event stream as nodes light up)
  • OpenTelemetry export (map each event to a span)

Those build on top of this; the log itself stays dependency-free.