Multi-agent coding pipelines fail in production the same way distributed jobs fail: unbounded retries, silent partial success, and no human interrupt when the plan goes sideways. Step Functions is the right control plane—not because it “runs agents,” but because it gives you retries with backoff, Map fan-out, task tokens for human approval, and a durable execution history you can replay in a postmortem.
⚡ TL;DR: Model the pipeline as Plan → Implement → Test → Review with explicit state machine transitions. Use Map for file batches, Catch/Retry on Bedrock throttles, and a Task Token human gate before any merge or infra mutate. Emit execution ARNs into PR comments. Never let an agent auto-merge. Pair with LLM Coding Agents on AWS: Safe Tool Sandboxes with Lambda and Bedrock Agents: Idempotent Tool Calls Against DynamoDB Writes.
State machine shape that seniors actually ship
Keep stages coarse. Fine-grained “think” states explode cost and make approvals unusable.
{
"Comment": "Multi-agent coding pipeline",
"StartAt": "PlanAgent",
"States": {
"PlanAgent": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "agent-plan",
"Payload": {
"ticket.$": "$.ticket",
"repoSha.$": "$.repoSha"
}
},
"ResultPath": "$.plan",
"Next": "HumanApprovePlan",
"Retry": [{
"ErrorEquals": ["Bedrock.ThrottlingException", "States.Timeout"],
"IntervalSeconds": 5, "MaxAttempts": 4, "BackoffRate": 2.0
}]
},
"HumanApprovePlan": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
"TimeoutSeconds": 86400,
"HeartbeatSeconds": 3600,
"Parameters": {
"FunctionName": "notify-slack-approval",
"Payload": {
"taskToken.$": "$$.Task.Token",
"plan.$": "$.plan",
"executionArn.$": "$$.Execution.Id"
}
},
"ResultPath": "$.approval",
"Next": "ImplementMap"
},
"ImplementMap": {
"Type": "Map",
"ItemsPath": "$.plan.Payload.fileBatches",
"MaxConcurrency": 3,
"Iterator": {
"StartAt": "ImplementBatch",
"States": {
"ImplementBatch": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "agent-implement",
"Payload.$": "$"
},
"End": true
}
}
},
"ResultPath": "$.patches",
"Next": "TestAgent"
},
"TestAgent": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "agent-test",
"Payload": { "patches.$": "$.patches", "repoSha.$": "$.repoSha" }
},
"ResultPath": "$.test",
"Next": "ReviewGate"
},
"ReviewGate": {
"Type": "Choice",
"Choices": [{
"Variable": "$.test.Payload.passed",
"BooleanEquals": true,
"Next": "OpenPrOnly"
}],
"Default": "FailClosed"
},
"OpenPrOnly": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": {
"FunctionName": "open-draft-pr",
"Payload": {
"patches.$": "$.patches",
"neverMerge": true
}
},
"End": true
},
"FailClosed": {
"Type": "Fail",
"Error": "TestsFailed",
"Cause": "Agent tests failed — no PR opened"
}
}
}
✅ Human token before implement; draft PR only; never gh pr merge from the state machine.
❌ Auto-merge on green tests from an unattended agent.
Human approval that is interruptible
// notify-slack-approval.ts
import { SQSClient, SendMessageCommand } from "@aws-sdk/client-sqs";
export async function handler(event: {
taskToken: string;
plan: { Payload: { summary: string; blastRadius: string[] } };
executionArn: string;
}) {
// ✅ Park the token; humans resume via Slack action → SendTaskSuccess
await new SQSClient({}).send(new SendMessageCommand({
QueueUrl: process.env.APPROVAL_QUEUE!,
MessageBody: JSON.stringify({
taskToken: event.taskToken,
executionArn: event.executionArn,
summary: event.plan.Payload.summary,
files: event.plan.Payload.blastRadius,
expiresAt: Date.now() + 86_400_000,
}),
}));
}
// resume.ts — Slack interactivity handler
import { SFNClient, SendTaskSuccessCommand, SendTaskFailureCommand } from "@aws-sdk/client-sfn";
export async function resume(decision: "approve" | "reject", taskToken: string, actor: string) {
const sfn = new SFNClient({});
if (decision === "reject") {
await sfn.send(new SendTaskFailureCommand({
taskToken,
error: "HumanRejected",
cause: `rejected_by:${actor}`,
}));
return;
}
await sfn.send(new SendTaskSuccessCommand({
taskToken,
output: JSON.stringify({ approvedBy: actor, at: new Date().toISOString() }),
}));
}
Tie approvals to change tickets the same way you would for on-call remediations in LLM Incident Runbooks: Ground On-Call Answers in CloudWatch Signals.
Observability: execution ARN is the correlation ID
// open-draft-pr.ts — stamp the PR with the Step Functions execution
const body = [
`## Agent pipeline`,
`- Execution: \`${executionArn}\``,
`- Plan approval: ${approval.approvedBy}`,
`- Tests: ${test.Payload.summary}`,
``,
`Do **not** merge until CODEOWNERS review. See [Agentic Git Workflows](https://cheatcoders.net/agentic-git-workflows-atomic-commits-from-noisy-llm-diffs/).`,
].join("\n");
CloudWatch metrics to alarm on: ExecutionsFailed, ExecutionsTimedOut, human-gate age > 24h, Map concurrency saturation. Trace Bedrock hops with the patterns in OpenTelemetry for LLMs.
Cost and throttle controls inside the Map
# Cap fan-out and budget tokens per batch
MaxConcurrency: 3
# Inside agent-implement Lambda:
# - application inference profile
# - per-execution token budget from $.plan.Payload.tokenBudget
Budget enforcement belongs at the org layer too — see LLM Cost Controls: Token Budgets Per PR and Per Engineer.
Closing checklist
- [ ] State machine has Plan → HumanApprove → Implement(Map) → Test → DraftPR only
- [ ]
waitForTaskTokenwith 24h timeout and heartbeat; reject path callsSendTaskFailure - [ ] Bedrock throttles use Retry with backoff; no infinite loops
- [ ] Map
MaxConcurrencycapped; per-execution token budget enforced - [ ] PR body includes execution ARN; merge is human-only
- [ ] Alarms on failed/timed-out executions and stale approvals
Related reading
- LLM Coding Agents on AWS: Safe Tool Sandboxes with Lambda
- Bedrock Agents: Idempotent Tool Calls Against DynamoDB Writes
- Agentic Git Workflows: Atomic Commits From Noisy LLM Diffs
- LLM Cost Controls: Token Budgets Per PR and Per Engineer
Last updated on September 11, 2026
Discover more from CheatCoders
Subscribe to get the latest posts sent to your email.
