Build and Deploy Pipeline for AWS Step Functions: Three Approaches with GitHub Actions
Learn three different approaches to set up build and deploy pipelines for AWS Step Functions using GitHub Actions. We'll explore deploying ASL files directly, using CDK constructs, and a hybrid approach that gives you the best of both worlds.
Setting up a proper build and deploy pipeline is essential for any production application, and AWS Step Functions is no exception.
Whether you're defining workflows in ASL (Amazon States Language) or using infrastructure-as-code with CDK, you need a reliable way to deploy changes to your workflows.
In this post, we'll explore three different approaches to deploying Step Functions:
- Deploying workflows defined directly as ASL files
- Using CDK to define workflows entirely in code
- A hybrid approach that combines visual workflow design with CDK resource management
I'll be using GitHub Actions to demonstrate the pipeline setup, but the same concepts apply regardless of which build/deploy tool you're using.
AWS Step Functions with CDK: Define Workflows in Code Using .NET

This article is sponsored by AWS and is part of my Step Functions Series.
The demo project contains a user onboarding workflow that we've been building throughout this series.
We'll deploy this same workflow using three different approaches and see how each one handles environment-specific configuration.
Approach 1: Deploying Step Function ASL Files
The most straightforward approach is to deploy a workflow defined in an ASL file using the AWS CLI.
This works well when you've designed your workflow using the visual workflow builder in the AWS Console and want to version control and deploy that definition.
The GitHub Actions Workflow
Here's the GitHub Actions workflow file
name: AWS Step Functions...jobs:aws-step-functions:runs-on: ubuntu-lateststeps:...- name: Deploy Step Functions State Machinerun: |echo "Deploying Step Functions State Machine from ASL definition..."STATE_MACHINE_NAME="UserOnboardingASL"ROLE_ARN="${{ secrets.STEP_FUNCTIONS_ROLE_ARN }}"if aws stepfunctions describe-state-machine \--state-machine-arn "arn:aws:states:region:account:stateMachine:${STATE_MACHINE_NAME}" 2>/dev/null; thenecho "State machine exists. Updating definition..."aws stepfunctions update-state-machine \--state-machine-arn "arn:aws:states:region:account:stateMachine:${STATE_MACHINE_NAME}" \--definition file://aws-step-functions/user-onboarding.asl.jsonelseecho "State machine does not exist. Creating new state machine..."aws stepfunctions create-state-machine \--name "${STATE_MACHINE_NAME}" \--definition file://aws-step-functions/user-onboarding.asl.json \--role-arn "${ROLE_ARN}" \--type STANDARDfiecho "Deployment completed successfully!"
The workflow checks out the code, configures AWS credentials (using access keys stored in GitHub secrets), and deploys the Step Function using AWS CLI commands.
The script checks if the state machine already exists using describe-state-machine. If it exists, it uses update-state-machine to update the definition. If not, it uses create-state-machine to create a new one.
The Challenge: Environment-Specific Resources
In this example, the ASL file contains hardcoded references to AWS resources like Lambda functions, DynamoDB tables, and SQS queues.
{"Lambda Invoke": {"Type": "Task","Resource": "arn:aws:states:::lambda:invoke","Arguments": {"FunctionName": "arn:aws:lambda:ap-southeast-2:189107071895:function:user-onboarding","Payload": {"userId": "{% $user.userId %}"}}}}
This makes it difficult to deploy the same workflow to multiple environments (dev, staging, production) where resource ARNs differ.
Instead of hardcoding resource ARNs, you can use parameters in your ASL file:
{"Lambda Invoke": {..."Arguments": {"FunctionName": "${LambdaFunctionArn}",...}}},}
You can then update your GitHub Actions workflow to replace these parameters with environment-specific values using string replacement tool.
Approach 2: Using CDK to Define Workflows
When your workflow is entirely defined using CDK constructs, deployment becomes much simpler.
AWS Step Functions with CDK: Define Workflows in Code Using .NET

Instead of writing ASL files manually, you use native CDK constructs to build your workflow. We explored this in detail in a previous post in this series.
The CDK Deployment Workflow
Here's the GitHub Actions workflow for deploying a CDK-defined Step Function:
name: AWS Step Functions CDK Stackon: ...env:AWS_REGION: ap-southeast-2DOTNET_VERSION: "8.0.x"NODE_VERSION: "20.x"STACK_NAME: UserOnboardingCDKWithResourcesStackjobs:deploy:name: Deploy CDK Stackruns-on: ubuntu-lateststeps:...- name: Build CDK projectworking-directory: aws-step-functions/step-functions/src/StepFunctionsrun: dotnet build --no-restore --configuration Release- name: CDK Deployworking-directory: aws-step-functions/step-functionsrun: |cdk deploy ${{ env.STACK_NAME }} \--require-approval never \--verbose
The workflow sets up .NET and Node.js, installs the AWS CDK CLI, restores dependencies, builds the CDK project, and deploys using cdk deploy with the --require-approval never flag to skip manual confirmation.
Benefits of the CDK Approach
When you define your workflow entirely in CDK, you can:
- Create all AWS resources (DynamoDB tables, Lambda functions, SQS queues) in the same stack
- Deploy to multiple environments
- Get type safety and IntelliSense in your IDE
- Version control your infrastructure alongside your application code
However, this approach has one significant drawback: you lose the ability to use the visual workflow builder.
For complex workflows, the drag-and-drop interface in the AWS Console is one of Step Functions' key advantages.
Having to write everything in code can be tedious.
Approach 3: Hybrid - CDK + ASL (The Best of Both Worlds)
The hybrid approach combines the visual workflow design of ASL with the resource management capabilities of CDK.
You still design your workflow visually using the AWS Console, but you use CDK to:
- Create environment-specific resources (DynamoDB, Lambda, SQS)
- Replace parameters in the ASL file with actual resource values
- Deploy everything together
The Hybrid Stack Implementation
Here's how the CDK stack loads an ASL file and replaces parameters with created resources:
public class UserOnboardingUsingASLWithResourcesStack : Stack{public UserOnboardingUsingASLWithResourcesStack(Construct scope, string id, IStackProps props = null): base(scope, id, props){var userTable = new Table( ... );var onboardingQueue = new Queue( ... );// Load ASL definition from filevar aslFilePath = Path.Combine(AppContext.BaseDirectory, "user-onboarding-cdk.asl.json");var aslDefinition = File.ReadAllText(aslFilePath);// Replace placeholders with actual resource valuesaslDefinition = aslDefinition.Replace("${TableName}", userTable.TableName).Replace("${LambdaFunctionArn}", lambdaFunction.FunctionArn).Replace("${QueueUrl}", onboardingQueue.QueueUrl);// Grant permissionsvar stateMachineRole = new Role( ... );userTable.GrantReadWriteData(stateMachineRole);onboardingQueue.GrantSendMessages(stateMachineRole);stateMachineRole.AddToPolicy(new PolicyStatement(new PolicyStatementProps{Effect = Effect.ALLOW,Actions = new[] { "lambda:InvokeFunction" },Resources = new[] { lambdaFunction.FunctionArn }}));var stateMachine = new StateMachine( ... );}}
The code creates AWS resources using CDK constructs, loads the ASL file as a string, replaces parameter placeholders with actual resource values, and deploys the state machine with the updated definition.
The GitHub Actions workflow for the hybrid approach is identical to the pure CDK approach shown earlier.
You use the same cdk deploy command, but behind the scenes, CDK handles both resource creation and ASL parameter replacement.
Making Updates to Your Workflows
One of the key benefits of having a CI/CD pipeline is that updates to your workflows are automatically deployed.
Let's test this by making a simple change to the workflow. Navigate to your ASL file and add a Wait state that pauses for 5 seconds:
{"Wait": {"Type": "Wait","Seconds": 5,"Next": "Lambda Invoke"}}
Commit and push the change to your repository. GitHub Actions will automatically trigger the deployment workflow.
The same applies to CDK-defined workflows. Any changes to your CDK code will be automatically deployed when you push to the main branch.
In my experience, the hybrid approach offers the best balance. You get the ease of visual workflow design while maintaining full control over resource management and environment configuration.