Guide · AWS Step Functions

MCP Server Step Functions Express Workflows — StartSyncExecution, CloudWatch Logs, and the 5-minute constraint

EXPRESS Step Functions state machines are optimized for high-throughput, short-duration workflows: they charge per execution duration instead of per state transition, support 100,000 executions per second, and can return output synchronously via StartSyncExecution — a single API call that starts an execution and blocks until it completes, returning the output directly in the response. For an MCP server, this means a multi-step tool chain (validate input → call API → transform output → write to DynamoDB) can be expressed as a state machine and invoked as a single synchronous MCP tool call with no polling loop. The hard constraint is 5 minutes maximum duration — an EXPRESS execution that runs longer than 5 minutes is terminated with States.Timeout. The second constraint is that EXPRESS executions have no built-in history store: GetExecutionHistory throws ExecutionDoesNotExist for EXPRESS executions. All observability must flow through CloudWatch Logs.

TL;DR

Use StartSyncExecution (not StartExecution) to run an EXPRESS workflow and get the output in one call — response contains output, status, error, and cause directly. Configure logging at level: 'ALL' before using EXPRESS in production — without logging, failed Express executions leave no observable trace. Never call GetExecutionHistory on an EXPRESS execution: it throws ExecutionDoesNotExist. For workflows that need >5 minutes, use STANDARD. For activity tasks, use STANDARD — waitForTaskToken on Lambda works with EXPRESS, but Activity resources do not.

StartSyncExecution: synchronous tool chains without a poll loop

StartSyncExecution is available only for EXPRESS state machines. It starts an execution and blocks the HTTP connection until the execution completes (or fails), then returns the final output in the response body. From an MCP server's perspective, invoking an EXPRESS workflow feels identical to calling a Lambda function directly — except the logic is a visual state machine with built-in retries, parallel branches, and error handling.

The response from StartSyncExecution includes: status (SUCCEEDED, FAILED, or TIMED_OUT), output (JSON string of final state output on success), error and cause (on failure). There is no execution ARN to poll — if you need the execution ARN for logging correlation, it is included in the response as executionArn.

import {
  SFNClient,
  StartSyncExecutionCommand,
  StartExecutionCommand,
  DescribeStateMachineCommand,
} from '@aws-sdk/client-sfn';
import { CloudWatchLogsClient, FilterLogEventsCommand } from '@aws-sdk/client-cloudwatch-logs';

const sfn = new SFNClient({ region: process.env.AWS_REGION ?? 'us-east-1' });
const EXPRESS_SM_ARN = process.env.EXPRESS_SM_ARN!;

// Run an EXPRESS workflow synchronously — blocks until completion (max 5 minutes)
async function runExpressWorkflow(input: unknown, executionName?: string) {
  const response = await sfn.send(new StartSyncExecutionCommand({
    stateMachineArn: EXPRESS_SM_ARN,
    input: JSON.stringify(input), // must be a JSON string
    name: executionName, // optional; if set, must be unique within 1 minute per state machine
    // traceHeader: 'Root=1-...;Parent=...;Sampled=1', // optional X-Ray trace header
  }));

  if (response.status === 'FAILED' || response.status === 'TIMED_OUT') {
    throw Object.assign(new Error(response.cause ?? response.error ?? 'Express execution failed'), {
      executionArn: response.executionArn,
      sfnError: response.error,
      sfnCause: response.cause,
      status: response.status,
    });
  }

  return {
    executionArn: response.executionArn,
    output: response.output ? JSON.parse(response.output) : null,
    startDate: response.startDate,
    stopDate: response.stopDate,
    durationMs: response.stopDate && response.startDate
      ? response.stopDate.getTime() - response.startDate.getTime()
      : null,
    billedDurationMs: response.billingDetails?.billedDurationInMilliseconds,
  };
}

// Verify the state machine type before calling type-specific APIs
async function assertStateMachineType(
  stateMachineArn: string,
  expectedType: 'STANDARD' | 'EXPRESS',
): Promise {
  const sm = await sfn.send(new DescribeStateMachineCommand({ stateMachineArn }));
  if (sm.type !== expectedType) {
    throw new Error(
      `Expected ${expectedType} state machine but got ${sm.type}: ${stateMachineArn}. ` +
      `${expectedType === 'EXPRESS'
        ? 'Use StartSyncExecution for EXPRESS machines.'
        : 'Use StartExecution + poll for STANDARD machines.'}`
    );
  }
}

One production trap: StartSyncExecution has a maximum HTTP response timeout of 5 minutes enforced by the client SDK. If the execution approaches the 5-minute wall, the SDK connection may time out before the execution's own TimeoutSeconds triggers. Always set a client-side timeout on the HTTP request slightly longer than the TimeoutSeconds in the state machine definition.

Async EXPRESS executions and CloudWatch Logs querying

Some scenarios require async EXPRESS executions: the state machine is invoked from an EventBridge rule, a Lambda trigger, or another state machine, and the caller does not hold an HTTP connection waiting for the result. In these cases, StartExecution (not StartSyncExecution) starts the EXPRESS execution asynchronously and returns immediately with an execution ARN. There is no way to poll completion via DescribeExecution for EXPRESS — the API exists but throws ExecutionDoesNotExist.

All observability for async EXPRESS executions flows through CloudWatch Logs — which requires that logging was configured on the state machine before the execution ran. Configure logging level ALL to capture every event, or ERROR to capture only failures. Without an explicitly configured logging destination, async EXPRESS executions produce no observable output on failure.

const cwl = new CloudWatchLogsClient({ region: process.env.AWS_REGION ?? 'us-east-1' });

// Log group name is configured at state machine creation time
// Convention: /aws/states/express/{state-machine-name}
const LOG_GROUP_NAME = process.env.EXPRESS_LOG_GROUP!;

// Find log events for a specific execution ARN
// EXPRESS log streams are prefixed with the execution ARN
async function getExpressExecutionLogs(executionArn: string, maxEvents = 100) {
  // The execution ARN is embedded in the log stream name after encoding
  // Filter by the execution ARN suffix (last segment is unique per execution)
  const executionId = executionArn.split(':').pop()!;

  const response = await cwl.send(new FilterLogEventsCommand({
    logGroupName: LOG_GROUP_NAME,
    filterPattern: `"${executionArn}"`, // filter for log events containing this ARN
    limit: maxEvents,
    startTime: Date.now() - 5 * 60 * 1000, // look back 5 minutes max
  }));

  return (response.events ?? []).map(event => {
    try {
      return { timestamp: event.timestamp, data: JSON.parse(event.message ?? '{}') };
    } catch {
      return { timestamp: event.timestamp, raw: event.message };
    }
  });
}

// Extract failure details from EXPRESS CloudWatch Logs events
// Log events for EXPRESS have a 'type' field matching execution history event types
async function getExpressFailureDetails(executionArn: string) {
  const events = await getExpressExecutionLogs(executionArn, 200);

  const failureEvents = events.filter(e =>
    e.data?.type?.includes('Failed') ||
    e.data?.type?.includes('TimedOut') ||
    e.data?.type?.includes('Aborted')
  );

  return failureEvents.map(e => ({
    timestamp: e.timestamp,
    type: e.data?.type,
    error: e.data?.details?.error,
    cause: (() => {
      try { return JSON.parse(e.data?.details?.cause); }
      catch { return e.data?.details?.cause; }
    })(),
  }));
}

EXPRESS vs STANDARD: choosing for MCP tool orchestration

The decision between EXPRESS and STANDARD for MCP tool orchestration reduces to three questions:

Duration: If any tool call chain can take longer than 5 minutes, use STANDARD. EXPRESS is hard-terminated at 5 minutes. STANDARD supports up to 1 year.

Audit and replay: If the tool orchestration needs durable execution history for compliance, debugging, or human review (approval gates, financial transactions, data migrations), use STANDARD. EXPRESS history is transient — it flows only to CloudWatch Logs and is subject to that log group's retention policy.

Throughput and cost: If the tool chain runs thousands of times per minute, EXPRESS pricing (per execution-duration-ms) is dramatically cheaper than STANDARD pricing (per state transition). A 100-state STANDARD execution costs 100 × $0.000025 = $0.0025 per run. A 100-state EXPRESS execution that runs for 500ms costs $0.00001 per run — 250× cheaper at high volume.

DimensionEXPRESSSTANDARD
Max duration5 minutes (hard)1 year
Execution historyCloudWatch Logs onlyBuilt-in, 90-day retention
GetExecutionHistoryThrows ExecutionDoesNotExistSupported, paginable
Activity tasksNot supportedSupported
waitForTaskToken (Lambda)SupportedSupported
Synchronous invocationStartSyncExecutionNot available
Throughput100,000 executions/second2,000 executions/second
Pricing modelPer execution-duration-msPer state transition
IdempotencyNo — same name can be reused after 1 minuteExecution name must be globally unique

Failure modes reference

FailureSymptomFix
GetExecutionHistory called on EXPRESS executionExecutionDoesNotExist error; no history returnedAlways check state machine type before calling type-specific APIs; use CloudWatch Logs for EXPRESS
No logging configured on EXPRESS state machineFailed async executions leave no trace; impossible to debugConfigure logging level ALL at state machine creation; alert on log group missing before first execution
StartSyncExecution timeout vs execution TimeoutSeconds mismatchSDK HTTP connection times out before Step Functions terminates the execution; orphaned execution continues runningSet SDK HTTP timeout to TimeoutSeconds + 10s; ensure TimeoutSeconds < 300s for EXPRESS
Activity tasks used with EXPRESSState machine definition validation error or execution fails immediatelyActivity tasks require STANDARD state machines; use Lambda tasks for EXPRESS
Execution name reuse within 1 minuteExecutionAlreadyExists error on StartSyncExecutionUse UUID for execution name or omit it; EXPRESS names are only unique for 1 minute
DescribeExecution polling for async EXPRESSExecutionDoesNotExist; no completion signal ever receivedFor async EXPRESS, subscribe to EventBridge events for execution status changes or query CloudWatch Logs