1.0 - Introduction
An agentic workflow chains multiple agent calls together, with the output of one
becoming the input of the next. Each call can have a different focus: analyze,
plan, generate, test. This chapter covers how to structure multi-step workflows,
how to pass state between calls, and where to add human-in-the-loop checkpoints.
Autonomy is the defining property of agentic AI: the system decides the sequence
of actions needed to reach a goal rather than waiting for each step to be
explicitly requested. The developer defines the goal and the available tools;
the AI plans and executes the steps.
When does autonomy help?
- When the task has multiple clearly-ordered steps and each step's
output is the next step's input.
- When you want repeatability - the same workflow produces the same
kind of output regardless of who runs it.
- When the individual steps are too small to justify a full CLI session
but too many to do manually each time.
2.0 - Analyze → Plan → Generate → Test
A four-call workflow for producing a new module:
-
Analyze - Read existing code; produce a JSON summary
of types, functions, and dependencies.
-
Plan - Feed the summary to a second call; ask for a
numbered implementation plan as JSON.
-
Generate - Feed the plan to a third call; ask for
source code, one file at a time.
-
Test - Run the generated code, capture stdout/stderr,
feed errors back into a fourth call for a fix.
Each step is a separate API call. Separating the steps prevents context bloat:
the analyze call only sees source files; the generate call only sees the plan.
Each call gets exactly the context it needs and nothing more.
The JSON boundary between steps is also a checkpoint boundary. You can inspect
the analysis output before the plan runs, or inspect the plan before code is
generated, without changing the architecture of the pipeline.
3.0 - Passing State Between Calls
Use plain Python data structures to carry results forward. A dict
or dataclass per step is enough for most workflows.
For long-running pipelines, serialize to a JSON file so a failed step can be
retried without re-running the earlier ones. A pattern that works well:
- Write step output to
step_N_output.json before starting step N+1.
- At startup, check whether
step_N_output.json already exists and
skip that step if it does.
- Delete all output files to force a full re-run from scratch.
This gives you idempotent steps at no cost: the pipeline resumes from the last
successful step rather than starting over on every transient failure.
4.0 - Human-in-the-Loop Checkpoints
Pause after high-risk steps for user confirmation. A simple
input("Continue? [y/N] ") is enough for a personal script.
For production workflows, write the plan to a file and require an explicit
approval file before the generate step runs.
Common checkpoint positions:
-
After Analyze - confirm the dependency summary is
correct before committing to a plan.
-
After Plan - review the numbered steps before any
code is written. This is the highest-value checkpoint: a bad plan produces
bad code regardless of how well the generation step runs.
-
After Generate - review generated files before the
test harness runs them. Useful when the generated code touches shared state
or external systems.
-
After Test failure - review the error and proposed
fix before applying it. Automated fix loops can amplify a misdiagnosis.
For CLI workflows, Claude Code's built-in permission prompts provide checkpoints
at every file write and shell command. For API-driven pipelines, add explicit
input() gates at the boundaries listed above.
5.0 - Example: Building CsTextFinder
CsTextFinder has three independent library projects - CommandLine,
DirNav, and Output - plus EntryPoint
that wires them together. Building each library first, in dependency order, catches failures
close to their source before the integration build runs.
The agentic element is Claude diagnosing build failures. When dotnet build
fails on a library, the script passes the compiler output to Claude, prints the suggested
fix, and pauses for a human-in-the-loop confirmation before retrying.
import subprocess, json, anthropic
client = anthropic.Anthropic()
LIBS = ["CommandLine", "DirNav", "Output"]
ROOT = "CsTextFinder"
def run(cmd, cwd=ROOT):
r = subprocess.run(cmd, capture_output=True, text=True, cwd=cwd)
return r.returncode, r.stdout + r.stderr
def diagnose(output, name):
resp = client.messages.create(
model="claude-opus-4-7",
max_tokens=512,
messages=[{"role": "user",
"content": f"Build failure in {name}:\n{output}\n"
"Suggest one concrete fix."}]
)
return resp.content[0].text
results = {}
for lib in LIBS:
code, out = run(["dotnet", "build", "-nologo", "-v", "q"],
f"{ROOT}/{lib}")
if code != 0:
print(f"\n--- {lib} build failed ---\n{out}")
print("Claude says:", diagnose(out, lib))
input("Apply fix, then press Enter to retry: ")
code, out = run(["dotnet", "build", "-nologo", "-v", "q"],
f"{ROOT}/{lib}")
results[lib] = "pass" if code == 0 else "fail"
# build + smoke-test the full project
code, out = run(["dotnet", "build", "-nologo", "-v", "q"])
if code == 0:
code, out = run(
["dotnet", "run", "--project", "EntryPoint", "--",
"/P", ".", "/p", "cs", "/r", "class"]
)
results["EntryPoint"] = "pass" if code == 0 else "fail"
print(json.dumps(results, indent=2))
State travels in the results dict. A library that fails to build leaves
"fail" in its slot, so the final report pinpoints exactly which component
broke without re-running the ones that already passed.
The smoke test runs the full pipeline - command-line parsing, directory walk, regex
match - with a single real query against the project's own source tree. A passing
smoke test confirms every component composes correctly at runtime, not just at compile time.
6.0 - References