Skip to main content
Workflow tracing records which steps ran via StepTransition. Trace capture goes deeper โ€” it records what happened inside each step: every tool call, thinking block, token count, and cost. The trace is written to a JSONL file during execution, and the file path flows through the workflow journal so analysis tools can find it.

When you need trace capture

  • Markov analysis โ€” fingerprint an agentโ€™s behavioral patterns across runs
  • Cost attribution โ€” break down per-step token usage and cost
  • Debugging โ€” replay exactly what the agent saw, thought, and did
  • Regression detection โ€” compare traces across code changes

Setup

Trace capture requires two things: an agent model that writes trace files, and a workflow client that propagates the path.

1. Configure traceDir on ClaudeAgentModel

The ClaudeAgentModel from agent-client writes a JSONL trace file per call() when traceDir is set:
ClaudeAgentModel model = ClaudeAgentModel.builder()
        .traceDir(Path.of("traces"))
        .build();
Each call produces a file like traces/agent-run-20260528-143000-a1b2c3d4.jsonl containing every message in the session.

2. Bridge to a trace-aware AgentClient

The workflow-flows AgentClient is a @FunctionalInterface that returns text. To carry trace metadata, override executeForResult():
var coreClient = AgentClient.create(model);

io.github.markpollack.workflow.flows.steps.AgentClient workflowClient =
    new io.github.markpollack.workflow.flows.steps.AgentClient() {

        @Override
        public String execute(String prompt, AgentContext ctx) {
            return executeForResult(prompt, ctx).text();
        }

        @Override
        public ExecutionResult executeForResult(String prompt, AgentContext ctx) {
            AgentClientResponse response = coreClient.run(prompt);
            String tracePath = (String) response.getMetadata().get("tracePath");
            return new ExecutionResult(response.getResult(), tracePath);
        }
    };
Plain lambdas still work โ€” executeForResult() defaults to calling execute() with a null trace path.

3. Use AgentClientStep in a workflow

AgentClientStep fixStep = AgentClientStep.of(workflowClient, "Fix: {input}");
AgentClientStep verifyStep = AgentClientStep.of(workflowClient, "Verify the fix: {input}");

TraceRecorder recorder = TraceRecorder.inMemory();
WorkflowExecutor executor = new WorkflowExecutor(recorder);

String result = Workflow.<String, String>define("remediate")
        .withExecutor(executor)
        .step(fixStep)
        .then(verifyStep)
        .run("failing test in AuthService");
Each AgentClientStep gets its own trace file. The path flows through to StepTransition:
List<StepTransition> trace = recorder.getTrace(runId);
for (StepTransition t : trace) {
    if (t.tracePath() != null) {
        System.out.println(t.toStep() + " โ†’ " + t.tracePath());
    }
}
// AgentClientStep โ†’ /abs/path/traces/agent-run-20260528-143000-a1b2c3d4.jsonl
// AgentClientStep โ†’ /abs/path/traces/agent-run-20260528-143500-e5f6g7h8.jsonl

How it works

The trace path flows through four layers:
AgentClient.executeForResult()        โ†’ ExecutionResult(text, tracePath)
  AgentClientStep.updateContext()     โ†’ sets AgentContext.TRACE_PATH
    WorkflowExecutor.recordTransition() โ†’ reads TRACE_PATH, clears it, records StepTransition
      TraceRecorder                   โ†’ stores/persists the transition
The executor clears TRACE_PATH from context after each step so deterministic steps donโ€™t inherit a stale path.

Journal integration

When using workflow-journal, trace paths appear in WorkflowStepEvent and are included in the journalโ€™s JSON output:
Journal.configure(new JsonFileStorage(journalDir));
WorkflowJournal.registerEventType();

try (Run run = Journal.run("remediate-experiment").start()) {
    WorkflowExecutor executor = new WorkflowExecutor(
            new LocalStepRunner(),
            WorkflowJournal.forRun(run));
    // ... run workflow
}
The journal event includes tracePath when present:
{
  "type": "workflow_step",
  "stepName": "AgentClientStep",
  "nodeType": "AGENT",
  "stepDurationMs": 12000,
  "tokensUsed": 3200,
  "costUsd": 0.048,
  "tracePath": "/abs/path/traces/agent-run-20260528-143000-a1b2c3d4.jsonl"
}

JDBC persistence

JdbcTraceRecorder stores trace paths in the trace_path column of step_transitions:
JdbcTraceRecorder recorder = new JdbcTraceRecorder(dataSource);
List<StepTransition> trace = recorder.getTrace("run-1");

// Find all trace files for a run
List<String> traceFiles = trace.stream()
        .map(StepTransition::tracePath)
        .filter(Objects::nonNull)
        .toList();

ClaudeStep vs AgentClientStep

ClaudeStepAgentClientStep
ExecutionCLI subprocess (claude -p)In-process via ClaudeAgentModel
Trace captureNot available (text-only output)Full JSONL trace files
Token/cost dataDiscarded at process boundaryAvailable in providerFields
Use caseQuick scripts, prototypingExperiments, production workflows
For any workflow where you need to analyze what the agent did โ€” use AgentClientStep.

Maven coordinates

<!-- workflow-flows (always needed) -->
<dependency>
    <groupId>io.github.markpollack</groupId>
    <artifactId>workflow-flows</artifactId>
    <version>0.9.0</version>
</dependency>

<!-- agent-client + Claude model (for trace-aware client) -->
<dependency>
    <groupId>io.github.markpollack</groupId>
    <artifactId>agent-client-core</artifactId>
    <version>0.21.0</version>
</dependency>
<dependency>
    <groupId>io.github.markpollack</groupId>
    <artifactId>agent-claude</artifactId>
    <version>0.21.0</version>
</dependency>
Or use the AgentWorks BOM (1.1.0+) for managed versions.

Durability

JdbcTraceRecorder, CheckpointingStepRunner

API Reference

StepTransition, TraceRecorder, WorkflowExecutor