AWS Step Functions just had its biggest year of integrations since launch. In March 2026, AWS added 28 new service integrations and more than 1,100 new API actions to Step Functions, covering everything from Amazon Bedrock AgentCore to Amazon S3 Vectors. Then in June 2026, AWS shipped a preview integration that lets a state machine call an AI agent as a workflow step. If you last touched Step Functions a year ago, the service you remember is not the service that exists today.
This tutorial walks through deploying a production-style AWS Step Functions workflow using Terraform 1.15.9, the current stable release as of August 19, 2026, together with the official HashiCorp AWS provider. You’ll build a state machine from scratch, wire it to Lambda, test it locally with Step Functions Local before touching real AWS resources, add retry and error handling, and deploy it with a repeatable Terraform pipeline instead of clicking through the console. By the end you’ll have a complete working project you can drop into any AWS account.
Don't miss new tech stories on Google
Add Tech Insider once in the Google app and our stories appear in your news suggestions.
What Are AWS Step Functions, and Why Terraform?
AWS Step Functions is a serverless orchestration service that coordinates multiple AWS services into workflows using visual state machines defined in Amazon States Language (ASL), a JSON-based format. Instead of writing glue code to sequence Lambda calls, retry failed steps, and handle branching logic, you describe the workflow declaratively and Step Functions runs it, retries it, and logs every transition automatically.
The console works fine for a five-minute demo. It falls apart the moment you need the same workflow in three environments (dev, staging, prod), need code review on workflow changes, or need to roll back a bad deployment. Terraform solves that by treating the state machine, its IAM role, and every dependency as version-controlled code. AWS itself now documents this pattern directly: the official Step Functions Terraform guide was published in AWS’s docs in early 2026 and lays out the exact prerequisites and workflow this article follows, per AWS’s own Terraform deployment guide.
Search interest backs this up. “step function aws” pulls roughly 4,400 monthly US searches with low keyword competition, according to DataForSEO data pulled in August 2026, which tells you a lot of engineers are hitting this exact wall right now: they know Step Functions is the right tool, but the console-first tutorials online don’t show them how to manage it as code.
Understanding Step Functions State Types Before You Write ASL
Every state in Amazon States Language has a Type field, and picking the wrong type is the fastest way to write a workflow that looks correct in the console but behaves oddly at scale. Seven types cover almost every real workflow, and knowing what each one is actually for saves you from reaching for a Lambda function to do something ASL already handles natively.
- Task — invokes work outside the state machine: a Lambda function, an SDK integration like the AgentCore call shown later in this article, an ECS task, or a Glue job. This is the only state type that does real work; everything else is control flow.
- Choice — branches based on the input, similar to an if/else chain. Used in Step 5 of this tutorial to route orders to review or auto-confirmation.
- Parallel — runs a fixed, known number of branches at the same time and waits for all of them to finish. Good for “fetch inventory, fetch pricing, and fetch fraud score simultaneously” patterns.
- Map — iterates over an array, running the same sub-workflow for each item. Standard Map processes up to 40 concurrent iterations in-line; Distributed Map (used for the Athena/Parquet pattern mentioned later) can fan out to tens of thousands of parallel executions for genuinely large datasets.
- Wait — pauses the execution for a fixed duration or until a specific timestamp, without consuming compute the way a sleeping Lambda would.
- Pass — passes input to output, optionally transforming it, without calling any external resource. Useful for testing branch logic before wiring in real Task states, exactly as this tutorial does in Step 8.
- Succeed / Fail — terminal states that end the execution successfully or with an explicit error, both used in the order workflow ASL above.
A common mistake is reaching for a Parallel state when Map fits better, or vice versa. Parallel is for a small, fixed set of different operations running together. Map is for running the identical operation across a variable-length list. Mixing the two up produces a technically working but hard-to-read state machine that’s painful to modify six months later.
Prerequisites: What You Need Before You Start
Get these installed and configured before Step 1. Skipping any of these is the single biggest reason this tutorial breaks for people halfway through.
- Terraform 1.15.9 or later (released August 19, 2026) — earlier 1.14.x versions work but lack the dynamic module sources feature used later in this guide
- AWS CLI v2, configured with an IAM user or role that has permissions for Step Functions, IAM, Lambda, and CloudWatch Logs
- An AWS account with billing enabled (Step Functions has a free tier of 4,000 state transitions/month, then charges per transition)
- Docker Desktop or Docker Engine, for running Step Functions Local
- AWS SAM CLI, used to pull and run the Step Functions Local Docker image
- Node.js 20.x or later, for the sample Lambda function used in this tutorial (Python 3.13 works identically if you prefer)
- A code editor — VS Code with the AWS Toolkit extension gives you inline ASL validation, which saves real debugging time
- Basic familiarity with JSON and IAM policy syntax
Verify your Terraform version before moving on:
terraform version
# Terraform v1.15.9
# on linux_amd64
aws --version
# aws-cli/2.27.x Python/3.12.x Linux/6.8.x
If your Terraform version reports anything below 1.14, upgrade first. HashiCorp ships frequent patch releases, and the AWS provider blocks used in Step 5 assume Terraform 1.14+ syntax for dynamic blocks.
Step 1: Bootstrap Your Terraform Project
Create a clean project directory with a standard Terraform layout. Keeping the state machine definition in its own file separate from the Terraform resource blocks makes both easier to review and lets non-Terraform engineers edit the workflow logic without touching infrastructure code.
mkdir aws-step-functions-terraform && cd aws-step-functions-terraform
mkdir -p statemachine lambda
touch main.tf variables.tf outputs.tf provider.tf
touch statemachine/workflow.asl.json
touch lambda/index.js
terraform init
This mirrors the structure AWS recommends in its own documentation: bootstrap the Terraform project with terraform init, prototype the workflow logic separately, then wire it into Terraform resource blocks once it works.
Step 2: Configure the AWS Provider
Pin the AWS provider version. Floating on “latest” is convenient until a provider update silently changes default behavior on a production apply — pin it and bump it deliberately.
# provider.tf
terraform {
required_version = ">= 1.15.0"
required_providers {
aws = {
source = "hashicorp/aws"
version = "~> 6.5"
}
}
}
provider "aws" {
region = var.aws_region
}
# variables.tf
variable "aws_region" {
description = "AWS region to deploy into"
type = string
default = "us-east-1"
}
variable "environment" {
description = "Deployment environment tag"
type = string
default = "dev"
}
Step 3: Write a Sample Lambda Function
Your state machine needs at least one task to orchestrate. This tutorial uses a minimal order-processing workflow: validate an order, check inventory, then either confirm or flag it for manual review. Start with a single Lambda function that simulates the validation step.
// lambda/index.js
exports.handler = async (event) => {
const { orderId, amount } = event;
if (!orderId || typeof amount !== "number") {
throw new Error("InvalidOrderPayload");
}
const isHighValue = amount > 5000;
return {
orderId,
amount,
validated: true,
requiresReview: isHighValue,
};
};
Zip it for deployment:
cd lambda && zip function.zip index.js && cd ..
Step 4: Define IAM Roles in Terraform
Step Functions needs an execution role that can invoke your Lambda functions and write logs. This is the step most tutorials rush through, and it’s the source of the most common failure (covered in the pitfalls section below): an IAM role with a trust policy for the wrong service principal.
# main.tf (IAM section)
resource "aws_iam_role" "sfn_execution_role" {
name = "step-functions-order-workflow-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = {
Service = "states.amazonaws.com"
}
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy" "sfn_lambda_invoke" {
name = "invoke-lambda-policy"
role = aws_iam_role.sfn_execution_role.id
policy = jsonencode({
Version = "2012-10-17"
Statement = [
{
Effect = "Allow"
Action = "lambda:InvokeFunction"
Resource = aws_lambda_function.order_validator.arn
},
{
Effect = "Allow"
Action = [
"logs:CreateLogDelivery",
"logs:GetLogDelivery",
"logs:UpdateLogDelivery",
"logs:DeleteLogDelivery",
"logs:ListLogDeliveries",
"logs:PutResourcePolicy",
"logs:DescribeResourcePolicies",
"logs:DescribeLogGroups"
]
Resource = "*"
}
]
})
}
Add the Lambda execution role and function resource alongside it:
resource "aws_iam_role" "lambda_exec_role" {
name = "order-validator-lambda-role-${var.environment}"
assume_role_policy = jsonencode({
Version = "2012-10-17"
Statement = [{
Effect = "Allow"
Principal = { Service = "lambda.amazonaws.com" }
Action = "sts:AssumeRole"
}]
})
}
resource "aws_iam_role_policy_attachment" "lambda_basic_logs" {
role = aws_iam_role.lambda_exec_role.name
policy_arn = "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
}
resource "aws_lambda_function" "order_validator" {
function_name = "order-validator-${var.environment}"
filename = "lambda/function.zip"
source_code_hash = filebase64sha256("lambda/function.zip")
handler = "index.handler"
runtime = "nodejs20.x"
role = aws_iam_role.lambda_exec_role.arn
timeout = 10
}
Step 5: Write the State Machine Definition (ASL)
This is the heart of the workflow. The Amazon States Language file below validates an order, branches based on whether it needs manual review, and includes retry logic for transient Lambda failures — the kind of resilience you’d otherwise have to hand-roll in application code.
{
"Comment": "Order processing workflow with review branch",
"StartAt": "ValidateOrder",
"States": {
"ValidateOrder": {
"Type": "Task",
"Resource": "${lambda_arn}",
"Retry": [
{
"ErrorEquals": ["Lambda.ServiceException", "Lambda.TooManyRequestsException"],
"IntervalSeconds": 2,
"MaxAttempts": 3,
"BackoffRate": 2.0
}
],
"Catch": [
{
"ErrorEquals": ["States.ALL"],
"ResultPath": "$.error",
"Next": "OrderFailed"
}
],
"Next": "CheckReviewRequired"
},
"CheckReviewRequired": {
"Type": "Choice",
"Choices": [
{
"Variable": "$.requiresReview",
"BooleanEquals": true,
"Next": "FlagForReview"
}
],
"Default": "OrderConfirmed"
},
"FlagForReview": {
"Type": "Pass",
"Result": "Order flagged for manual review",
"End": true
},
"OrderConfirmed": {
"Type": "Pass",
"Result": "Order confirmed automatically",
"End": true
},
"OrderFailed": {
"Type": "Fail",
"Error": "OrderValidationFailed",
"Cause": "Order could not be validated"
}
}
}
Save this as statemachine/workflow.asl.json. The ${lambda_arn} placeholder gets interpolated by Terraform in the next step using templatefile(), so the ASL file stays reusable across environments without hardcoded ARNs.
Step 6: Define the Step Functions Resource in Terraform
Now wire the ASL definition into an actual aws_sfn_state_machine resource, with logging enabled so you can debug executions from CloudWatch instead of guessing.
resource "aws_cloudwatch_log_group" "sfn_logs" {
name = "/aws/vendedlogs/states/order-workflow-${var.environment}"
retention_in_days = 30
}
resource "aws_sfn_state_machine" "order_workflow" {
name = "order-processing-workflow-${var.environment}"
role_arn = aws_iam_role.sfn_execution_role.arn
definition = templatefile("${path.module}/statemachine/workflow.asl.json", {
lambda_arn = aws_lambda_function.order_validator.arn
})
logging_configuration {
log_destination = "${aws_cloudwatch_log_group.sfn_logs.arn}:*"
include_execution_data = true
level = "ALL"
}
tracing_configuration {
enabled = true
}
}
The tracing_configuration block turns on AWS X-Ray, which becomes essential once your workflow has more than three or four states and you need to see exactly where execution time is going.
Step 7: Add Outputs for Verification
Add outputs so you can grab the state machine ARN without digging through the console after every apply.
# outputs.tf
output "state_machine_arn" {
value = aws_sfn_state_machine.order_workflow.arn
}
output "lambda_function_arn" {
value = aws_lambda_function.order_validator.arn
}
Step 8: Test Locally With Step Functions Local Before Deploying
Don’t skip this step to save time — it costs you more time later. Step Functions Local runs a Docker container that emulates the state machine engine, letting you validate your ASL logic without touching real AWS resources or paying for state transitions.
docker run -p 8083:8083 amazon/aws-stepfunctions-local
# In a second terminal, create the state machine locally
aws stepfunctions create-state-machine \
--endpoint-url http://localhost:8083 \
--name "order-workflow-local-test" \
--definition file://statemachine/workflow.asl.json \
--role-arn "arn:aws:iam::123456789012:role/DummyRole"
# Start an execution with test input
aws stepfunctions start-execution \
--endpoint-url http://localhost:8083 \
--state-machine-arn "arn:aws:states:us-east-1:123456789012:stateMachine:order-workflow-local-test" \
--input '{"orderId": "ORD-1001", "amount": 250}'
Step Functions Local doesn’t actually invoke Lambda — it needs a mock configuration file to simulate task responses. For quick logic checks, replace the Task states temporarily with Pass states carrying sample data, run the local test, then swap back before deploying. This catches Choice-state routing bugs and JSON path typos before they cost you a real deployment cycle.
Step 9: Run terraform plan and Review Every Change
With local logic validated, move to a real plan against your AWS account.
terraform plan -out=tfplan
# Expected output tail:
# Plan: 6 to add, 0 to change, 0 to destroy.
#
# Changes to Outputs:
# + lambda_function_arn = (known after apply)
# + state_machine_arn = (known after apply)
Read the full plan output, not just the summary line. Specifically check that the IAM role’s assume_role_policy shows states.amazonaws.com as the principal — a typo here is the most common deploy-time failure, covered below.
Step 10: Deploy With terraform apply
Apply the saved plan so there’s no drift between what you reviewed and what gets deployed.
terraform apply tfplan
# Expected output tail:
# aws_sfn_state_machine.order_workflow: Creation complete after 3s
# [id=arn:aws:states:us-east-1:123456789012:stateMachine:order-processing-workflow-dev]
#
# Apply complete! Resources: 6 added, 0 changed, 0 destroyed.
#
# Outputs:
# lambda_function_arn = "arn:aws:lambda:us-east-1:123456789012:function:order-validator-dev"
# state_machine_arn = "arn:aws:states:us-east-1:123456789012:stateMachine:order-processing-workflow-dev"
Step 11: Trigger and Verify a Real Execution
Start a real execution against the deployed state machine and confirm it completes as expected.
ARN=$(terraform output -raw state_machine_arn)
aws stepfunctions start-execution \
--state-machine-arn "$ARN" \
--input '{"orderId": "ORD-2002", "amount": 7500}'
# Check the result
aws stepfunctions describe-execution \
--execution-arn "arn:aws:states:us-east-1:123456789012:execution:order-processing-workflow-dev:xxxxxxxx" \
--query "{status: status, output: output}"
# Expected output for a $7,500 order (above the $5,000 review threshold):
# {
# "status": "SUCCEEDED",
# "output": "\"Order flagged for manual review\""
# }
Run it again with "amount": 250 and confirm you get "Order confirmed automatically" instead — that’s your Choice state branching correctly on live infrastructure.
Step 12: Set Up CloudWatch Monitoring and Alarms
A workflow with no alerting is a workflow you find out about when a customer complains. Add a CloudWatch alarm on execution failures as the final piece of the Terraform config.
resource "aws_cloudwatch_metric_alarm" "sfn_failures" {
alarm_name = "order-workflow-failures-${var.environment}"
comparison_operator = "GreaterThanThreshold"
evaluation_periods = 1
metric_name = "ExecutionsFailed"
namespace = "AWS/States"
period = 300
statistic = "Sum"
threshold = 0
alarm_description = "Triggers when any order workflow execution fails"
dimensions = {
StateMachineArn = aws_sfn_state_machine.order_workflow.arn
}
}
Run terraform apply one more time to add the alarm. Wire it to an SNS topic and email/Slack subscription if you want a page instead of a silent failure — that’s a one-resource addition once the alarm exists.
What’s New in AWS Step Functions in 2026
Step Functions has changed more in the past six months than in the two years before it, according to AWS’s own recent feature launches log and documentation history. Four releases matter for anyone building workflows right now:
| Release date | What shipped | Why it matters |
|---|---|---|
| March 26, 2026 | 28 new SDK service integrations, 1,100+ new API actions | Direct integrations with Amazon Bedrock AgentCore, Amazon S3 Vectors, and AWS Billing and Cost Management Dashboards without custom Lambda glue |
| June 3, 2026 | AgentCore-powered agentic reasoning steps (preview) | A state machine can now call a managed AI agent harness directly as a task type |
| September 18, 2025 | Distributed Map expands data sources, adds observability | Distributed Map now reads Athena manifests and Parquet files, with better per-item execution visibility |
| March 2026 | Official Terraform deployment guide published | AWS now documents the Terraform + Step Functions Local workflow this article follows, rather than leaving it to third-party blogs |
The AgentCore integration is worth calling out specifically. Per AWS’s June 2026 announcement, Step Functions can now add AI agent reasoning steps to a workflow through an optimized integration with the managed harness in Amazon Bedrock AgentCore, currently in preview. In practice, that means a state in your ASL definition can hand off to an AI agent for a reasoning task — document classification, anomaly triage, natural-language routing — and get a structured result back into the workflow, without you managing the agent runtime yourself.
Adding Amazon Bedrock AgentCore as a Workflow Step
If you want to extend the order workflow above with an AI-driven review step instead of the static amount > 5000 threshold, add a Task state that targets the AgentCore SDK integration. This is a preview feature, so treat it as directional rather than something to hard-commit to production today:
"InvokeReviewAgent": {
"Type": "Task",
"Resource": "arn:aws:states:::aws-sdk:bedrockagentcore:invokeAgentRuntime",
"Parameters": {
"AgentRuntimeArn": "${agent_runtime_arn}",
"Payload": {
"orderId.$": "$.orderId",
"amount.$": "$.amount"
}
},
"Next": "CheckReviewRequired"
}
This replaces the hardcoded threshold logic in the Lambda function with a reasoning step that can weigh multiple signals — order history, customer tier, fraud score — the same way a human reviewer would, without you standing up separate agent infrastructure.
5 Common Pitfalls When Deploying Step Functions With Terraform
1. Wrong trust policy principal on the execution role. The IAM role’s assume_role_policy must trust states.amazonaws.com, not lambda.amazonaws.com or your own account. Copy-pasting a Lambda role as a starting point is the single most common cause of an AccessDeniedException on the first execution.
2. Forgetting source_code_hash on the Lambda resource. Without it, Terraform won’t detect that your zip file changed and will skip redeploying the function even after you edit the code. Always pair filename with filebase64sha256().
3. Hardcoding ARNs in the ASL JSON file. This breaks the moment you deploy to a second environment or account. Use templatefile() to interpolate ARNs at apply time, as shown in Step 6, so the same ASL file works across dev, staging, and prod.
4. No retry logic on Task states. Lambda throttling and transient service exceptions are normal at scale. A state machine without a Retry block fails permanently on the first blip instead of recovering automatically.
5. Testing only in the console, then deploying blind with Terraform. A workflow that works when you click “Start Execution” in the console can still fail on `terraform apply` if the templated ARNs don’t resolve correctly. Always run a real terraform plan and inspect the rendered definition before applying.
Advanced Tips for Production Workflows
Once the basic workflow is running, these patterns separate a demo from something you’d trust in production:
- Use Express workflows for high-volume, short-duration tasks. Standard workflows are billed per state transition and durable for up to a year; Express workflows are billed per execution duration and built for high-throughput, sub-5-minute processes like event stream processing.
- Separate the ASL definition into reusable modules using Terraform’s dynamic module sources, a feature that shipped in Terraform 1.15 in April 2026 — this lets you version a state machine template independently from the environment-specific IAM and Lambda wiring.
- Use Distributed Map for fan-out over large datasets. The September 2025 update added support for reading directly from Athena manifests and Parquet files, so you can iterate over millions of records without staging them in a different format first.
- Enable X-Ray tracing from day one, not after your first production incident. Retrofitting tracing onto a live workflow means you lose historical visibility into exactly the failures you’re trying to diagnose.
- Version your state machine name with a suffix tied to your Terraform workspace (as done in this tutorial with
${var.environment}) so dev, staging, and prod never collide in the same account.
Troubleshooting Common Errors
Error: “AccessDeniedException” on execution start. Your Step Functions execution role lacks permission to invoke the target Lambda. Confirm the aws_iam_role_policy resource attached to the execution role includes lambda:InvokeFunction scoped to the correct function ARN.
Error: “InvalidDefinition: Invalid State Machine Definition.” Your ASL JSON has a syntax error or references a state name that doesn’t exist in a Next field. Validate the JSON independently with jq . statemachine/workflow.asl.json before running terraform plan.
Error: Terraform apply hangs on the Lambda resource. Usually caused by a zip file path that’s wrong relative to your working directory. Run ls -la lambda/function.zip to confirm the file exists exactly where filename in your Terraform config expects it.
Step Functions Local container exits immediately. Check that port 8083 isn’t already bound by another process (lsof -i :8083 on Mac/Linux). Also confirm Docker has at least 2GB of memory allocated, since the local emulator is a full JVM process.
Execution succeeds but output doesn’t match expectations. Check your ResultPath and OutputPath fields in each state — a missing ResultPath replaces the entire input with the task’s output instead of merging it, which silently drops fields later states depend on.
CloudWatch Logs group not receiving entries. The log delivery permissions (logs:CreateLogDelivery and related actions in Step 4) are separate from standard CloudWatch write permissions and are easy to miss. Re-check the IAM policy against the block in Step 4 exactly.
“ResourceNotReady” errors on first apply. IAM roles sometimes take a few seconds to propagate across AWS regions after creation. Add a depends_on = [aws_iam_role_policy.sfn_lambda_invoke] to the state machine resource to force Terraform to wait for the policy attachment before creating the state machine.
Terraform plan shows the state machine being replaced on every apply. This usually means the rendered ASL JSON from templatefile() has non-deterministic key ordering. Use jsonencode() around the templated output, or keep the ASL file’s key order stable, since Step Functions treats the definition as a plain string for diffing purposes.
What Step Functions Actually Costs at Different Volumes
Pricing surprises are a common reason teams abandon Step Functions after a proof of concept. The free tier covers 4,000 state transitions per month for Standard workflows, and after that you pay per transition, not per execution — a five-state workflow that runs once counts as five transitions, not one. Express workflows are priced differently: by execution duration and requests, similar to Lambda, which makes them dramatically cheaper for short, high-frequency workflows.
| Workflow volume | Standard workflow (per-transition) | Express workflow (per-duration) | Best fit |
|---|---|---|---|
| 10,000 executions/month, 5 states each | ~50,000 transitions, well within typical budgets | Cheaper if each execution runs under a few seconds | Standard, for auditability of each transition |
| 1M executions/month, 3 states each | 3M transitions — costs add up fast | Substantially cheaper due to duration-based billing | Express, for high-volume event processing |
| Long-running workflows (hours to a year) | Supported natively, full execution history retained | Not supported — 5-minute execution cap | Standard, no alternative |
| Sub-second, high-throughput event streams | Technically works but expensive at scale | Purpose-built for this pattern | Express |
The order-processing workflow built in this tutorial uses a Standard workflow because order records benefit from the full year of execution history and per-state audit trail. If you were processing millions of low-value events — clickstream data, IoT telemetry — Express would be the better default, and switching is a one-line change to the type argument on the aws_sfn_state_machine resource.
Deploying Through CI/CD Instead of a Local Terminal
Running terraform apply from a laptop works for this tutorial, but production workflows should deploy from a pipeline where changes go through review before touching real infrastructure. A minimal GitHub Actions workflow for this project looks like this:
# .github/workflows/deploy.yml
name: Deploy Step Functions Workflow
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
terraform:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: hashicorp/setup-terraform@v3
with:
terraform_version: 1.15.9
- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v4
with:
role-to-assume: arn:aws:iam::123456789012:role/github-actions-deploy
aws-region: us-east-1
- run: terraform init
- run: terraform plan -out=tfplan
- name: Apply on main branch only
if: github.ref == 'refs/heads/main'
run: terraform apply -auto-approve tfplan
Note the role-to-assume line uses OIDC federation instead of long-lived access keys stored as GitHub secrets — the same short-lived credential pattern AWS has been pushing across its own connector products in 2026. Pull requests run terraform plan so reviewers see exactly what will change; only merges to main trigger the actual apply.
Security Best Practices for Production State Machines
The IAM role built in Step 4 grants exactly one permission beyond logging: invoking one specific Lambda function ARN. That’s deliberate, and it’s worth calling out why it matters as your workflow grows: every additional Task state you add is a temptation to widen the execution role instead of scoping a new statement to the new resource.
- Scope IAM policies to specific resource ARNs, never
"Resource": "*"for Lambda invocations, even though it’s faster to write during development. - Enable CloudTrail logging for Step Functions API calls separately from the state machine’s own execution logs — this catches who started, stopped, or modified a state machine, not just what happened inside an execution.
- Avoid putting sensitive data directly in state machine input or output. Execution history is retained for up to a year on Standard workflows and is visible to anyone with read access to the state machine, so pass references (an S3 key, a database ID) rather than raw secrets or PII where possible.
- Use resource-based policies on Lambda functions in addition to the Step Functions execution role, so a function can only be invoked by the specific state machine it belongs to, not any caller with
lambda:InvokeFunction. - Rotate and audit the OIDC trust policy on your CI/CD deployment role periodically — it’s the single highest-privilege credential in this entire pipeline, since it can create and modify IAM roles.
Cleaning Up: Destroying the Stack
Step Functions charges per state transition after the free tier, and idle infrastructure still costs money in CloudWatch Logs storage. Tear down the tutorial stack when you’re done testing:
terraform destroy
# Review the destroy plan carefully, then confirm with:
# yes
Always run terraform destroy without -auto-approve in any account you don’t fully control — reviewing the destroy plan before confirming is the difference between tearing down a test stack and accidentally deleting a shared resource.
Terraform vs the AWS Console vs the CDK: Which Should You Use?
| Approach | Best for | Version control | Learning curve |
|---|---|---|---|
| AWS Console | One-off prototypes, learning ASL syntax visually | None — manual export required | Lowest |
| Terraform | Multi-cloud teams, existing Terraform infrastructure | Full, via Git | Moderate |
| AWS CDK | AWS-only teams who prefer TypeScript/Python over HCL | Full, via Git | Moderate to high |
| AWS SAM | Serverless-only stacks, tight Lambda integration | Full, via Git | Low to moderate |
If your team already manages EC2, RDS, or VPC infrastructure in Terraform, extending that same tool to Step Functions keeps everything in one state file and one review process. If you’re AWS-only and prefer writing infrastructure in a real programming language, AWS’s own learning resources page includes a parallel tutorial using the CDK to build an Express workflow — worth a look if HCL isn’t your team’s preference.
Real-World Use Cases for This Pattern
The order-processing example in this tutorial is deliberately simple so the Terraform and ASL mechanics stay visible, but the same six-resource pattern scales to workflows teams run in production today. A few patterns worth knowing before you extend the base project:
- Data pipeline orchestration. Replace the single Lambda Task with a chain of Glue jobs and a Distributed Map state that fans out over Parquet files in S3, using the data-source expansion AWS shipped for Distributed Map in September 2025.
- Human-in-the-loop approval workflows. Add a
Waitstate combined with a callback pattern (.waitForTaskToken) so the state machine pauses until a human approves a step through a separate UI, then resumes exactly where it left off — no polling required. - Multi-service saga transactions. Use the
Catchblock pattern from Step 5 to trigger compensating transactions (refunds, inventory rollback) when a downstream service fails partway through a multi-step business process. - AI agent orchestration. The AgentCore Task state shown earlier turns Step Functions into an orchestration layer for multi-step AI reasoning chains, coordinating multiple agent calls with the same retry and error handling used for any other AWS service.
- Scheduled batch processing. Pair a state machine with an EventBridge Scheduler rule instead of triggering executions manually — a pattern already covered in this site’s Amazon EventBridge setup guide if you need the scheduling side built out first.
What all five have in common: none of them require a different deployment approach than the one built in this tutorial. You’re still writing an ASL definition, wiring it into an aws_sfn_state_machine resource, and testing locally before you apply. The complexity grows in the workflow logic, not in how you ship it.
Complete Working Project Reference
Here’s the full file structure for the project built across this tutorial. Copy this layout, fill in each file with the code blocks above, and you have a deployable, testable, monitored Step Functions workflow:
aws-step-functions-terraform/
├── provider.tf # AWS + Terraform version constraints
├── variables.tf # aws_region, environment
├── main.tf # IAM roles, Lambda, state machine, CloudWatch alarm
├── outputs.tf # state_machine_arn, lambda_function_arn
├── statemachine/
│ └── workflow.asl.json # Amazon States Language definition
└── lambda/
├── index.js # Order validation handler
└── function.zip # Deployment package (generated)
Six Terraform resources total: one IAM role and policy for Step Functions, one IAM role and policy attachment for Lambda, one Lambda function, one state machine, one CloudWatch log group, and one CloudWatch alarm. That’s a complete, monitored, retry-aware workflow in under 150 lines of Terraform.
Frequently Asked Questions
Is AWS Step Functions free to use?
Step Functions has a free tier of 4,000 state transitions per month for Standard workflows. Beyond that, Standard workflows are billed per state transition, while Express workflows are billed by execution duration and memory, similar to Lambda pricing.
Can I use Terraform and the AWS Console together on the same state machine?
Technically yes, but don’t. Manual console edits to a Terraform-managed state machine cause drift that terraform plan will try to revert on the next apply, silently undoing your console changes.
What’s the difference between Standard and Express workflows?
Standard workflows are durable, support executions up to one year, and are billed per state transition. Express workflows are optimized for high-volume, short-duration event processing (up to 5 minutes), billed by duration and memory, and don’t retain full execution history the same way.
Do I need Step Functions Local for every project?
It’s not mandatory, but skipping it means every logic test costs real state transitions and real deployment cycles. For any workflow with more than two or three states, local testing pays for itself within the first debugging session.
Can Step Functions call an AI agent directly now?
Yes, as of the June 2026 preview integration with Amazon Bedrock AgentCore. A Task state can invoke a managed agent runtime and receive structured output back into the workflow, without you hosting the agent infrastructure yourself.
Why does my Terraform plan show unrelated changes every time I run it?
This is almost always non-deterministic JSON key ordering in a templated ASL file, or a provider version drift. Pin your AWS provider version (Step 2) and keep your ASL file’s key order stable between edits.
Is Terraform 1.15.9 required, or will older versions work?
Terraform 1.14.x will run this tutorial’s core resources fine. Version 1.15+ is needed only if you adopt the dynamic module sources pattern mentioned in the advanced tips section for splitting reusable state machine templates into separate modules.
What IAM permissions does my own AWS CLI user need to run this tutorial?
At minimum: states:*, lambda:*, iam:CreateRole, iam:PutRolePolicy, iam:PassRole, logs:*, and cloudwatch:PutMetricAlarm. In a real organization, scope these down with a dedicated deployment role rather than using broad wildcard permissions.
Related Coverage
- How to Set Up Amazon EventBridge: 12 Steps, 90 Min [2026]
- How to Deploy AWS ECS Fargate: 12 Steps, 90 Min [2026]
- Kubernetes on AWS EKS Setup: 12 Steps, 100 Min [2026]
- How to Build Amazon Bedrock AI Agents: 12 Steps [2026]
- AWS CloudFormation Express Mode: 12 Steps, 4x Faster [2026]
- How to Set Up AWS Glue: 12 Steps, 90 Min [2026]


