Essay

How to Estimate Agent Work From Its Logs

By Greg Ceccarelli ·

Want to skip ahead? Jump to Getting started

Claude Code workflows log when every subagent starts and stops. By timing each stage from those transcripts, an agent can estimate your next piece of work in hours instead of imaginary engineering weeks.

Not long ago, I asked Claude how long a new feature plan would take. It answered “2 to 3 engineering weeks.” Anyone who builds with coding agents knows that calibration is broken. Even our largest specs take agents 9 to 22 hours of working time. Once I pointed Claude at the logs from our own past runs, it started answering in hours, broken down by stage:

Stage of workEstimate
Agents build the change and integrate their work7 hours
Agents verify the build and fix what they find5 to 6 hours
Release to production45 minutes

I call each backlog item a phase. To produce an estimate like this, Claude compares the new phase’s spec against completed phases of similar scope, then adds the median duration of our CI release pipeline. Over the last few weeks, its estimates have landed within about 10% of actual working time.

I run every phase as a Claude Code workflow, a script that spawns subagents and coordinates work between them. To time and estimate work this way, you only need three things:

  • Claude Code with workflows enabled.
  • A timing script. I wrote phase-timings.py (220 lines of standard-library Python) for this post.
  • A consistent pipeline across runs. Estimation works by comparing new specs against historical runs, so your workflows need to run the same stages from one task to the next.

All of the data in this post comes from two products I built almost entirely with Claude Code workflows:

ProjectWhat it doesWorkflow runsAgents spawned
RunStoryHosted service that runs the software in a commit and returns a verification verdict to the agent that wrote it4623,409
TortiemacOS app that organizes all of my projects and coding-agent sessions in a single window6004,300

What a Claude Code workflow is

In Claude Code, a dynamic workflow is a JavaScript file executed by the built-in Workflow tool. Rather than relying on a single long chat session to remember every step, the script spawns isolated subagents, passes prompts and structured outputs between them, and uses ordinary JavaScript control flow for loops, branches, and retries. You can also pass a JSON Schema to any agent call so its response is validated before the next step runs.

You don’t write workflow scripts by hand; Claude generates them from your prompt. Most of my workflows follow this basic build-verify-commit shape:

export const meta = {
  name: 'small-fix',
  description: 'One builder, one verifier, one commit',
  phases: [{ title: 'Build' }, { title: 'Verify' }, { title: 'Commit' }],
}
const VERDICT = {
  type: 'object',
  properties: { verdict: { type: 'string', enum: ['pass', 'needs_work'] }, problems: { type: 'array', items: { type: 'string' } } },
  required: ['verdict', 'problems'],
}
phase('Build')
const build = await agent(`Fix ${args.issue}. Edit only ${args.files}. Run its tests.`, { label: 'build' })
phase('Verify')
const check = await agent(`You did not write this change. Run it and try to break it. The builder said ${build}`, { label: 'verify', schema: VERDICT })
if (check.verdict !== 'pass') return { state: 'needs work', problems: check.problems }
phase('Commit')
return await agent('Make one commit with a conventional subject.', { label: 'commit' })

Launch arguments arrive in args, and the script body runs in an async context where top-level await and return work out of the box. Each phase() call groups the agents that follow under a single heading. (Claude Code calls that grouping a “phase”; since my backlog already uses “phase” for a feature or bug fix, I’ll call the steps inside a workflow stages to keep the two distinct.)

While the workflow runs, the main Claude Code session orchestrates the script and automatically writes a run journal alongside an individual transcript for every subagent:

A workflow run and the records it leavesMain sessionlaunches it with argsWorkflow scriptBuildermakes the changeVerifierreturns JSONpassCommitterone commitneeds_workReturn the problemswritten while it runsJournal and transcriptsone journal for the run and one transcript for each agent
The script branches on the verifier’s JSON verdict, while Claude Code logs the run journal and per-agent transcripts automatically.

Turning workflows on

If workflows aren’t enabled by default on your plan, toggle Dynamic workflows in /config or set enableWorkflows to true in ~/.claude/settings.json. To make workflows the default for every non-trivial task, enable Ultracode: either per session with /effort ultracode (or claude --effort ultracode), globally via "ultracode": true in your settings, or for a single prompt by including the word ultracode.

Because every subagent runs in its own context window, workflows consume more tokens than a single chat session. A small 6-agent bug fix (Phase 256) used 1.49M tokens, so expect to hit usage limits more frequently on multi-agent runs.

What Claude Code logs on every run

Without any custom instrumentation, Claude Code (tested on versions 2.1.226 through 2.1.301) writes four sets of files under ~/.claude/projects/<project>/<session>/:

RecordContentsPath
Workflow scriptThe JavaScript file executed for the runworkflows/scripts/<name>-<runId>.js (if generated in chat; scripts launched by path stay in place)
Run summaryStatus, start time, duration, token count, and script sourceworkflows/<runId>.json
Run journalOne started line and one result line per agent, with its label and stagesubagents/workflows/<runId>/journal.jsonl
Agent transcriptsEvery message and tool call for each subagent, with ISO timestampssubagents/workflows/<runId>/agent-<id>.jsonl

Here is an entry from journal.jsonl (with the prompt-hash key truncated):

{"type":"started","key":"v2:15a7371483e0...","agentId":"aa3cd8997f594219b","label":"build","phase":"Build"}

Notice that journal.jsonl maps each agentId to its label and stage, but doesn’t include timestamps. The timestamps live inside each agent’s agent-<id>.jsonl transcript. For the builder above, the transcript runs from 21:06:48Z to 21:32:44Z, or 26 minutes of work.

How I run a phase

Logs only become useful for estimation when your workflows follow a repeatable structure. In both RunStory and Tortie, every phase runs in an isolated git worktree as a single build workflow that ends in a single commit.

The rules for every agent live in AGENTS.md, which CLAUDE.md pulls into every session via @AGENTS.md. Each backlog item is assigned a verification depth from Tier 1 (lightest) to Tier 3 (deepest). A small Tier 2 fix might use 1 builder and 1 verifier; a large Tier 3 feature uses 7 or 8 parallel builders (split into 1 or 2 waves) and 3 independent verifiers. If any verifier finds a serious issue, the script runs up to 3 fix-and-reverify rounds before committing:

The order of a large phaseSpecthe written planBasemakes the worktreeBuilderson separate filesIntegratormerges their work3 verifiersrun the changepassCommitterone commitRelease trainmake releasea problem remainsFix roundat most 3
If any verifier reports a problem, a fixer patches it and the verifiers re-check, up to 3 rounds.

Strict boundaries keep stages from stepping on each other:

StageWhat it ownsWhat it never does
Specspec.md and the per-builder file assignmentsWrites product code
BaseCreating the git worktreeEdits files
Build (1–8 in parallel)Only the disjoint files assigned to it in the specTouches another builder’s files
IntegrateMerging all builders’ changes cleanlyCommits
Verify (1–3 in parallel)Independent runtime proofTrusts a builder’s self-report
FixResolving issues flagged by verifiersCommits
CommitA single commit after make gates passes all 37 checksPushes or deploys

The Spec agent returns its builder breakdown as JSON, and the workflow launches all builders in a wave concurrently with parallel(). You can save reusable scripts in .claude/workflows/; I keep a copy of my build script alongside each phase’s launch arguments in an ignored scratch/phase<N>/build/ directory.

Resuming interrupted runs

Multi-hour workflows inevitably hit rate limits or manual pauses. When you resume a stopped workflow in the same session (passing the original script, identical args, and resumeFromRunId), Claude Code replays the cached results of every completed subagent immediately. Any subagent that was mid-flight when the run stopped starts over from scratch.

To make resuming and timing reliable, I keep a small run.json file in each phase’s folder with its run ID, transcript directory, and stop history:

{
  "workflow": "scratch/phase252/build/workflow.js",
  "args_file": "scratch/phase252/build/launch-args.json",
  "run_id": "wf_4b9ab0eb-3cd",
  "transcript_dir": "~/.claude/projects/<project>/<session>/subagents/workflows/wf_4b9ab0eb-3cd",
  "launched_at": "2026-10-17T03:40Z",
  "resume": "Workflow({scriptPath: workflow, resumeFromRunId: run_id, args: <the exact content of args_file>}). Stop the live run first with TaskStop.",
  "history": [
    { "at": "2026-10-17T17:41:38Z", "event": "TaskStop during reverify2 to ship an earlier release" }
  ]
}

The release train

The final piece of every estimate is the release itself. I ship finished phases on a release train via a single command, make release, which runs 20 numbered checkpoints and logs a JSON timestamp as each one completes.

All durations in the tables below use H:MM format (so 4:52 is 4 hours 52 minutes). Here is a typical release breakdown:

CheckpointsWhat happensDuration
01 to 03Build the release branch in a clean checkout0:01
04Run full CI on the exact commit0:25
05 to 07Build sandbox images, deploy a preview environment, and run an end-to-end smoke check0:15
08 to 20Acquire the release lock, deploy to production, push main, and clean up0:02
Total0:43

Across nine recent releases, trains took between 29 and 45 minutes (median 41 minutes), with CI accounting for the bulk of the time. If your release process is scripted, having each step log a completion timestamp lets an agent include deployment time in its estimates automatically.

How I time each stage

To turn raw subagent transcripts into stage durations, phase-timings.py applies four rules:

  1. Agent span: An agent starts at the first timestamp in its transcript and ends at the last.
  2. Parallel union: A stage’s wall time is the time during which at least one of its agents was running, so parallel builders or verifiers aren’t double-counted.
  3. Idle gaps: My prompts instruct agents to cap blocking tool calls at 10 minutes, meaning an active agent writes to its transcript at least every ~10 minutes. Any silence longer than 20 minutes is treated as an inactive gap rather than working time.
  4. Rate limits vs. pauses: If an agent logs a rate_limit error within the 10 minutes before a gap, the gap is classified as a usage limit; otherwise it’s marked as a manual stop or stall.

Because the script only inspects timestamps and metadata without printing prompt or message text, its output is safe to paste or feed straight back to an agent. (One caveat: if an agent spends hours actively polling on an external job, the script will see regular transcript lines and count that polling as work. I exclude pure wait stages, such as a Base stage waiting for an earlier phase to finish, when comparing working times.)

Here is the script’s output for a small Tier 2 bug fix (Phase 256):

$ python3 phase-timings.py ~/.claude/projects/<project>/<session>/subagents/workflows/wf_e3c484d2-242
runs: wf_e3c484d2-242
stage                  agents lost  start UTC    end UTC        wall working agent time
Build                       1    0  10-17 21:06  10-17 21:32    0:26    0:26       0:26
Verify                      2    0  10-17 21:32  10-17 22:49    1:16    1:16       1:42
Fix                         2    0  10-17 22:49  10-17 23:30    0:41    0:41       0:41
Commit                      1    0  10-17 23:30  10-17 23:50    0:20    0:20       0:20

elapsed, first start to last end   2:43
working, some agent at work        2:43
agent time, all agents added up    3:09
gaps over 20 minutes: 0
Claude Code's record of wf_e3c484d2-242, last launch only: phase256-closed-run-narrow-host, 2:43, 6 agents, 1,491,785 tokens
ColumnWhat it measures
lostSubagents that were interrupted mid-run (for example, by a rate limit or manual stop) and had to restart
wallUnion of time intervals where at least one agent in the stage was active
workingStage wall time minus any internal silent gaps longer than 20 minutes
agent timeSum of all individual agents’ working times (shows total compute effort across parallel agents)
elapsedEnd-to-end wall clock time from the first agent’s start to the last agent’s completion, including all gaps

In Phase 256 above, Verify ran two agents concurrently (an independent verifier and a gates runner) for a combined 1:42 of agent time inside 1:16 of wall time.

By default, phase-timings.py groups agents by the stage title passed to phase(). Passing --by label groups them by agent label prefix instead (stripping suffixes like :B3 from build1:B3), which breaks out individual fix and reverify rounds. Here is a large Tier 3 feature (Phase 252) that went through 3 fix rounds:

$ python3 phase-timings.py .../subagents/workflows/wf_4b9ab0eb-3cd --by label
runs: wf_4b9ab0eb-3cd
stage                  agents lost  start UTC    end UTC        wall working agent time
spec                        1    0  10-17 03:43  10-17 04:35    0:52    0:52       0:52
base                        2    0  10-17 04:35  10-17 18:31    0:06    0:06       0:06
build1                      9    2  10-17 04:37  10-17 09:20    3:39    3:39      11:57
integrate1                  1    0  10-17 09:20  10-17 10:48    1:28    1:28       1:28
verify                      3    0  10-17 10:48  10-17 12:49    2:01    2:01       3:54
fix1                        1    0  10-17 12:49  10-17 14:44    1:55    1:55       1:55
reverify1                   3    0  10-17 14:44  10-17 15:37    0:53    0:53       2:24
fix2                        1    0  10-17 15:37  10-17 16:42    1:05    1:05       1:05
reverify2                   3    0  10-17 16:42  10-17 17:38    0:55    0:55       2:14
fix3                        2    1  10-17 17:38  10-17 19:23    0:53    0:53       0:53
reverify3                   3    0  10-17 19:23  10-17 20:07    0:45    0:45       1:59
commit                      1    0  10-17 20:07  10-17 21:43    1:36    1:36       1:36

elapsed, first start to last end   18:00
working, some agent at work        16:08
agent time, all agents added up    30:24
gaps over 20 minutes: 2
  10-17 06:26 to 10-17 07:30 UTC    1:04  usage limit
  10-17 17:41 to 10-17 18:27 UTC    0:47  no agent at work: a stop, a stall or a wait
Claude Code's record of wf_4b9ab0eb-3cd, last launch only: phase-build-queue, 3:11, 26 agents, 2,008,447 tokens

Notice the bottom line: Claude Code’s built-in summary only recorded the final resumed launch (3:11). Reading the transcripts recovers the full 16:08 of working time across both interruptions (a 64-minute rate limit during build1 and a 47-minute pause before fix3 to ship another release).

How to estimate a future phase

Once you have stage timings for your past runs, estimating a new phase is straightforward reference-class forecasting: count the scope inputs in the new spec, find the closest completed phases, and sum their stage durations.

Here is the working-time breakdown across seven recent phases: five large Tier 3 features (Phases 247–253) and two small Tier 2 bug fixes (Phases 256–257), excluding Base worktree setup:

PhaseBuildersFix roundsSpecBuild and integrateVerify and fixCommitWorking timeRelease
2478 in 2 waves30:2610:259:591:3622:250:44
2487 in 2 waves30:357:459:011:3418:550:42
249711:215:194:321:4012:520:45
252730:525:078:271:3616:020:41
253811:013:073:391:269:130:41
25611none0:261:570:202:430:35
25711none1:352:020:193:550:43

When comparing a new Tier 3 spec against those five large runs, I look at five scope signals:

Scope signalRange across the five Tier 3 phases
Spec length433 to 661 lines
Builders and waves7 or 8 builders across 1 or 2 waves
Verification proofs (scenarios verifiers must run)24 to 49 in the initial verify round
Files touched in the final commit155 to 397
Fix rounds required1 to 3

The first three signals can be read directly off a new spec (along with the number of files assigned to builders); the expected number of fix rounds is inferred from whichever past phases are closest in complexity.

Breaking the five Tier 3 phases down by individual stage reveals where the time actually goes:

StageMedianRangeSamples
Spec0:520:26 to 1:065
One build wave + integrator4:523:07 to 5:347
Initial verify round (3 verifiers in parallel)1:300:59 to 2:015
First fix round (fix + reverify)2:482:38 to 3:545
Each subsequent fix round2:171:38 to 2:276
Commit (including make gates)1:361:26 to 1:405
Release train0:410:29 to 0:459

Two variables dominate total runtime: whether the build requires a second wave (adding 3:24 to 4:52) and how many fix rounds it takes (adding 1:38 to 3:54 per round). Surprisingly, the raw number of verification proofs barely moved the initial verify stage: Phase 247 ran 24 proofs in 1:30, while Phase 249 ran 49 proofs in 1:14.

Suppose a new Tier 3 spec calls for a single wave of 7 builders, and comparable specs needed 2 fix rounds. Summing the stage medians produces the estimate:

PartStages includedMedian estimateStage-extremes range
Build and integrateSpec + 1 build wave5:433:33 to 6:40
Verify and fixInitial verify + 2 fix rounds + commit8:116:40 to 10:03
Release1 release train0:410:29 to 0:45
Total working timeLaunch to production (excluding idle gaps)14:3610:42 to 17:27

Notice that 13:54 of pre-release working time sits right between the two closest historical runs: Phase 249 (7 builders, 1 fix round, 12:52) and Phase 252 (7 builders, 3 fix rounds, 16:02). Adding every stage’s minimum and maximum gives a wide theoretical envelope (10:42 to 17:27); anchoring on the closest comparable phases gives a much tighter practical range.

Always ask for working time and elapsed wall-clock time separately. Across those seven phases, idle gaps over 20 minutes added anywhere from 0:00 to 22:29 on top of working time, almost entirely from rate limits. Phase 247 hit 7 rate limits and one 5-hour stall, turning 22:25 of active work into 45:07 on the wall clock.

Here is the prompt I use to have Claude generate an estimate from our logs (using the directory layout from Getting started):

Estimate how long the work in work/<name>/spec.md will take, from launch to release.

1. Read every work/*/run.json. Each names a past run and its transcript folder.
2. Run python3 tools/phase-timings.py --csv on each transcript folder, one run per
   call. Make one table with a row per run and a column per stage.
3. Add each past run's inputs from its spec and its commit: spec lines, builders,
   files the commit touched, and fix rounds.
4. If work/releases.csv exists, give the median and range of its release times.
5. Measure the same inputs for the new spec. Name the closest past runs and say why.
6. Answer in three parts, each with a range: build and integrate, then verify and
   fix, then release. Then give the total working time.
7. Give the elapsed time separately, with the stops you expect, e.g. usage limits.
8. Say what would move the estimate most, and how sure you are.
9. Save the answer to work/<name>/estimate.md.

Even before I extracted phase-timings.py into a standalone tool, my main session was already making informal estimates in its status updates by comparing in-flight work against earlier phases (“If it goes like 247…”). Checking four of those estimates against the transcript logs shows how quickly the calibration converges:

Timestamp (UTC)Claude’s estimateActual duration in logs
Oct 16, 02:29Release 0.42.1 “should take about an hour”60 minutes
Oct 16, 03:49Phase 248’s remaining stages will “take 8 to 15 more working hours”13h 13m to commit (13h 56m including release), all continuous working time
Oct 16, 16:43Phase 248’s final gate checks need “about 15 to 20 minutes”19 minutes
Oct 16, 16:43Release 0.43.0 takes “about 30 to 40 minutes”42 minutes

Getting started

You can set up the pipeline and timing script in an afternoon; after about 5 completed runs, you’ll have enough baseline data for your first comparative estimate.

  1. Enable Ultracode. Run /effort ultracode in your session, or prefix your task prompt with ultracode.
  2. Standardize your workflow stages in CLAUDE.md. Subagents read CLAUDE.md on every run, ensuring stage names stay consistent across sessions:

    Add to CLAUDE.md that each piece of work runs as one workflow with these stages,
    named exactly so in phase(): Spec, Build, Verify, Fix, Commit. The spec goes in
    work/<name>/spec.md and gives each builder its own files. One verifier who did
    not build the change runs the code. Allow at most one fix round. Only the
    committer commits. At launch, write work/<name>/run.json with the run id, the
    transcript folder and the launch time. Save the args beside it and never edit
    them. Add a line to its history at every stop, with the cause.
  3. Run a small workflow with a JSON schema verdict. Replace the issue and file path with one from your own repo:

    ultracode. Fix issue 12 as a workflow. One builder edits only src/parser.ts and
    its test. One verifier runs the tests and returns JSON with a verdict of pass
    or needs_work and a list of problems. Then one committer.
  4. Save the workflow script. Once your first run succeeds, ask Claude to save the script in .claude/workflows/ and reuse it for future tasks so stage names never drift.
  5. Time the run. Drop phase-timings.py into tools/phase-timings.py and ask Claude:

    Give me the transcript folder of that workflow, then run
    python3 tools/phase-timings.py on it and show me the table.
  6. Estimate your next task after ~5 runs. Run the estimation prompt from the previous section to generate work/<name>/estimate.md before launching the workflow.
  7. Audit the estimate when the run finishes. Close the loop so future estimates learn from any misses:

    Compare work/<name>/estimate.md with what the transcripts show, stage by stage.
The estimate loopRun a workflowthe same stages each timeTime itphase-timings.pyIts recordsjoin the past runsAsk for the next estimateit reads the past runsComparewith what happened
Every completed workflow adds another row of stage timings for future estimates to reference.

What this changes for planning

An estimate stops being a guess when it cites the specific historical runs it came from. Before launching a phase in my backlog, I know whether it’s a 3-hour bug fix or a 16-hour overnight build. And when an in-flight stage runs past its historical range, I can spot the outlier early and investigate why.

None of this required building custom telemetry into Claude Code; the transcripts were already sitting on disk. What made the logs predictive was standardizing the shape of the work so every phase could be measured against the ones before it. When you introduce a genuinely new kind of workflow with unfamiliar stages, the first run won’t have a baseline; its transcript becomes the first row for the runs that follow.