Task decomposition means breaking a large goal into smaller subtasks that are independently executable and checkable. Agents that skip this step try to “do the whole thing” in one blob of reasoning — and then cannot tell which part failed.
“Create the weekly support summary” is a goal, not a step. Fetching tickets is a step. Clustering themes is a step. Each step has inputs, outputs, and a pass/fail check. When step 3 fails, you retry step 3 — not the entire week. Decomposition is how reliability and traceability enter agent design.
fetch_tickets → TicketList).Goal: “Create weekly support summary.”
Each step can fail differently: CRM auth vs empty clusters vs tone policy on recommendations.
A step is not done because the model said “done.” Prefer programmatic checks: len(tickets) > 0, JSON schema valid, pytest exit code 0, Slack API 200. Use LLM judges only when the property is inherently semantic (summary faithfulness).
A tiny decomposer with per-step validation and retry.
from dataclasses import dataclass, field
@dataclass
class Step:
name: str
run: callable
check: callable
retries: int = 2
@dataclass
class RunState:
artifacts: dict = field(default_factory=dict)
log: list = field(default_factory=list)
def fetch(state: RunState):
state.artifacts["tickets"] = [{"id": 1, "type": "billing"}, {"id": 2, "type": "billing"}]
def cluster(state: RunState):
types = [t["type"] for t in state.artifacts["tickets"]]
state.artifacts["clusters"] = {t: types.count(t) for t in set(types)}
def summarize(state: RunState):
c = state.artifacts["clusters"]
state.artifacts["summary"] = f"Top: {max(c, key=c.get)} ({max(c.values())})"
STEPS = [
Step("fetch", fetch, lambda s: len(s.artifacts.get("tickets", [])) > 0),
Step("cluster", cluster, lambda s: bool(s.artifacts.get("clusters"))),
Step("summarize", summarize, lambda s: "Top:" in s.artifacts.get("summary", "")),
]
def run_plan(steps: list[Step]) -> RunState:
state = RunState()
for step in steps:
for attempt in range(step.retries + 1):
step.run(state)
if step.check(state):
state.log.append(f"{step.name}:ok")
break
else:
state.log.append(f"{step.name}:failed")
break
return state
print(run_plan(STEPS).artifacts["summary"])
Take one messy goal from your backlog and force it into a table with columns: step name, input artifact, output artifact, check, retryable?, HITL?. If you cannot fill a row, the step is still a wish. Keep the first production version under eight steps; merge vanity steps that do not change the world or the artifact.
For side-effecting steps, write the idempotency story before the prompt. “Send Slack message” needs a dedupe key; “create ticket” needs an external reference you can look up. Decomposition without idempotency just creates precise, repeatable duplicates.
When two steps share no artifacts — for example fetching CRM tickets and fetching status-page incidents — run them concurrently inside the orchestrator, then join before summarize. Decomposition makes that parallelism obvious; a monolithic prompt hides it. Just keep join checks strict: both branches must pass their predicates before the merge step reads their outputs.
Use stable step IDs in logs (fetch_tickets, not “Step 1”). Dashboards and alerts should key off those IDs so a spike in cluster_themes failures pages the right owner. When you rename a step, keep the old ID as an alias for a release so historical metrics do not fracture. Decomposition is an operational interface, not only a prompting trick.
Decompose goals into named, dependency-aware steps with programmatic checks and retries so agents make reliable partial progress instead of one opaque attempt.