Multi-step business processes that span several systems — an approval chain, a data validation and enrichment pipeline, a provisioning workflow that touches five different APIs in sequence — are exactly the workloads that turn into unmaintainable chains of Lambda functions invoking each other directly if there's no explicit orchestration layer. AWS Step Functions exists to be that layer, and the difference between a Step Functions implementation that holds up in production and one that becomes its own maintenance burden comes down to how error handling, retries, and human checkpoints are designed, not whether Step Functions was the right tool to reach for.
The State Machine as the Source of Truth for Process Logic
{
"Comment": "Multi-step approval and provisioning workflow",
"StartAt": "ValidateRequest",
"States": {
"ValidateRequest": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:validate-request",
"Retry": [
{
"ErrorEquals": ["Lambda.ServiceException", "Lambda.TooManyRequestsException"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["ValidationError"],
"ResultPath": "$.error",
"Next": "RejectRequest"
}
],
"Next": "RequiresApproval"
},
"RequiresApproval": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.requestValue",
"NumericGreaterThan": 10000,
"Next": "WaitForHumanApproval"
}
],
"Default": "ProvisionResources"
},
"WaitForHumanApproval": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke.waitForTaskToken",
"Parameters": {
"FunctionName": "request-approval-notification",
"Payload": {
"taskToken.$": "$$.Task.Token",
"request.$": "$"
}
},
"TimeoutSeconds": 259200,
"Next": "ProvisionResources"
},
"ProvisionResources": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:provision-resources",
"Next": "NotifyComplete"
},
"RejectRequest": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:notify-rejection",
"End": true
},
"NotifyComplete": {
"Type": "Task",
"Resource": "arn:aws:lambda:us-east-1:123456789012:function:notify-completion",
"End": true
}
}
}
Two design decisions here matter more than the individual states. First, the waitForTaskToken pattern in WaitForHumanApproval — Step Functions pauses execution entirely (at no compute cost while waiting) until an external system calls back with the task token, which is the correct pattern for human-in-the-loop steps instead of a polling Lambda that checks an approval status repeatedly. Second, the Retry block on ValidateRequest with exponential backoff (BackoffRate: 2.0) handles transient service errors without manual intervention, while the Catch block routes genuine validation failures (not transient errors) to an explicit rejection path rather than letting them retry uselessly against a request that will never become valid.
Error Handling Design Is Where Most Step Functions Builds Fall Short
A state machine with no Retry or Catch blocks looks identical to one with them in the happy-path demo and behaves completely differently the first time a downstream service has a transient blip. The distinction that matters in a production build:
- Retry transient, likely-to-succeed-on-retry errors (throttling, timeout, temporary service unavailability) with exponential backoff and a capped max-attempts
- Catch errors that won't be fixed by retrying (validation failures, business logic rejections, permanent downstream errors) and route them to an explicit failure/compensation path rather than letting the retry policy exhaust attempts against something that was never going to succeed
- Never conflate the two — retrying a validation error wastes time and obscures the real failure; failing immediately on a transient error creates unnecessary manual intervention for something that would have self-resolved
Observability: Step Functions' Execution History Is a Feature, Not an Afterthought
Every state transition, input, and output in a Step Functions execution is retained in its execution history by default, which gives a debugging and audit capability that a chain of directly-invoking Lambda functions doesn't provide out of the box — reconstructing exactly what happened in a failed execution, including the precise input that caused a failure and every retry attempt along the way, without needing custom logging built into every function.
# Pull full execution history for debugging or audit purposes
aws stepfunctions get-execution-history \
--execution-arn "arn:aws:states:us-east-1:123456789012:execution:approval-workflow:abc123" \
--query 'events[*].[timestamp,type]'
This execution history is also directly useful compliance evidence for any workflow that includes an approval step — it shows exactly when the approval was requested, how long it waited, and what decision was recorded, without needing a separate audit log built specifically for that purpose.
Frequently Asked Questions
When does a workflow justify Step Functions over a simpler direct Lambda-to-Lambda chain?
Once a process has more than two or three sequential steps, needs conditional branching, requires a human approval or long-wait step, or needs reliable error handling and retry logic distinct per step, the orchestration and observability Step Functions provides outweighs the added architectural component — below that complexity threshold, direct invocation is often simpler and sufficient.
How much does Step Functions cost compared to the same logic in Lambda alone?
Standard Step Functions workflows bill per state transition, which adds a real but usually small cost on top of the underlying Lambda invocations. Express Workflows offer a different pricing model (by duration and memory, similar to Lambda) better suited to high-volume, short-duration workflows where per-transition billing would add up quickly.
Can Step Functions call non-AWS services directly?
Yes, via HTTP task integrations that call external HTTPS endpoints directly from the state machine without needing a Lambda function as an intermediary for simple API calls — useful for straightforward external API integrations where a full Lambda wrapper adds unnecessary overhead.
What happens if the human approval step times out with no response?
The TimeoutSeconds field defines the maximum wait, and the state machine should have an explicit path for the timeout case (routing to an escalation or auto-rejection state) rather than leaving the execution to fail silently — this needs to be designed as its own branch in the state machine, not left as an unhandled edge case.
Is Step Functions execution history sufficient for full compliance audit evidence?
It's strong evidence for what happened within the orchestrated workflow itself, but it should be paired with domain-specific evidence (the actual data validated, the specific approver identity, business context) rather than relied on as the sole audit artifact — Step Functions shows you the process executed correctly; it doesn't independently validate that the business decision made within it was correct.
Rutagon builds production Step Functions and event-driven AWS architectures — the orchestration layer that turns a chain of scripts into a reliable, observable, auditable workflow.
Discuss your project → rutagon.com/contact | 907-841-8407 | contact@rutagon.com
Related reading: Custom AWS Serverless Data Pipeline: What We Built · Custom AWS Event-Driven Integrations · AWS Cloud Infrastructure Capability
External reference: AWS Step Functions Developer Guide