How do I invoke an AWS Step Function using an API Gateway POST request, and the request's JSON payload to the Step Function ?
2 Answers
1. Create your step function
Quite obvious. I guess that if you're reading this you know how to do it.
Otherwise, you can go have a look at the documentation here: What is AWS Step Functions?.
2. Create the IAM Role for your API
It can be for either all Step Functions, or only this one. We'll only cover the first case, as explained in an Amazon tutorial: Creating an API Using API Gateway.
To create the IAM role
Log in to the AWS Identity and Access Management console.
On the Roles page, choose Create New Role.
On the Set Role Name page, type APIGatewayToStepFunctions for Role Name, and then choose Next Step.
On the Select Role Type page, under Select Role Type, select Amazon API Gateway.
On the Attach Policy page, choose Next Step.
On the Review page, note the Role ARN, for example:
arn:aws:iam::123456789012:role/APIGatewayToStepFunctions
- Choose Create Role.
To attach a policy to the IAM role
- On the Roles page, search for your role by name (APIGatewayToStepFunctions) and then choose the role.
- On the Permissions tab, choose Attach Policy.
- On the Attach Policy page, search for AWSStepFunctionsFullAccess, choose the policy, and then choose Attach Policy.
3. Setup
3.a If you don't have a JSON payload
As explained by Ka Hou Ieong in How can i call AWS Step Functions by API Gateway?, you can create an AWS Service integration via API Gateway Console, like this:
- Integration Type: AWS Service
- AWS Service: Step Functions
- HTTP method: POST
- Action Type: Use action name
- Action: StartExecution
- Execution role: role to start the execution (the one we just created. Just paste it's ARN)
Headers:
X-Amz-Target -> 'AWSStepFunctions.StartExecution'
Content-Type -> 'application/x-amz-json-1.0'Body Mapping Templates/Request payload:
{ "input": "string" (optional), "name": "string" (optional), "stateMachineArn": "string" }
3.b If you do have JSON payload to pass as an input
Everything is the same as in 2.a, except for the body mapping template. You have to do is make it into a string. Using $util.escapeJavascript(), like this for example. It will pass your whole request's body as an input to your Step Function
#set($data = $util.escapeJavaScript($input.json('$')))
{
"input": "$data",
"name": "string" (optional),
"stateMachineArn": "string" (required)
}
Notes
stateMachineArn
: If you do not want to have to pass the stateMachineArn as part of your requests to API Gateway, you can simply hard-code it inside your Body Mapping Template (see AWS API Gateway with Step Function)name
: Omitting the name property will have API Gateway generate a different one for you at each execution.
Now, this is my first "Answer your own question", so maybe this is not how it's done, but I did spend quite a few hours trying to understand what was wrong with my Mapping Template. Hope this will help save other people's hair and time.

- 908
- 1
- 8
- 26
-
1Note that the max payload size to a StepFunction execution is 32K, so if your body is larger than that, I'd suggest the API calls a Lambda to store the body in S3 and then executes the StepFunction, passing in the S3 bucket/key. Makes the API logic simpler and is less fragile to variable data size. – Geoff Jun 01 '17 at 22:28
-
Absolutely right. And this limit is also enforced within the StepFunction (when a step requires downloading a picture from a source), which is solved in the same way. Should I update my answer ? – ElFitz Jun 09 '17 at 20:49
-
@ElFitz Referring your post helped me a lot. In section 3.b I wanted to form the stateMachineArn with region and account id populating dynamically. As in my case Stepfunction and API Gateway is in same region and account id. I was able to get the account id from `$context.accountId` but not able to find anything that gives the current region name. In my template stateMachineArn looks like this now, `"arn:aws:states:us-east-1:$context.accountId:stateMachine:AbcStateMachine"` Any idea how I can get the current region dynamically. – rockyPeoplesChamp Feb 26 '21 at 21:55
-
1question: since using that `$data` above simply uses the data field of the request body, how would the `name` field make it into the request if we were to pass it thru in a similar fashion, since it would be inside `$data`? i tried using `$data.name` but it did not work. any idea? – ChumiestBucket Jul 09 '21 at 19:59
For those ones that are looking a way to directly connect ApiGateway with a Step Functions State Machine using the OpenApi integration and CloudFormation, this is an example of how I managed to make it work:
This is the Visual Workflow I designed (more details in the CloudFormation file) as a proof of concept:
template.yaml
AWSTemplateFormatVersion: 2010-09-09
Transform: 'AWS::Serverless-2016-10-31'
Description: POC Lambda Examples - Step Functions
Parameters:
CorsOrigin:
Description: Header Access-Control-Allow-Origin
Default: "'http://localhost:3000'"
Type: String
CorsMethods:
Description: Header Access-Control-Allow-Headers
Default: "'*'"
Type: String
CorsHeaders:
Description: Header Access-Control-Allow-Headers
Default: "'Content-Type,X-Amz-Date,Authorization,X-Api-Key'"
Type: String
SwaggerS3File:
Description: 'S3 "swagger.yaml" file location'
Default: "./swagger.yaml"
Type: String
Resources:
LambdaRoleForRuleExecution:
Type: AWS::IAM::Role
Properties:
RoleName: !Sub ${AWS::StackName}-lambda-role
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action: 'sts:AssumeRole'
Principal:
Service: lambda.amazonaws.com
Policies:
- PolicyName: WriteCloudWatchLogs
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'logs:CreateLogGroup'
- 'logs:CreateLogStream'
- 'logs:PutLogEvents'
Resource: 'arn:aws:logs:*:*:*'
ApiGatewayStepFunctionsRole:
Type: AWS::IAM::Role
Properties:
Path: !Join ["", ["/", !Ref "AWS::StackName", "/"]]
AssumeRolePolicyDocument:
Version: 2012-10-17
Statement:
- Sid: AllowApiGatewayServiceToAssumeRole
Effect: Allow
Action:
- 'sts:AssumeRole'
Principal:
Service:
- apigateway.amazonaws.com
Policies:
- PolicyName: CallStepFunctions
PolicyDocument:
Version: 2012-10-17
Statement:
- Effect: Allow
Action:
- 'states:StartExecution'
Resource:
- !Ref Workflow
Start:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ${AWS::StackName}-start
Code: ../dist/src/step-functions
Handler: step-functions.start
Role: !GetAtt LambdaRoleForRuleExecution.Arn
Runtime: nodejs8.10
Timeout: 1
Wait3000:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ${AWS::StackName}-wait3000
Code: ../dist/src/step-functions
Handler: step-functions.wait3000
Role: !GetAtt LambdaRoleForRuleExecution.Arn
Runtime: nodejs8.10
Timeout: 4
Wait500:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ${AWS::StackName}-wait500
Code: ../dist/src/step-functions
Handler: step-functions.wait500
Role: !GetAtt LambdaRoleForRuleExecution.Arn
Runtime: nodejs8.10
Timeout: 2
End:
Type: AWS::Lambda::Function
Properties:
FunctionName: !Sub ${AWS::StackName}-end
Code: ../dist/src/step-functions
Handler: step-functions.end
Role: !GetAtt LambdaRoleForRuleExecution.Arn
Runtime: nodejs8.10
Timeout: 1
StateExecutionRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service:
- !Sub states.${AWS::Region}.amazonaws.com
Action:
- 'sts:AssumeRole'
Policies:
- PolicyName: "StatesExecutionPolicy"
PolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: "Allow"
Action: "lambda:InvokeFunction"
Resource:
- !GetAtt Start.Arn
- !GetAtt Wait3000.Arn
- !GetAtt Wait500.Arn
- !GetAtt End.Arn
Workflow:
Type: AWS::StepFunctions::StateMachine
Properties:
StateMachineName: !Sub ${AWS::StackName}-state-machine
RoleArn: !GetAtt StateExecutionRole.Arn
DefinitionString: !Sub |
{
"Comment": "AWS Step Functions Example",
"StartAt": "Start",
"Version": "1.0",
"States": {
"Start": {
"Type": "Task",
"Resource": "${Start.Arn}",
"Next": "Parallel State"
},
"Parallel State": {
"Type": "Parallel",
"Next": "End",
"Branches": [
{
"StartAt": "Wait3000",
"States": {
"Wait3000": {
"Type": "Task",
"Resource": "${Wait3000.Arn}",
"End": true
}
}
},
{
"StartAt": "Wait500",
"States": {
"Wait500": {
"Type": "Task",
"Resource": "${Wait500.Arn}",
"End": true
}
}
}
]
},
"End": {
"Type": "Task",
"Resource": "${End.Arn}",
"End": true
}
}
}
RestApi:
Type: AWS::Serverless::Api
Properties:
StageName: !Ref Environment
Name: !Sub ${AWS::StackName}-api
DefinitionBody:
'Fn::Transform':
Name: AWS::Include
Parameters:
# s3 location of the swagger file
Location: !Ref SwaggerS3File
swagger.yaml
openapi: 3.0.0
info:
version: '1.0'
title: "pit-jv-lambda-examples"
description: POC API
license:
name: MIT
x-amazon-apigateway-request-validators:
Validate body:
validateRequestParameters: false
validateRequestBody: true
params:
validateRequestParameters: true
validateRequestBody: false
Validate body, query string parameters, and headers:
validateRequestParameters: true
validateRequestBody: true
paths:
/execute:
options:
x-amazon-apigateway-integration:
type: mock
requestTemplates:
application/json: |
{
"statusCode" : 200
}
responses:
"default":
statusCode: "200"
responseParameters:
method.response.header.Access-Control-Allow-Headers:
Fn::Sub: ${CorsHeaders}
method.response.header.Access-Control-Allow-Methods:
Fn::Sub: ${CorsMethods}
method.response.header.Access-Control-Allow-Origin:
Fn::Sub: ${CorsOrigin}
responseTemplates:
application/json: |
{}
responses:
200:
$ref: '#/components/responses/200Cors'
post:
x-amazon-apigateway-integration:
credentials:
Fn::GetAtt: [ ApiGatewayStepFunctionsRole, Arn ]
uri:
Fn::Sub: arn:aws:apigateway:${AWS::Region}:states:action/StartExecution
httpMethod: POST
type: aws
responses:
default:
statusCode: 200
responseParameters:
method.response.header.Access-Control-Allow-Headers:
Fn::Sub: ${CorsHeaders}
method.response.header.Access-Control-Allow-Origin:
Fn::Sub: ${CorsOrigin}
".*CREATION_FAILED.*":
statusCode: 403
responseParameters:
method.response.header.Access-Control-Allow-Headers:
Fn::Sub: ${CorsHeaders}
method.response.header.Access-Control-Allow-Origin:
Fn::Sub: ${CorsOrigin}
responseTemplates:
application/json: $input.path('$.errorMessage')
requestTemplates:
application/json:
Fn::Sub: |-
{
"input": "$util.escapeJavaScript($input.json('$'))",
"name": "$context.requestId",
"stateMachineArn": "${Workflow}"
}
summary: Start workflow
responses:
200:
$ref: '#/components/responses/200Empty'
403:
$ref: '#/components/responses/Error'
components:
schemas:
Error:
title: Error
type: object
properties:
code:
type: string
message:
type: string
responses:
200Empty:
description: Default OK response
200Cors:
description: Default response for CORS method
headers:
Access-Control-Allow-Headers:
schema:
type: "string"
Access-Control-Allow-Methods:
schema:
type: "string"
Access-Control-Allow-Origin:
schema:
type: "string"
Error:
description: Error Response
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
headers:
Access-Control-Allow-Headers:
schema:
type: "string"
Access-Control-Allow-Origin:
schema:
type: "string"
step-functions.js
exports.start = (event, context, callback) => {
console.log('start event', event);
console.log('start context', context);
callback(undefined, { function: 'start' });
};
exports.wait3000 = (event, context, callback) => {
console.log('wait3000 event', event);
console.log('wait3000 context', context);
setTimeout(() => {
callback(undefined, { function: 'wait3000' });
}, 3000);
};
exports.wait500 = (event, context, callback) => {
console.log('wait500 event', event);
console.log('wait500 context', context);
setTimeout(() => {
callback(undefined, { function: 'wait500' });
}, 500);
};
exports.end = (event, context, callback) => {
console.log('end event', event);
console.log('end context', context);
callback(undefined, { function: 'end' });
};

- 994
- 16
- 28
-
3
-
@Nihil, what part of it? Maybe my example that includes IaC and a direct connection between Api Gateway and Step Functions brings more complications and it isn't simple enough? – Julio Villane Jul 03 '20 at 06:10
-
1Not your example Julio, that's actually very helpful. I meant the whole setup of triggering Step Functions using API Gateway. Many resources needed (Method, integration, response, response method..), strange request ARN/URI (apigateway:{region}:action/StartExecution), request template syntax.. all very complicated. Got it working (with your help, thanks!), and now it seems simple enough, but when getting started - might as well be particle physics (you got an up quark, down quark, strange and beauty - what could be simpler?) – Nihil Jul 05 '20 at 10:03
-
3Yeah, it's true, looks very complicated at start, now i'm used to, and there is an architectural satisfaction on not using a lambda in between, because maybe that solution (using lambdas) make a simpler example. Nice to know it helped you. – Julio Villane Jul 05 '20 at 12:43
-
Nice, is there a way to do it in synchronous way? I mean to wait for the final response from the workflow and then send it back with API Gateway response. – m52509791 Dec 07 '20 at 09:23
-
1@m52509791, there is always the less elegant solution that considers using a Lambda to wait the end of the Step Function instance, but you will be paying for that waiting time and an error if the process takes more than 30 seconds (Api Gateway's requests have a 30 seconds timeout). A better alternative is using websockets through Api Gateway, to let the client know the process has end. – Julio Villane Dec 07 '20 at 18:34
-
I was not able to get it working using SAM and tried multiple options. Followed the instruction mentioned above and it didn't help. The details mentioned in this blog helped me to move forward and fix it - https://towardsserverless.com/articles/how-to-invoke-step-fn-using-aws-apigateway/ – Annette May 24 '22 at 01:33