Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 27 additions & 13 deletions human-in-the-loop/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Human in the Loop

This pattern allows you to integrate an human review or approval process into your workflows. Each task sends a message to a SNS topic which sends a notification to a human reviewer or approver by email for example. The workflow then waits until the approver completes their review. Depending on the review outcome a different Lambda function can be invoked.
This pattern allows you to integrate a human review or approval process into your workflows with **one-click email approval**. An AWS Lambda function sends an approval request via Amazon SNS email containing clickable approve/reject links. The task token is URL-encoded to ensure special characters don't break the Amazon API Gateway callback URL. The workflow pauses until the reviewer clicks a link, which triggers an API Gateway endpoint to resume the AWS Step Functions execution.

Learn more about this workflow at Step Functions workflows collection: [Human in the Loop](https://serverlessland.com/workflows/human-in-the-loop)

Expand All @@ -12,6 +12,7 @@ Important: this application uses various AWS services and there are costs associ
* [AWS CLI](https://docs.aws.amazon.com/cli/latest/userguide/install-cliv2.html) installed and configured
* [Git Installed](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git)
* [AWS Serverless Application Model](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) (AWS SAM) installed
* Python 3.13 or later

## Deployment Instructions

Expand Down Expand Up @@ -41,24 +42,37 @@ Important: this application uses various AWS services and there are costs associ

![image](./resources/statemachine.png)

1. Data that should be reviewed by a human is passed to the workflow. A message is send to a [Amazon Simple Notification Service (SNS)](https://aws.amazon.com/sns/) topic which sends out a notification via Email. The notification contains a [task token](https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token) which is automatically generated by AWS Step Functions.
2. After approving or denying, the reviewer calls the `SendTaskSuccess` API and passes the task token as well as the review result.
3. The result is evaluated by Step Functions and the corresponding AWS Lambda function is invoked.
1. Data that should be reviewed by a human is passed to the workflow. The state machine invokes a Lambda function using the `.waitForTaskToken` integration pattern. The Lambda function URL-encodes the task token and constructs approve/reject links pointing to an API Gateway endpoint.
2. The Lambda function publishes an email via [Amazon Simple Notification Service (SNS)](https://aws.amazon.com/sns/) containing the clickable approve/reject links.
3. The reviewer clicks one of the links in the email. This triggers a GET request to API Gateway.
4. API Gateway invokes a second Lambda function that decodes the task token and calls `SendTaskSuccess` (approve) or `SendTaskFailure` (reject) on the Step Functions execution.
5. The workflow resumes and routes to the appropriate processing Lambda based on the approval outcome.


## Testing

1. After deployment you receive an email titled `AWS Notification - Subscription Confirmation`. Click on the link in the email to confirm your subscription. This will allow SNS to send you emails.
2. Navigate to the AWS Step Functions console and select the `human-in-the-loop` workflow.
3. Select `Start Execution` and use any valid JSON data as input.
4. Select `Start Execution` and wait until you receive the email from SNS.
5. Copy the task token from the email.
6. Use the AWS CLI to complete the task by calling the `SendTaskSuccess` API. Replace the task token with the value you copied earlier.
```
aws stepfunctions send-task-success --task-token <YOUR-TASK-TOKEN> --task-output '{"result":true}'
```
Make sure to use that the cli uses the same region as the one you used to deploy your state machine.
5. Observe the task in the Step Functions console. Because response states that the approval was granted, the task transitioned to the `Process Approval` step.
6. If you trigger a new execution and replace `{"result":true}` with `{"result":false}` in step 6, the workflow transitions to `Process Rejection` respectively.
4. Select `Start Execution` and wait until you receive the approval request email from SNS.
5. Click the **Approve** or **Reject** link in the email.
6. You will see a confirmation page in your browser indicating the workflow was approved or rejected.
7. Observe the execution in the Step Functions console — the workflow transitions to `Handle approval` or `Handle rejection` based on your response.

### Testing via CLI (alternative)

You can also test the API Gateway endpoint directly:

```bash
# Get the API endpoint from stack outputs
export API_ENDPOINT=$(aws cloudformation describe-stacks \
--stack-name <your-stack-name> \
--query "Stacks[0].Outputs[?OutputKey=='ApiEndpoint'].OutputValue" \
--output text)

# Approve (replace <ENCODED_TASK_TOKEN> with the URL-encoded token from the email link)
curl "$API_ENDPOINT?taskToken=<ENCODED_TASK_TOKEN>&decision=approve"
```

## Cleanup

Expand Down
32 changes: 23 additions & 9 deletions human-in-the-loop/example-workflow.json
Original file line number Diff line number Diff line change
@@ -1,22 +1,23 @@
{
"title": "Human in the Loop",
"description": "Wait for an approval from a human reviewer before continuing",
"language": "",
"description": "Wait for an approval from a human reviewer with one-click email approve/reject links",
"language": "Python",
"simplicity": "2 - Pattern",
"usecase": "",
"type": "Standard",
"diagram":"/resources/statemachine.png",
"videoId": "",
"level": "100",
"level": "200",
"framework": "SAM",
"services": ["sns","lambda"],
"services": ["sns","lambda","apigateway"],
"introBox": {
"headline": "How it works",
"text": [
"This pattern allows you to integrate an human review or approval process into your workflows.",
"The task sends a message to a SNS topic which sends a notification to a human reviewer or approver by email for example. The workflow then waits until the approver completes their review. Depending on the review outcome a different Lambda function can be invoked.",
"Waiting for completion of the review is done using the .waitForTaskToken service integration. The payload of the SNS message contains a task token, which is automatically generated by AWS Step Functions.",
"The task will pause until it receives that task token back with a SendTaskSuccess or SendTaskFailure API call."
"This pattern allows you to integrate a human review or approval process into your workflows with one-click email approval.",
"A Lambda function sends an approval request via SNS email containing clickable approve/reject links. The task token is URL-encoded to avoid issues with special characters in API Gateway URLs.",
"The workflow pauses using the .waitForTaskToken service integration until the reviewer clicks an approve or reject link.",
"The link triggers an API Gateway endpoint backed by a Lambda function that calls SendTaskSuccess or SendTaskFailure to resume the workflow.",
"Depending on the review outcome, a different processing path is followed."
]
},
"testing": {
Expand Down Expand Up @@ -56,6 +57,14 @@
{
"text": "The AWS Step Functions Workshop",
"link": "https://catalog.workshops.aws/stepfunctions/en-US"
},
{
"text": "AWS Step Functions Task Tokens",
"link": "https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token"
},
{
"text": "Step Functions Service Integration Patterns",
"link": "https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html"
}
]
},
Expand All @@ -66,7 +75,12 @@
"bio": "Ben is a Senior Solutions Architect at Amazon Web Services (AWS) based in Frankfurt, Germany.",
"linkedin": "benfreiberg",
"twitter": ""
},
{
"name": "Yogesh Nain",
"image": "link-to-your-photo.jpg",
"bio": "Yogesh is a Cloud Support Engineer and SME of Lambda, API Gateway at Amazon Web Services.",
"linkedin": "https://www.linkedin.com/in/yogesh-nain-a54133170/"
}
]
}

59 changes: 59 additions & 0 deletions human-in-the-loop/lambda/handle_approval.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
"""
MIT No Attribution

Copyright 2022 Amazon Web Services

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import boto3
import json
from urllib.parse import unquote


def lambda_handler(event, context):
query_params = event.get('queryStringParameters', {})
task_token = unquote(query_params.get('taskToken', ''))
decision = query_params.get('decision')

print(f"Received request - decision: {decision}, taskToken (first 50 chars): {task_token[:50]}...")

sfn = boto3.client('stepfunctions')

try:
if decision == 'approve':
sfn.send_task_success(
taskToken=task_token,
output=json.dumps({'result': True})
)
message = 'Workflow approved successfully!'
else:
sfn.send_task_success(
taskToken=task_token,
output=json.dumps({'result': False})
)
message = 'Workflow rejected.'

print(f"Successfully processed decision: {decision}")
return {
'statusCode': 200,
'headers': {'Content-Type': 'text/html'},
'body': f'<html><body><h2>{message}</h2></body></html>'
}
except Exception as e:
print(f"Error processing decision '{decision}': {str(e)}")
return {
'statusCode': 500,
'headers': {'Content-Type': 'text/html'},
'body': f'<html><body><h2>Error processing request</h2><p>{str(e)}</p></body></html>'
}
61 changes: 61 additions & 0 deletions human-in-the-loop/lambda/send_approval_email.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
"""
MIT No Attribution

Copyright 2022 Amazon Web Services

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
"""
import boto3
import json
import os
from urllib.parse import quote


def lambda_handler(event, context):
task_token = quote(event['taskToken'], safe='')
execution_id = event['execution']

print(f"Sending approval email for execution: {execution_id}")
print(f"Task token (first 50 chars): {event['taskToken'][:50]}...")

api_endpoint = os.environ['API_ENDPOINT']
approve_url = f"{api_endpoint}?taskToken={task_token}&decision=approve"
reject_url = f"{api_endpoint}?taskToken={task_token}&decision=reject"

message_data = {
'default': 'Workflow Approval Required',
'email': (
f"Workflow Approval Required\n"
f"{'=' * 40}\n\n"
f"A new approval is required for workflow execution:\n"
f"{execution_id}\n\n"
f"To APPROVE, click the link below:\n"
f"{approve_url}\n\n"
f"To REJECT, click the link below:\n"
f"{reject_url}\n"
)
}

sns = boto3.client('sns')
sns.publish(
TopicArn=os.environ['SNS_TOPIC_ARN'],
Subject='Step Functions Workflow - Approval Required',
Message=json.dumps(message_data),
MessageStructure='json'
)

return {
'statusCode': 200,
'body': 'Approval notification sent successfully'
}
Loading