Skip to content

Commit 557a6e6

Browse files
authored
Merge pull request #416 from YogeshNain2015/improve/human-in-the-loop-one-click-approval
Improve human-in-the-loop with one-click email approval
2 parents 3bfc183 + 9d2006f commit 557a6e6

7 files changed

Lines changed: 450 additions & 35 deletions

File tree

human-in-the-loop/README.md

Lines changed: 27 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# Human in the Loop
22

3-
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.
3+
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.
44

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

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

1617
## Deployment Instructions
1718

@@ -41,24 +42,37 @@ Important: this application uses various AWS services and there are costs associ
4142
4243
![image](./resources/statemachine.png)
4344
44-
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.
45-
2. After approving or denying, the reviewer calls the `SendTaskSuccess` API and passes the task token as well as the review result.
46-
3. The result is evaluated by Step Functions and the corresponding AWS Lambda function is invoked.
45+
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.
46+
2. The Lambda function publishes an email via [Amazon Simple Notification Service (SNS)](https://aws.amazon.com/sns/) containing the clickable approve/reject links.
47+
3. The reviewer clicks one of the links in the email. This triggers a GET request to API Gateway.
48+
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.
49+
5. The workflow resumes and routes to the appropriate processing Lambda based on the approval outcome.
50+
4751
4852
## Testing
4953
5054
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.
5155
2. Navigate to the AWS Step Functions console and select the `human-in-the-loop` workflow.
5256
3. Select `Start Execution` and use any valid JSON data as input.
53-
4. Select `Start Execution` and wait until you receive the email from SNS.
54-
5. Copy the task token from the email.
55-
6. Use the AWS CLI to complete the task by calling the `SendTaskSuccess` API. Replace the task token with the value you copied earlier.
56-
```
57-
aws stepfunctions send-task-success --task-token <YOUR-TASK-TOKEN> --task-output '{"result":true}'
58-
```
59-
Make sure to use that the cli uses the same region as the one you used to deploy your state machine.
60-
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.
61-
6. If you trigger a new execution and replace `{"result":true}` with `{"result":false}` in step 6, the workflow transitions to `Process Rejection` respectively.
57+
4. Select `Start Execution` and wait until you receive the approval request email from SNS.
58+
5. Click the **Approve** or **Reject** link in the email.
59+
6. You will see a confirmation page in your browser indicating the workflow was approved or rejected.
60+
7. Observe the execution in the Step Functions console — the workflow transitions to `Handle approval` or `Handle rejection` based on your response.
61+
62+
### Testing via CLI (alternative)
63+
64+
You can also test the API Gateway endpoint directly:
65+
66+
```bash
67+
# Get the API endpoint from stack outputs
68+
export API_ENDPOINT=$(aws cloudformation describe-stacks \
69+
--stack-name <your-stack-name> \
70+
--query "Stacks[0].Outputs[?OutputKey=='ApiEndpoint'].OutputValue" \
71+
--output text)
72+
73+
# Approve (replace <ENCODED_TASK_TOKEN> with the URL-encoded token from the email link)
74+
curl "$API_ENDPOINT?taskToken=<ENCODED_TASK_TOKEN>&decision=approve"
75+
```
6276

6377
## Cleanup
6478

human-in-the-loop/example-workflow.json

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,23 @@
11
{
22
"title": "Human in the Loop",
3-
"description": "Wait for an approval from a human reviewer before continuing",
4-
"language": "",
3+
"description": "Wait for an approval from a human reviewer with one-click email approve/reject links",
4+
"language": "Python",
55
"simplicity": "2 - Pattern",
66
"usecase": "",
77
"type": "Standard",
88
"diagram":"/resources/statemachine.png",
99
"videoId": "",
10-
"level": "100",
10+
"level": "200",
1111
"framework": "SAM",
12-
"services": ["sns","lambda"],
12+
"services": ["sns","lambda","apigateway"],
1313
"introBox": {
1414
"headline": "How it works",
1515
"text": [
16-
"This pattern allows you to integrate an human review or approval process into your workflows.",
17-
"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.",
18-
"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.",
19-
"The task will pause until it receives that task token back with a SendTaskSuccess or SendTaskFailure API call."
16+
"This pattern allows you to integrate a human review or approval process into your workflows with one-click email approval.",
17+
"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.",
18+
"The workflow pauses using the .waitForTaskToken service integration until the reviewer clicks an approve or reject link.",
19+
"The link triggers an API Gateway endpoint backed by a Lambda function that calls SendTaskSuccess or SendTaskFailure to resume the workflow.",
20+
"Depending on the review outcome, a different processing path is followed."
2021
]
2122
},
2223
"testing": {
@@ -56,6 +57,14 @@
5657
{
5758
"text": "The AWS Step Functions Workshop",
5859
"link": "https://catalog.workshops.aws/stepfunctions/en-US"
60+
},
61+
{
62+
"text": "AWS Step Functions Task Tokens",
63+
"link": "https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html#connect-wait-token"
64+
},
65+
{
66+
"text": "Step Functions Service Integration Patterns",
67+
"link": "https://docs.aws.amazon.com/step-functions/latest/dg/connect-to-resource.html"
5968
}
6069
]
6170
},
@@ -66,7 +75,12 @@
6675
"bio": "Ben is a Senior Solutions Architect at Amazon Web Services (AWS) based in Frankfurt, Germany.",
6776
"linkedin": "benfreiberg",
6877
"twitter": ""
78+
},
79+
{
80+
"name": "Yogesh Nain",
81+
"image": "link-to-your-photo.jpg",
82+
"bio": "Yogesh is a Cloud Support Engineer and SME of Lambda, API Gateway at Amazon Web Services.",
83+
"linkedin": "https://www.linkedin.com/in/yogesh-nain-a54133170/"
6984
}
7085
]
7186
}
72-
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
"""
2+
MIT No Attribution
3+
4+
Copyright 2022 Amazon Web Services
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
7+
software and associated documentation files (the "Software"), to deal in the Software
8+
without restriction, including without limitation the rights to use, copy, modify,
9+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
10+
permit persons to whom the Software is furnished to do so.
11+
12+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
14+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
15+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
16+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
17+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18+
"""
19+
import boto3
20+
import json
21+
from urllib.parse import unquote
22+
23+
24+
def lambda_handler(event, context):
25+
query_params = event.get('queryStringParameters', {})
26+
task_token = unquote(query_params.get('taskToken', ''))
27+
decision = query_params.get('decision')
28+
29+
print(f"Received request - decision: {decision}, taskToken (first 50 chars): {task_token[:50]}...")
30+
31+
sfn = boto3.client('stepfunctions')
32+
33+
try:
34+
if decision == 'approve':
35+
sfn.send_task_success(
36+
taskToken=task_token,
37+
output=json.dumps({'result': True})
38+
)
39+
message = 'Workflow approved successfully!'
40+
else:
41+
sfn.send_task_success(
42+
taskToken=task_token,
43+
output=json.dumps({'result': False})
44+
)
45+
message = 'Workflow rejected.'
46+
47+
print(f"Successfully processed decision: {decision}")
48+
return {
49+
'statusCode': 200,
50+
'headers': {'Content-Type': 'text/html'},
51+
'body': f'<html><body><h2>{message}</h2></body></html>'
52+
}
53+
except Exception as e:
54+
print(f"Error processing decision '{decision}': {str(e)}")
55+
return {
56+
'statusCode': 500,
57+
'headers': {'Content-Type': 'text/html'},
58+
'body': f'<html><body><h2>Error processing request</h2><p>{str(e)}</p></body></html>'
59+
}
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
"""
2+
MIT No Attribution
3+
4+
Copyright 2022 Amazon Web Services
5+
6+
Permission is hereby granted, free of charge, to any person obtaining a copy of this
7+
software and associated documentation files (the "Software"), to deal in the Software
8+
without restriction, including without limitation the rights to use, copy, modify,
9+
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
10+
permit persons to whom the Software is furnished to do so.
11+
12+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
13+
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
14+
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
15+
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
16+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
17+
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
18+
"""
19+
import boto3
20+
import json
21+
import os
22+
from urllib.parse import quote
23+
24+
25+
def lambda_handler(event, context):
26+
task_token = quote(event['taskToken'], safe='')
27+
execution_id = event['execution']
28+
29+
print(f"Sending approval email for execution: {execution_id}")
30+
print(f"Task token (first 50 chars): {event['taskToken'][:50]}...")
31+
32+
api_endpoint = os.environ['API_ENDPOINT']
33+
approve_url = f"{api_endpoint}?taskToken={task_token}&decision=approve"
34+
reject_url = f"{api_endpoint}?taskToken={task_token}&decision=reject"
35+
36+
message_data = {
37+
'default': 'Workflow Approval Required',
38+
'email': (
39+
f"Workflow Approval Required\n"
40+
f"{'=' * 40}\n\n"
41+
f"A new approval is required for workflow execution:\n"
42+
f"{execution_id}\n\n"
43+
f"To APPROVE, click the link below:\n"
44+
f"{approve_url}\n\n"
45+
f"To REJECT, click the link below:\n"
46+
f"{reject_url}\n"
47+
)
48+
}
49+
50+
sns = boto3.client('sns')
51+
sns.publish(
52+
TopicArn=os.environ['SNS_TOPIC_ARN'],
53+
Subject='Step Functions Workflow - Approval Required',
54+
Message=json.dumps(message_data),
55+
MessageStructure='json'
56+
)
57+
58+
return {
59+
'statusCode': 200,
60+
'body': 'Approval notification sent successfully'
61+
}

0 commit comments

Comments
 (0)