AWS Step Functions for Data Engineers
Standard vs Express, state types, a full ETL state machine, .sync and callback integrations, data passing, Map and Distributed Map, error handling, triggering and monitoring.
1. What AWS Step Functions Is
AWS Step Functions orchestrates workflows as state machines defined in JSON (Amazon States Language, ASL). It calls AWS services directly (Glue, EMR, Athena, Lambda, ECS, Batch, SNS, DynamoDB and 200+ others), waits for them, retries failures, and shows each execution visually. It is serverless: no scheduler to run.
| Standard workflow | Express workflow | |
|---|---|---|
| Maximum duration | Up to 1 year | Up to 5 minutes |
| Execution semantics | Exactly-once state transitions | At-least-once (async) / at-most-once (sync) |
| History | Full visual history in the console | Logs in CloudWatch |
| Pricing | Per state transition | Per request and duration |
| Use for | Data pipelines, long jobs, human approval | High-volume, short event processing |
2. State Types
| State | Purpose |
|---|---|
Task | Do work: call a service API or an activity |
Choice | Branch on input values |
Parallel | Run fixed branches at the same time, then join |
Map | Run the same steps for every item in a list (inline) or millions of S3 objects (distributed) |
Wait | Pause for a time or until a timestamp |
Pass | Transform or inject data, or act as a placeholder |
Succeed / Fail | End the execution |
3. Example: A Daily ETL Pipeline
Validate the file with Lambda, run a Glue job and wait for it, check data quality, refresh Athena partitions, and notify. Every failure goes to one alert state.
{
"Comment": "Daily transactions load",
"StartAt": "ValidateFile",
"States": {
"ValidateFile": {
"Type": "Task",
"Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "file-validator", "Payload.$": "$" },
"ResultSelector": { "rows.$": "$.Payload.rows", "valid.$": "$.Payload.valid" },
"ResultPath": "$.validation",
"Next": "IsValid",
"Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "NotifyFailure" }]
},
"IsValid": {
"Type": "Choice",
"Choices": [{ "Variable": "$.validation.valid", "BooleanEquals": true, "Next": "RunGlueJob" }],
"Default": "NotifyFailure"
},
"RunGlueJob": {
"Type": "Task",
"Resource": "arn:aws:states:::glue:startJobRun.sync",
"Parameters": {
"JobName": "raw-to-silver-transactions",
"Arguments": { "--run_date.$": "$.run_date" }
},
"ResultPath": "$.glue",
"Retry": [{
"ErrorEquals": ["Glue.ConcurrentRunsExceededException", "States.TaskFailed"],
"IntervalSeconds": 60, "MaxAttempts": 2, "BackoffRate": 2.0
}],
"Catch": [{ "ErrorEquals": ["States.ALL"], "ResultPath": "$.error", "Next": "NotifyFailure" }],
"Next": "AddPartition"
},
"AddPartition": {
"Type": "Task",
"Resource": "arn:aws:states:::athena:startQueryExecution.sync",
"Parameters": {
"QueryString.$": "States.Format('ALTER TABLE silver.transactions ADD IF NOT EXISTS PARTITION (dt = \\'{}\\')', $.run_date)",
"WorkGroup": "data-eng"
},
"ResultPath": null,
"Next": "NotifySuccess"
},
"NotifySuccess": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn": "arn:aws:sns:ap-south-1:111122223333:pipeline-events",
"Message.$": "States.Format('Loaded {} rows for {}', $.validation.rows, $.run_date)"
},
"End": true
},
"NotifyFailure": {
"Type": "Task",
"Resource": "arn:aws:states:::sns:publish",
"Parameters": {
"TopicArn": "arn:aws:sns:ap-south-1:111122223333:pipeline-alerts",
"Message.$": "States.JsonToString($)"
},
"Next": "Failed"
},
"Failed": { "Type": "Fail", "Error": "PipelineFailed" }
}
}
4. Service Integration Patterns
| Pattern | Resource suffix | Behaviour |
|---|---|---|
| Request-response | (none), e.g. arn:aws:states:::glue:startJobRun | Call the API and move on immediately |
| Run a job | .sync | Start the job and wait until it finishes; fail the state if the job fails |
| Callback | .waitForTaskToken | Pass a task token to something external (a human approval, an on-prem system); the workflow waits until it calls SendTaskSuccess/Failure |
# a worker (or approval UI) completes a waiting callback task
import boto3, json
sfn = boto3.client("stepfunctions")
sfn.send_task_success(taskToken=token, output=json.dumps({"approved": True, "by": "risk-team"}))
# or: sfn.send_task_failure(taskToken=token, error="Rejected", cause="Totals do not reconcile")
5. Passing Data Between States
With the classic JSONPath query language, each state filters its input and output in a fixed order:
state input
→ InputPath select part of the input "$.order"
→ Parameters build the API request {"JobName": "x", "Arguments": {"--id.$": "$.id"}}
→ (task runs)
→ ResultSelector keep only useful result fields {"status.$": "$.JobRunState"}
→ ResultPath where to put the result in input "$.glue" (null = discard the result)
→ OutputPath select what passes to next state "$"
- Keys ending in
.$are evaluated as paths or intrinsic functions; others are literal values. - Intrinsic functions:
States.Format,States.StringToJson,States.JsonToString,States.Array,States.MathAdd,States.UUIDand more. - Workflows can instead use JSONata (
"QueryLanguage": "JSONata"), withArgumentsandOutputfields and expressions in{% … %}, plus variables that later states can read. - State payloads are limited to 256 KB: pass S3 keys, not data.
6. Map: Fan-Out over Many Items
"LoadEachTable": {
"Type": "Map",
"ItemsPath": "$.tables",
"MaxConcurrency": 5,
"ItemSelector": { "table.$": "$$.Map.Item.Value", "run_date.$": "$.run_date" },
"ItemProcessor": {
"ProcessorConfig": { "Mode": "INLINE" },
"StartAt": "LoadTable",
"States": {
"LoadTable": {
"Type": "Task",
"Resource": "arn:aws:states:::glue:startJobRun.sync",
"Parameters": { "JobName": "generic-table-load",
"Arguments": { "--table.$": "$.table", "--run_date.$": "$.run_date" } },
"End": true
}
}
},
"Next": "Done"
}
Distributed Map processes very large datasets: it reads items straight from S3 (a prefix listing, CSV, JSON lines or an S3 Inventory manifest) and runs up to thousands of child workflows in parallel:
"ProcessAllFiles": {
"Type": "Map",
"ItemReader": {
"Resource": "arn:aws:states:::s3:listObjectsV2",
"Parameters": { "Bucket": "acme-lake-prod", "Prefix": "raw/backfill/" }
},
"MaxConcurrency": 500,
"ToleratedFailurePercentage": 1,
"ItemBatcher": { "MaxItemsPerBatch": 50 },
"ItemProcessor": {
"ProcessorConfig": { "Mode": "DISTRIBUTED", "ExecutionType": "EXPRESS" },
"StartAt": "ConvertBatch",
"States": {
"ConvertBatch": { "Type": "Task", "Resource": "arn:aws:states:::lambda:invoke",
"Parameters": { "FunctionName": "convert-to-parquet", "Payload.$": "$" }, "End": true }
}
},
"ResultWriter": { "Resource": "arn:aws:states:::s3:putObject",
"Parameters": { "Bucket": "acme-lake-prod", "Prefix": "_runs/backfill/" } },
"End": true
}
7. Error Handling
| Field | Meaning |
|---|---|
ErrorEquals | Which errors the rule matches: service errors, States.TaskFailed, States.Timeout, States.ALL |
IntervalSeconds / BackoffRate | First wait and its multiplier (e.g. 60, 120, 240 seconds) |
MaxAttempts | Retries after the first attempt (0 = never retry) |
MaxDelaySeconds / JitterStrategy | Cap the wait and add randomness to avoid retry storms |
Catch + ResultPath | Route to a fallback state, keeping the original input and adding the error under a path |
TimeoutSeconds / HeartbeatSeconds | Fail a task that runs too long or stops reporting |
Order matters: Retry rules are tried first, and Catch runs only after retries are exhausted. Put specific errors before States.ALL.
8. Triggering, Testing & Monitoring
aws stepfunctions start-execution \
--state-machine-arn arn:aws:states:ap-south-1:111122223333:stateMachine:daily-load \
--name load-2026-10-26 \
--input '{"run_date": "2026-10-26", "bucket": "acme-lake-prod", "key": "raw/txn/2026-10-26.csv.gz"}'
aws stepfunctions describe-execution --execution-arn <execution-arn>
aws stepfunctions list-executions --state-machine-arn <arn> --status-filter FAILED
- Schedules: EventBridge Scheduler (cron with time zones) targeting the state machine. Events: EventBridge rules on S3 Object Created, Glue job state changes, or custom events.
- Execution names must be unique for 90 days in Standard workflows: use them as an idempotency key (e.g.
load-2026-10-26). - Test a single state with the TestState API before deploying; use versions and aliases for safe rollouts.
- Alarm on
ExecutionsFailedandExecutionsTimedOut, and send failure events from EventBridge to SNS or chat.
| Choose | When |
|---|---|
| Step Functions | AWS-native, event-driven pipelines, serverless, strong retries and visual history |
| Amazon MWAA (Airflow) | Complex schedules and backfills, many non-AWS systems, existing Airflow skills |
| Glue workflows | Simple chains of Glue jobs and crawlers only |
9. Common Interview Questions
| Question | Short answer |
|---|---|
| Standard vs Express? | Standard: up to a year, exactly-once transitions, full history, priced per transition. Express: up to 5 minutes, high volume, at-least-once, priced per request and duration |
| How does Step Functions wait for a Glue job? | With the .sync integration (glue:startJobRun.sync), which starts the job and waits for its final state |
| What is the callback pattern? | .waitForTaskToken passes a token to an external process; the workflow pauses until SendTaskSuccess or SendTaskFailure is called with that token |
| How do you process a million S3 files? | Distributed Map reading from S3, batching items, high MaxConcurrency, Express child workflows, and a tolerated failure percentage |
| How do you avoid huge payloads between states? | Pass references (S3 keys, IDs) instead of data; state input/output is limited to 256 KB |
| Retry vs Catch? | Retry re-attempts the same state with backoff; Catch routes to another state after retries are exhausted |