AWS Step Functions · 2026-09-17 · AWS Step Functions arc

AWS Step Functions for MCP Servers: Standard Workflows, Express, Map State, Wait-for-Callback, and Error Handling — Five Orchestration Patterns

An MCP server that runs all tool logic synchronously inside the JSON-RPC request-response cycle will eventually hit the limits of synchronous processing: tools that run too long, parallel fan-outs that overwhelm downstream APIs, workflows that need human approval at a decision point, or multi-step chains where a single failure should trigger different recovery paths. AWS Step Functions solves all four problems with a declarative state machine that runs outside the MCP server process, persists execution state durably, and handles retry logic without a line of backoff code. This guide synthesizes five patterns — STANDARD activity tasks, EXPRESS synchronous invocation, Map state parallel fan-out, waitForTaskToken approval gates, and declarative Retry/Catch error handling — into a decision framework for choosing the right Step Functions primitive for each MCP tool orchestration need.

Why MCP tool orchestration needs a state machine

MCP tools are invoked inside the JSON-RPC cycle: the client sends tools/call, the server executes the handler synchronously, and the client waits for the response. This model works for tools that finish in under a second. It fails in three increasingly common scenarios:

Duration exceeds the protocol timeout. MCP clients enforce a timeout on tool calls — typically 30–120 seconds. A tool that runs a multi-step database migration, invokes an LLM for chain-of-thought reasoning, or coordinates a multi-service deployment pipeline can easily exceed this window. When the timeout fires, the client gets an error. The tool handler continues executing. The result is silently discarded.

Parallel fan-out without rate control. An agent that fans out 200 tool calls to process a list of URLs, validate a set of documents, or run a compliance check on each item in a data set creates 200 simultaneous in-flight requests from the MCP server process. Without concurrency control, downstream APIs rate-limit or fail under the load. A Map state with MaxConcurrency: 20 limits the fan-out to 20 concurrent items regardless of input size.

Non-deterministic recovery logic. A tool chain that calls five services in sequence must handle the case where any one of them fails. Writing explicit retry loops, exponential backoff, and conditional routing in tool handler code produces fragile, hard-to-test imperative logic. Step Functions expresses the same logic as a declarative Retry array with BackoffRate, MaxDelaySeconds, and JitterStrategy — and a Catch array that routes to specific recovery states on named errors.

The patterns below address each failure mode with the minimum Step Functions surface area needed.

Pattern 1: STANDARD activity tasks — the MCP server as a polling worker

The STANDARD workflow activity pattern is the correct architecture when the MCP server is a long-running process outside of Lambda, when tool processing exceeds Lambda's 15-minute limit, or when the server needs to access local resources (file system, in-process cache, GPU hardware) that Lambda cannot access.

An Activity is an ARN registered with Step Functions that acts as a logical work queue. The state machine's ASL definition routes executions to the Activity via "Resource": "arn:aws:states:region:account:activity:name". The MCP server calls GetActivityTask with long polling — the call blocks for up to 60 seconds waiting for work, returns a task token and input when a task is available, and returns an empty string when no task arrived within the window.

The critical operational parameter is the heartbeat interval. The Activity task state in ASL must configure HeartbeatSeconds — the maximum time between successive SendTaskHeartbeat calls before Step Functions concludes the worker has died and fails the task with States.HeartbeatTimeout. The worker must send heartbeats at an interval shorter than HeartbeatSeconds / 2 to maintain a safe margin. If the worker's processing takes longer than the heartbeat window — for example, a tool call waits 90 seconds for an external API response — the heartbeat timer must be maintained on a separate interval timer running concurrently with the tool execution.

ConfigurationWhat it controlsProduction default
HeartbeatSecondsMax time between SendTaskHeartbeat calls before States.HeartbeatTimeout120s (send heartbeats every 60s)
TimeoutSecondsTotal execution time limit for the task stateMatch the expected tool duration × 2 + buffer
workerName in GetActivityTaskCloudWatch metric label per worker instanceUnique per process instance (hostname + PID)
cause field in SendTaskFailureError detail visible in execution historyTruncate at 30,000 chars; include log correlation ID

The STANDARD workflow stores every execution event — state entered, task scheduled, task heartbeat received, task succeeded — in Step Functions' own durable history for 90 days. This makes STANDARD workflows the right choice when auditability matters: you can replay execution history days later to diagnose exactly which step failed and what input it received. STANDARD workflows also support the 25,000-event ceiling per execution — complex workflows that produce thousands of state transitions (long Map iterations, deeply nested parallel branches) can exhaust this budget. Monitor GetExecutionHistory event counts and alert when any execution's count approaches 20,000.

Pattern 2: EXPRESS synchronous invocation — multi-step tool chains in a single call

The EXPRESS workflow pattern offers a capability that STANDARD does not: StartSyncExecution, which runs an EXPRESS state machine and blocks the HTTP connection until the execution completes, returning the output in the response body. For an MCP server, this means a multi-step tool chain — validate input → call external API → transform response → write to DynamoDB — can be expressed as a state machine and invoked as a single synchronous tool call with no polling loop or separate result-retrieval step.

The hard constraint is 5 minutes maximum duration. EXPRESS executions that exceed 5 minutes are terminated with States.Timeout regardless of where they are in the workflow. For any tool chain that might run longer than 5 minutes — multi-step migrations, long LLM calls, complex data transformations — use STANDARD instead.

The second EXPRESS constraint is observability. EXPRESS executions have no built-in history store: calling GetExecutionHistory on an EXPRESS execution throws ExecutionDoesNotExist. All diagnostics flow through CloudWatch Logs, which requires configuring a logging destination on the state machine before the first execution runs. Without logging, a failed async EXPRESS execution leaves no observable trace — the failure disappears entirely.

The pricing model favors EXPRESS for high-volume, short-duration tool chains. A STANDARD execution with 100 state transitions costs 100 × $0.000025 = $0.0025. An EXPRESS execution running the same workflow for 500ms costs $0.00001 — 250× cheaper. For MCP tools that execute thousands of times per hour, this difference is significant.

The decision framework between STANDARD and EXPRESS for MCP tool orchestration:

ConditionUseReason
Tool chain can exceed 5 minutesSTANDARDEXPRESS hard limit is 5 minutes
Need durable execution audit trailSTANDARDEXPRESS history is ephemeral (CloudWatch Logs TTL)
Human approval steps or activity workersSTANDARDActivities and long waitForTaskToken not supported by EXPRESS
High-volume, <5 min, no audit requirementEXPRESS100,000 exec/sec; 250× cheaper than STANDARD at same state count
Need synchronous response from state machineEXPRESSStartSyncExecution only available for EXPRESS
Trigger from EventBridge or another state machineEitherBoth support async StartExecution; EXPRESS needs logging for observability

Pattern 3: Map state — controlled parallel fan-out for batch tool calls

The Map state pattern runs the same sub-workflow for each item in an array, with configurable concurrency and error tolerance. For MCP tools that must process a list of items — URLs to scrape, documents to analyze, records to validate — Map state provides concurrency control that synchronous parallel Promise.all() cannot: MaxConcurrency limits simultaneous iterations regardless of input size, and ToleratedFailurePercentage allows the Map to complete successfully even if some items fail.

Map state comes in two modes. Inline Map runs iterations within the parent execution's event history — suitable for up to ~40 concurrent iterations before approaching the 25,000-event ceiling. Distributed Map runs each iteration as an isolated child execution, supports millions of items read directly from S3 or DynamoDB, and uses ResultWriter to write results to S3 rather than accumulating them in the parent execution's output (which is capped at 256KB).

The MaxConcurrency value determines production behavior for external API tools. MaxConcurrency: 0 means unbounded — all items fan out simultaneously. For a 500-item list calling a third-party API with a 100 requests/second rate limit, unbounded concurrency produces 400 rate-limit failures. The correct calculation: MaxConcurrency = API rate limit (req/s) × expected iteration duration (s). For a tool that calls an API allowing 100 req/s and each call takes 0.5s, max concurrency = 50.

The ItemBatcher feature (Distributed Map only) groups items into sub-arrays before passing to the iterator. This allows the iterator Lambda to use batch APIs — DynamoDB BatchWriteItem, S3 bulk operations, or tool handlers that accept arrays — instead of making one API call per item. The iterator's input shape changes from a single item to { Items: [...] } — handlers must be adapted to expect this structure.

The combined failure mode that causes the most production incidents with Map states:

ScenarioInline Map behaviorDistributed Map behavior
Single item failureFails entire Map state (unless ToleratedFailurePercentage set)Same — all items still processed up to tolerance threshold
500+ items with complex iteratorExecution fails with ExecutionLimitExceededRuns in child executions — parent unaffected by event count
Result set > 256KBParent execution fails at output bindingResultWriter writes to S3; parent stores only S3 manifest
MaxConcurrency: 0 with external APIRate limit cascade; partial results; retry stormsSame — MaxConcurrency applies to both modes

Pattern 4: waitForTaskToken — human approval gates in the tool chain

The waitForTaskToken pattern pauses a state machine execution and waits indefinitely for an external signal — the correct architecture for any MCP tool chain that requires human review before proceeding: code deployment approval, financial transaction authorization, data deletion confirmation, or any policy decision that cannot be automated.

The flow: the Task state is defined with .waitForTaskToken appended to its Resource ARN. When the execution enters the Task state, Step Functions delivers the task token to a configured SQS queue, Lambda, or API destination — then pauses. The external system reads the token, processes the approval (presents it to a human, runs a policy check, calls an MCP tool), and calls SendTaskSuccess or SendTaskFailure with the token to resume the execution.

Three operational requirements determine whether this pattern works reliably:

Durable token storage. The task token must be stored durably — in DynamoDB with a TTL matching TimeoutSeconds — before the SQS message that delivered it is deleted. If the SQS message is deleted first and the worker process restarts before storing the token, the token is lost permanently. The execution waits until TimeoutSeconds expires with no way to resume it.

Heartbeat maintenance. For approval gates that may wait hours or days, HeartbeatSeconds must be set on the Task state and the approval service must call SendTaskHeartbeat periodically to keep the wait alive. A common pattern: a scheduled Lambda calls SendTaskHeartbeat every hour for all pending approvals (found by scanning the DynamoDB token store). Without heartbeats, the execution fails with States.HeartbeatTimeout at the end of the HeartbeatSeconds window.

Single-use token handling. Task tokens are single-use. Calling SendTaskSuccess twice with the same token — or calling it after the execution has already timed out — throws TaskTimedOut or InvalidToken. The callback handler must: delete the token from DynamoDB atomically before calling Step Functions (preventing duplicate calls), and treat TaskTimedOut as a no-op (the execution already failed).

The natural MCP tool design is a two-tool pair: submit_approval_request (starts a Step Functions execution with a waitForTaskToken state, returns a requestId) and check_approval_status (looks up whether the approval has been granted or denied). The agent calls submit, continues other work, and periodically polls check — a pattern that fits naturally into an agentic tool-use loop without blocking.

Pattern 5: declarative error handling — Retry, Catch, and intrinsic functions

The Step Functions error handling pattern moves retry logic, backoff calculation, and error routing out of tool handler code and into the state machine definition. This produces simpler handler code that does exactly one thing (call the API, transform the data, write the record) and defers all failure handling to the orchestration layer.

The Retry array specifies: which errors to retry (ErrorEquals), how long to wait before the first retry (IntervalSeconds), how many times to retry (MaxAttempts), how aggressively to back off (BackoffRate), what the maximum wait between retries is (MaxDelaySeconds), and whether to add jitter (JitterStrategy: "FULL"). The Catch array specifies: which errors to catch after retries are exhausted (ErrorEquals) and which state to route to (Next). Both arrays match errors in order — States.ALL must be last or it shadows specific handlers.

The four data-flow operators determine what state the next step receives:

The most common production mistake in Catch configuration is using ResultPath: "$" (or omitting it — the default is also "$" for Catch). This replaces the entire execution state with { Error: "...", Cause: "..." } — all accumulated input state is lost. The correct pattern is ResultPath: "$.error", which merges the error into the existing state so the error handler receives both the original input and the error details.

Intrinsic functionUse caseExample
States.FormatBuild S3 keys, log messages, API paths from execution contextStates.Format('executions/{}/{}', $$.Execution.Name, $.id)
States.JsonMergeCombine two objects from parallel branchesStates.JsonMerge($.primary, $.metadata, false)
States.StringToJsonParse Lambda HTTP response body (which is a string)States.StringToJson($.httpResponse.body)
States.JsonToStringSerialize an object for a downstream system expecting a stringStates.JsonToString($.payload)
States.ArrayLengthCount items before a Map stateStates.ArrayLength($.items)
States.MathRandomGenerate jitter for custom retry logicStates.MathRandom(0, 10, $$.Execution.Id)

Unified failure mode table across all five patterns

The failure modes across all five Step Functions patterns share a common structure: a configuration omission or wrong default produces a silent failure that is only detectable hours later. The table below consolidates the highest-impact misconfigurations:

PatternMisconfigurationSymptomFix
STANDARD activityNo heartbeat while worker processesStates.HeartbeatTimeout mid-processing; worker continues unawareSend heartbeats on interval < HeartbeatSeconds/2; catch TaskTimedOut to stop
STANDARD activity25,000-event ceilingExecutionLimitExceeded mid-workflow; no partial outputMonitor event counts; restructure flat sequences as chunked Map states
EXPRESSGetExecutionHistory on EXPRESS executionExecutionDoesNotExist; no history returnedCheck state machine type first; use CloudWatch Logs for EXPRESS history
EXPRESSNo logging configured on EXPRESSFailed async executions leave no traceConfigure logging level ALL at state machine creation
Map stateMaxConcurrency: 0 with external APIRate limit cascade; duplicate failures; retry stormsSet MaxConcurrency = API rate (req/s) × expected duration (s)
Map stateInline Map with >200 itemsParent execution fails with ExecutionLimitExceededUse Distributed Map; alert when parent event count approaches 20,000
waitForTaskTokenToken not stored before SQS message deletedToken lost on restart; execution waits until TimeoutSecondsStore in DynamoDB with TTL before deleting SQS message
waitForTaskTokenSendTaskSuccess called with expired tokenTaskTimedOut error; unhandled throws in callbackCatch TaskTimedOut and treat as no-op; log for reconciliation
Error handlingStates.ALL before specific errors in Catch/RetrySpecific handlers never reached; all errors routed to catch-allAlways put specific error names first; States.ALL last in every array
Error handlingResultPath: "$" on CatchError handler receives only {Error, Cause}; all input state lostUse ResultPath: "$.error" to merge alongside input state
Error handlingNo MaxDelaySeconds on BackoffRate > 1Retry intervals grow unbounded; MaxAttempts 8 with BackoffRate 2 waits up to 128× IntervalSecondsSet MaxDelaySeconds to cap at sensible ceiling (60s for APIs)
Error handlingLambda error class name doesn't match ErrorEqualsSpecific Catch/Retry never triggers; falls through to States.ALLErrorEquals must match the thrown error's class name exactly

Integration with AliveMCP health monitoring

Step Functions executions are a critical dependency for any MCP server that uses them — a broken state machine means broken tools. Register your state machines with AliveMCP to monitor them continuously:

The combined monitoring posture: one AliveMCP probe per state machine checking for stuck executions, one CloudWatch alarm per Activity checking for zero polling, and one DynamoDB scan per waiting state checking for near-timeout tokens. Together, these surface failures in the orchestration layer before they surface to the end user as broken MCP tool calls.