Guide · Workflow Orchestration
MCP Tools for AWS Step Functions — STANDARD vs EXPRESS, execution history, and waitForTaskToken
AWS Step Functions exposes its workflows as state machine executions queryable via the @aws-sdk/client-sfn SDK or the Step Functions REST API. When you build MCP tools for Step Functions — starting executions, polling completion, fetching execution history, or sending task heartbeats — two dimensions dominate the design: STANDARD vs EXPRESS state machine type (different execution history storage, duration limits, and pricing; an MCP tool that calls GetExecutionHistory on an EXPRESS state machine will get a 400 error because EXPRESS executions are not stored in Step Functions — they emit to CloudWatch Logs instead) and the 25,000-event execution history limit (STANDARD executions approaching this limit will fail with ExecutionLimitExceeded; your MCP tool must handle pagination when reading history and warn when executions are approaching the ceiling).
TL;DR
Start executions with StartExecution, passing the state machine ARN and an input JSON string. Poll completion with DescribeExecution — terminal statuses are SUCCEEDED, FAILED, TIMED_OUT, and ABORTED. For STANDARD state machines, use GetExecutionHistory with pagination to trace failures to a specific state and read the executionFailed event's cause and error fields. For EXPRESS state machines, history is in CloudWatch Logs — use FilterLogEvents with the execution ARN as the log stream filter. Never call GetExecutionHistory on an EXPRESS execution: it throws ExecutionDoesNotExist.
STANDARD vs EXPRESS: choosing and detecting the right type
STANDARD state machines store every execution event in Step Functions' own durable history store for up to 90 days. They support executions up to 1 year long and charge per state transition. STANDARD is the right choice for business-critical long-running workflows where audit history, pause-and-resume, and human approval steps are needed. The execution ARN can be used to call GetExecutionHistory, DescribeExecution, and send task tokens via SendTaskSuccess or SendTaskFailure.
EXPRESS state machines do not store history in Step Functions at all — they are designed for high-throughput, short-duration workflows (up to 5 minutes) and charge per execution duration. Execution events are emitted to CloudWatch Logs if you configure a logging destination on the state machine. GetExecutionHistory throws an error because there is no history to retrieve. To inspect EXPRESS executions, you must have configured logging at LEVEL ALL or ERROR on the state machine, then query CloudWatch Logs.
import {
SFNClient,
StartExecutionCommand,
DescribeExecutionCommand,
GetExecutionHistoryCommand,
ListExecutionsCommand,
StopExecutionCommand,
DescribeStateMachineCommand,
} from '@aws-sdk/client-sfn';
const sfn = new SFNClient({ region: 'us-east-1' });
// Detect state machine type before calling type-specific APIs
async function getStateMachineType(stateMachineArn) {
const cmd = new DescribeStateMachineCommand({ stateMachineArn });
const sm = await sfn.send(cmd);
// sm.type: 'STANDARD' | 'EXPRESS'
return {
type: sm.type,
name: sm.name,
status: sm.status, // 'ACTIVE' | 'DELETING'
loggingConfiguration: sm.loggingConfiguration,
// For EXPRESS: check if logging is configured
expressLoggingEnabled: sm.type === 'EXPRESS'
&& sm.loggingConfiguration?.destinations?.length > 0,
};
}
async function startExecution(stateMachineArn, input, name) {
const cmd = new StartExecutionCommand({
stateMachineArn,
input: JSON.stringify(input), // Must be a JSON string, not an object
name, // Optional: custom execution name (must be unique per state machine)
});
const result = await sfn.send(cmd);
return {
executionArn: result.executionArn,
startDate: result.startDate,
};
}
Polling execution status and parsing terminal events
STANDARD execution status has five possible values: RUNNING, SUCCEEDED, FAILED, TIMED_OUT, and ABORTED. The first is the only non-terminal state. When an execution is terminal, DescribeExecution also returns stopDate and, for FAILED/TIMED_OUT, cause and error fields — though these are brief summaries. For detailed failure context including which state failed and what the error was, you must read the execution history.
The execution history contains an ordered list of events, each with an id (sequence number), timestamp, type, and a type-specific detail object. Failure-relevant events include TaskFailed, ExecutionFailed, ActivityFailed, LambdaFunctionFailed, and MapIterationFailed. The last ExecutionFailed event has the executionFailedEventDetails.cause and .error fields that map to the state machine's Catch/Retry configuration.
async function pollExecutionToCompletion(executionArn, pollIntervalMs = 5_000, timeoutMs = 1_800_000) {
const terminal = new Set(['SUCCEEDED', 'FAILED', 'TIMED_OUT', 'ABORTED']);
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const cmd = new DescribeExecutionCommand({ executionArn });
const exec = await sfn.send(cmd);
if (terminal.has(exec.status)) {
return {
executionArn: exec.executionArn,
status: exec.status,
startDate: exec.startDate,
stopDate: exec.stopDate,
input: exec.input ? JSON.parse(exec.input) : null,
output: exec.output ? JSON.parse(exec.output) : null,
// Brief failure summary (full cause is in execution history)
error: exec.error ?? null,
cause: exec.cause ?? null,
durationMs: exec.stopDate
? exec.stopDate.getTime() - exec.startDate.getTime()
: null,
};
}
await new Promise((r) => setTimeout(r, pollIntervalMs));
}
throw new Error(`Execution timed out after ${timeoutMs}ms — ARN: ${executionArn}`);
}
// Retrieve execution history with pagination, filtering for failure events
async function getExecutionFailureEvents(executionArn) {
const failureEventTypes = new Set([
'ExecutionFailed', 'TaskFailed', 'ActivityFailed',
'LambdaFunctionFailed', 'MapIterationFailed', 'TaskTimedOut',
'ActivityTimedOut', 'LambdaFunctionTimedOut',
]);
const failureEvents = [];
let nextToken;
do {
const cmd = new GetExecutionHistoryCommand({
executionArn,
maxResults: 1000,
reverseOrder: true, // Start from end — failure events are near the end
nextToken,
});
const response = await sfn.send(cmd);
for (const event of response.events) {
if (failureEventTypes.has(event.type)) {
// Extract failure details from the type-specific detail field
const detail = event.executionFailedEventDetails
?? event.taskFailedEventDetails
?? event.activityFailedEventDetails
?? event.lambdaFunctionFailedEventDetails
?? event.mapIterationFailedEventDetails
?? {};
failureEvents.push({
id: event.id,
timestamp: event.timestamp,
type: event.type,
error: detail.error,
cause: detail.cause,
// cause can be a nested JSON string — try to parse it
parsedCause: (() => {
try { return JSON.parse(detail.cause); } catch { return null; }
})(),
});
}
}
nextToken = response.nextToken;
// Stop early once we have the primary failure events — don't read all 25,000 events
if (failureEvents.length >= 10) break;
} while (nextToken);
return failureEvents;
}
waitForTaskToken pattern: external callbacks to running executions
The waitForTaskToken integration pattern allows a Step Functions state to pause and wait for an external system to call back with success or failure. The state machine passes a task token (an opaque string) to an SQS queue, Lambda function, or API destination at the time it pauses. The external system — a human approval service, a long-running batch process, or an MCP tool itself — calls SendTaskSuccess or SendTaskFailure with the token when the work is done. This is the correct pattern for human-in-the-loop workflows, external system callbacks, and approval gates.
The task token is single-use and expires after the step's HeartbeatSeconds or the state machine's overall TimeoutSeconds. Calling SendTaskSuccess or SendTaskFailure with an expired or already-used token throws TaskTimedOut or InvalidToken. If a waiting state needs long-running external work, use SendTaskHeartbeat periodically to reset the heartbeat timer.
import { SendTaskSuccessCommand, SendTaskFailureCommand, SendTaskHeartbeatCommand } from '@aws-sdk/client-sfn';
// Complete a waitForTaskToken step with success
async function completeTaskWithSuccess(taskToken, output) {
const cmd = new SendTaskSuccessCommand({
taskToken,
output: JSON.stringify(output), // Must be a JSON string
});
await sfn.send(cmd);
return { status: 'success', taskToken };
}
// Complete a waitForTaskToken step with failure (causes the state to take its Catch branch)
async function completeTaskWithFailure(taskToken, errorCode, cause) {
const cmd = new SendTaskFailureCommand({
taskToken,
error: errorCode, // Short error code: 'ValidationFailed', 'ApprovalDenied', etc.
cause: typeof cause === 'string' ? cause : JSON.stringify(cause),
});
await sfn.send(cmd);
return { status: 'failed', taskToken, error: errorCode };
}
// Keep a waitForTaskToken step alive during long external work
async function sendHeartbeat(taskToken) {
const cmd = new SendTaskHeartbeatCommand({ taskToken });
await sfn.send(cmd);
return { heartbeatSent: true, timestamp: new Date().toISOString() };
}
// An MCP tool that acts as a human approval gate:
// 1. Read task tokens from SQS queue where Step Functions delivers them
// 2. Present to user or automated policy check
// 3. Call completeTaskWithSuccess or completeTaskWithFailure
async function processApprovalQueue(sqsClient, queueUrl) {
const { Messages } = await sqsClient.receiveMessage({ QueueUrl: queueUrl, MaxNumberOfMessages: 1 });
if (!Messages?.length) return null;
const message = Messages[0];
const body = JSON.parse(message.Body);
// body.TaskToken is set by Step Functions when it enqueues the message
const taskToken = body.TaskToken;
const executionInput = JSON.parse(body.Input ?? '{}');
return {
taskToken,
executionInput,
receiptHandle: message.ReceiptHandle,
// Caller must call completeTaskWithSuccess/Failure with taskToken
// and then delete the SQS message with receiptHandle
};
}
Listing executions and detecting stuck runs
ListExecutions returns executions for a state machine, filterable by status. For STANDARD state machines, it returns up to 1,000 results per page and supports filtering by statusFilter. Use it to detect stuck RUNNING executions older than a threshold — a RUNNING execution that started more than your expected maximum duration ago is a candidate for investigation or forced stop.
async function listRunningExecutions(stateMachineArn) {
const executions = [];
let nextToken;
do {
const cmd = new ListExecutionsCommand({
stateMachineArn,
statusFilter: 'RUNNING',
maxResults: 1000,
nextToken,
});
const response = await sfn.send(cmd);
executions.push(...response.executions);
nextToken = response.nextToken;
} while (nextToken);
return executions;
}
async function detectStuckExecutions(stateMachineArn, maxExpectedDurationMs) {
const running = await listRunningExecutions(stateMachineArn);
const now = Date.now();
return running
.filter((exec) => now - exec.startDate.getTime() > maxExpectedDurationMs)
.map((exec) => ({
executionArn: exec.executionArn,
name: exec.name,
startDate: exec.startDate,
runningForMs: now - exec.startDate.getTime(),
stateMachineArn: exec.stateMachineArn,
}));
}
// Stop a stuck execution
async function stopExecution(executionArn, reason) {
const cmd = new StopExecutionCommand({
executionArn,
error: 'StuckExecutionStopped',
cause: reason,
});
await sfn.send(cmd);
return { stopped: true, executionArn };
}
Register your Step Functions health check with AliveMCP on a 60-second probe. The check should verify IAM permission to call ListExecutions (tests credential validity), check for executions in RUNNING state older than your SLA threshold (stuck detection), and count FAILED executions in the last hour (regression detection). Alert on stuck executions separately from failed executions — a stuck execution indicates an external dependency that never called back, while a failed execution indicates a logic or infrastructure error.
Related guides
- MCP tools for Temporal — workflow execution queries, signals, and namespace health
- MCP tools for Apache Airflow — DAG run states, task instance hierarchy, and REST API v1
- MCP tools for Prefect — flow run states, work pool health, and deployment triggers
- MCP tools for Dagster — asset materializations, run status, and sensor management
- MCP tools for AWS Lambda — invocation, error classification, and cold start detection
- MCP server health check patterns — composite endpoints and failure classification