The previous article covered what Claude Opus 4.8's dynamic workflows are — the architecture, the benchmarks, the pricing, and the token explosion risk. This article is the implementation companion: how to actually run a workflow, what to configure before you do, and how to replicate the same pipeline in Python when you need programmatic control. The worked example is a data pipeline code review: each module gets its own agent, agents run in parallel, and the orchestrator aggregates the results into a structured report.
There are two distinct ways to use Claude Opus 4.8 workflows in production. They are not interchangeable — each has a different control model, a different integration surface, and different failure modes. Choosing the wrong one for your context is the most common mistake teams make when first adopting agentic workflows.
- Claude decides how to orchestrate
- 16 concurrent agents max; 1,000 total per run
- Claude writes a JS orchestration script — you provide direction
- Best for: open-ended codebase tasks
- Setup time: minutes (AGENTS.md + one command)
- Cost visibility:
usage.iterationsin output
- You write the orchestration code
- Concurrency limited by your Semaphore + rate limits
- Full control: retry logic, kill switch, custom routing
- Best for: structured, repeatable pipelines
- Setup time: hours (orchestrator class + telemetry)
- Cost visibility: explicit per-call
usagetracking
The use case: Python data pipeline code review
The example codebase throughout this article is a standard Python ETL project: four modules, each with known issues that a code review should catch — missing error handling, no type annotations, unsafe SQL concatenation, and untested edge cases. The goal is to have each module reviewed by a dedicated agent, with test generation and fix proposals produced in parallel, then aggregated into a single report.
etl_pipeline/ ├── AGENTS.md # agent instructions (shared by both tracks) ├── src/ │ ├── extractors/ │ │ └── api_extractor.py # fetches data from external API │ ├── transformers/ │ │ └── data_transformer.py # normalises and validates raw records │ ├── loaders/ │ │ └── db_loader.py # writes to PostgreSQL via SQLAlchemy │ └── validators/ │ └── schema_validator.py # validates Pydantic schemas against data └── tests/ # empty — agents will populate this
Track A: Claude Code CLI with ultracode
Prerequisites
Dynamic workflows require Claude Code v2.1.154 or later. Check your version with claude --version and update with claude update (or npm update -g @anthropic-ai/claude-code if you prefer npm directly). You must also switch to Opus 4.8 inside the session — the workflow engine only activates on models that support xhigh effort (Anthropic, 2026b).
Plan availability: Dynamic workflows are active by default on Max and Team plans. Pro users can enable them manually from /config. Enterprise plans default to off — administrators enable them through managed settings before team members can access the feature.
Step 1: Write AGENTS.md
AGENTS.md is the document Claude reads before starting any work. Write it before triggering the workflow — Claude uses it to plan the orchestration, allocate agents, and enforce boundaries. Vague instructions produce vague orchestration; specific instructions produce specific, reviewable output.
# AGENTS.md — ETL Pipeline Code Review ## Context Python ETL pipeline. Four modules in src/. Tests in tests/ are empty and must be populated. The pipeline runs in production on AWS Lambda with a 15-second timeout per invocation. ## Review criteria (apply to every module) 1. Security: SQL injection, secrets in code, unsafe deserialization, unvalidated inputs 2. Error handling: all IO operations must have try/except with specific exception types 3. Type annotations: all public functions must have full signatures (PEP 484) 4. Testability: functions with side effects must be injectable (no hard-coded dependencies) ## Output format per module For each file, produce three artefacts: - ISSUES.md — numbered list of issues with severity (critical / high / medium) - tests/test_[module].py — pytest file that fails on the identified issues - PATCH.diff — unified diff that fixes all issues and makes the tests pass ## Forbidden - Do NOT modify files outside src/ and tests/ - Do NOT add external dependencies not already in requirements.txt - Do NOT rewrite a module from scratch — patch the existing code ## Concurrency note Each module is independent. All four can be reviewed in parallel. Assign one primary agent and one adversarial reviewer per module.
Step 2: Launch the workflow
# 1. Navigate to the project root (where AGENTS.md lives) cd etl_pipeline/ # 2. Start a Claude Code session claude # 3. Inside the session: switch to Opus 4.8 /model claude-opus-4-8 # 4. Set ultracode effort — activates xhigh reasoning + workflow engine /effort ultracode # 5. Trigger the workflow — include "workflow" anywhere in the prompt Create a workflow to review all four Python modules in src/ following AGENTS.md, producing ISSUES.md, a test file, and a PATCH.diff for each module. Modules are independent — run reviews in parallel. # Claude will now plan the orchestration, confirm the approach, # then dispatch agents. Each wave runs up to 16 agents concurrently. # Track spend in real time: /usage
After you submit the prompt, Claude Code enters planning mode: it reads AGENTS.md, surveys the src/ directory, and writes a JavaScript orchestration script — not Python, not prose — that encodes loops, branching, and agent-count decisions as runnable code. The model's context window receives only the final verified answer; the intermediate work lives in script variables. Claude then dispatches agents in waves of up to 16 concurrent workers (the concurrency ceiling protects local CPU resources). For this four-module task you will see: a planning agent that maps dependencies, four primary review agents (one per module), four adversarial reviewers checking for missed issues, and a synthesis agent that produces SUMMARY.md. Claude Code surfaces progress in the terminal — the /usage command shows token spend per iteration (Anthropic, 2026b).
When the workflow finishes, drop back to standard effort immediately: /effort high. Ultracode applies xhigh reasoning to every subsequent message in the session, including trivial follow-up questions.
Track B: Python SDK orchestration
The CLI approach works well for one-off tasks where Claude can freely explore the codebase. For scheduled pipelines, CI/CD integration, or workflows that need custom retry logic and cost accounting, you need a Python orchestrator — a class that manages the Anthropic API calls, enforces concurrency limits, tracks token spend, and handles compaction in long sessions.
Why build your own orchestrator
The Python SDK gives you three things the CLI cannot: deterministic retry behaviour on API errors, a token budget kill switch enforced in your code (not dependent on Anthropic's optional guardrails), and structured output that feeds directly into your downstream systems — a database, a Slack notification, a CI gate — without manual intervention.
The orchestrator class
import asyncio import anthropic from dataclasses import dataclass, field from pathlib import Path from typing import Optional @dataclass class ReviewResult: file_path: str issues: str = "" test_code: str = "" patch: str = "" tokens_used: int = 0 error: Optional[str] = None class AsyncWorkflowOrchestrator: """Parallel code review orchestrator with token budget enforcement.""" SYSTEM_PROMPT = """You are a senior Python engineer performing a code review. For the file provided, return a JSON object with three keys: "issues": markdown list of issues with severity labels "test_code": complete pytest file that fails on the issues you found "patch": unified diff that fixes all issues and makes the tests pass Return ONLY the JSON — no prose before or after.""" def __init__( self, model: str = "claude-opus-4-8", token_budget: int = 200_000, # kill switch threshold max_concurrency: int = 5, # respect API rate limits ): self.client = anthropic.AsyncAnthropic() self.model = model self.token_budget = token_budget self.tokens_spent = 0 self._killed = False self._semaphore = asyncio.Semaphore(max_concurrency) self._lock = asyncio.Lock() # guards tokens_spent counter async def _charge_tokens(self, tokens: int) -> bool: """Thread-safe token accounting. Returns False if budget exceeded.""" async with self._lock: self.tokens_spent += tokens if self.tokens_spent > self.token_budget: self._killed = True return False return True async def review_file(self, path: Path) -> ReviewResult: result = ReviewResult(file_path=str(path)) if self._killed: result.error = "Workflow killed: token budget exceeded" return result async with self._semaphore: try: content = path.read_text(encoding="utf-8") response = await self.client.messages.create( model=self.model, max_tokens=8192, thinking={"type": "enabled", "budget_tokens": 10_000}, system=[{ "type": "text", "text": self.SYSTEM_PROMPT, "cache_control": {"type": "ephemeral", "ttl": "1h"} }], messages=[{ "role": "user", "content": f"Review this file ({path.name}):\n\n```python\n{content}\n```" }] ) # Charge tokens — kill if over budget total = response.usage.input_tokens + response.usage.output_tokens result.tokens_used = total if not await self._charge_tokens(total): result.error = f"Budget exceeded after {self.tokens_spent:,} tokens" return result # Extract the text block (skip thinking blocks) text = next( b.text for b in response.content if b.type == "text" ) import json data = json.loads(text) result.issues = data.get("issues", "") result.test_code = data.get("test_code", "") result.patch = data.get("patch", "") except Exception as e: result.error = str(e) return result async def run(self, src_dir: Path) -> list[ReviewResult]: files = list(src_dir.rglob("*.py")) tasks = [self.review_file(f) for f in files] return await asyncio.gather(*tasks)
Running the orchestrator
import asyncio from pathlib import Path from orchestrator import AsyncWorkflowOrchestrator async def main(): src = Path("etl_pipeline/src") out = Path("review_output") out.mkdir(exist_ok=True) orchestrator = AsyncWorkflowOrchestrator( model="claude-opus-4-8", token_budget=300_000, # ~$1.50 at Standard tier — adjust to your comfort max_concurrency=4, # 4 files in parallel, one API call each ) print(f"Reviewing {src} — kill switch at {orchestrator.token_budget:,} tokens") results = await orchestrator.run(src) # Write artefacts and print summary errors, reviewed = [], [] for r in results: name = Path(r.file_path).stem if r.error: errors.append(r) print(f" ✗ {name}: {r.error}") continue (out / f"{name}_ISSUES.md").write_text(r.issues) (out / f"test_{name}.py").write_text(r.test_code) (out / f"{name}.patch").write_text(r.patch) reviewed.append(r) print(f" ✓ {name}: {r.tokens_used:,} tokens") total = orchestrator.tokens_spent cost = (total / 1_000_000) * 5.00 # Standard tier input rate print(f"\nTotal: {len(reviewed)} reviewed, {len(errors)} failed") print(f"Tokens: {total:,} | Estimated cost: ${cost:.4f}") print(f"Output: {out}/") asyncio.run(main())
A typical Python module of 150–300 lines costs roughly 2,000–4,000 input tokens when passed with the system prompt (which is cached after the first call). Four modules in parallel: approximately 8,000–16,000 input tokens total, plus output. At Standard-tier pricing ($5.00/M input, $25.00/M output), the full review of a four-module project costs under $0.15. The token_budget=300_000 in the example is a conservative safety net — at Standard pricing that is $1.50 of maximum exposure before the kill switch fires.
The shared system prompt is cached for 1 hour after the first agent writes it. Agents 2–4 hit the cache and are charged $0.50/M instead of $5.00/M on those tokens — a 90% reduction on the most expensive input block. This is why setting the system prompt cache before dispatching parallel agents is worth the one-line overhead.
Handling long sessions: compaction in the Python orchestrator
For pipelines that run continuously — a nightly review across a large codebase, or a monitoring agent that accumulates context over hours — you need to handle compaction inside the orchestrator. The pattern from the engineering guide applies directly: enable pause_after_compaction, intercept the stop reason, persist the summary, and resume (Anthropic, 2026d).
async def call_with_compaction( client: anthropic.AsyncAnthropic, messages: list, vector_store, session_id: str, **kwargs, ) -> anthropic.types.Message: """Wrap a beta messages call; handles compaction transparently.""" response = await client.beta.messages.create( betas=["compact-2026-01-12"], context_management={ "edits": [{ "strategy": "compact_20260112", "trigger": {"type": "token_count", "threshold": 80_000}, "instructions": "Preserve all file paths, issue IDs, and patch hunks verbatim.", "pause_after_compaction": True, }] }, messages=messages, **kwargs, ) if response.stop_reason == "compaction": block = next(b for b in response.content if b.type == "compaction") summary = block.content or "" # guard against null (known edge case) if summary: await vector_store.upsert(session_id, summary) # Resume with empty messages — server has already replaced history return await call_with_compaction(client, [], vector_store, session_id, **kwargs) return response
Choosing between the two tracks
| Decision point | Use Claude Code CLI | Use Python SDK |
|---|---|---|
| Task structure | Open-ended — the scope is not fully defined upfront | Structured — you know exactly which files get which treatment |
| Trigger | Manual, interactive, or scheduled via cron + shell script |
Any Python event: CI webhook, schedule, API call, queue message |
| Output | Files written to disk; Claude Code formats them | Structured data (JSON, dataclass) that your code processes |
| Error handling | Claude Code retries internally; limited visibility | Full control: exponential backoff, partial retry, selective skipping |
| Cost visibility | /usage inside the session; not easily exported |
Per-call usage object — log to any observability system |
| Compaction | Automatic; you cannot pause and inspect the summary | Explicit pause_after_compaction — you control persistence |
A hybrid approach works well for mature teams: use Claude Code CLI for exploratory runs (new codebases, ambiguous tasks, proof-of-concept), then codify the successful pattern into a Python orchestrator for scheduled production use. The AGENTS.md file travels between both tracks unchanged — it is the stable specification that both Claude Code and your Python agents read.
Production checklist before going live
max_concurrency=4, only one of the four parallel calls writes the cache — the rest are instant reads. Set "ttl": "1h" if all agents will finish within an hour.json.loads() call in a try/except and log the raw response on failure — do not silently discard it. A ReviewResult with error set is far more useful than a silently empty file.anthropic.RateLimitError, wait 2attempt seconds (capped at 60s), and retry. The Semaphore in the orchestrator limits concurrency at dispatch time; backoff handles transient limits during execution./effort high after a workflow finish means your next "what files did you change?" message burns xhigh reasoning tokens. This is a common source of unexplained cost spikes in team deployments.- Anthropic. (2026a). Introducing Claude Opus 4.8. anthropic.com/news/claude-opus-4-8
- Anthropic. (2026b). Introducing dynamic workflows in Claude Code. claude.com/blog/introducing-dynamic-workflows-in-claude-code
- Anthropic. (2026c). Python SDK — Claude API Docs. platform.claude.com/docs/en/api/sdks/python
- Anthropic. (2026d). Compaction — Claude API Docs. platform.claude.com/docs/en/build-with-claude/compaction
- Anthropic. (2026e). Prompt caching — Claude API Docs. platform.claude.com/docs/en/build-with-claude/prompt-caching
- Anthropic. (2026f). CLI reference — Claude Code Docs. code.claude.com/docs/en/cli-reference