This solution enables you to receive WhatsApp notifications for Trello alerts, specifically for cards with an assigned due date.
Scenario
I needed a way to better organize my personal projects, including preparing for my AWS certification, managing medical appointments, social meetings, and various other tasks. With so many apps to juggle for work and multiple email accounts, I often missed or muted notifications from tools like Google Calendar. As a result, I frequently found myself forgetting important tasks.
Then it hit me: What app do I use every day that truly grabs my attention? The answer was clear—WhatsApp. For me, it’s the central hub for communication. Whenever I need to check something, I open WhatsApp and see all the tasks I still need to complete. After exploring the Trello integrations and power-ups, I found that the WhatsApp-to-Trello power-ups were charging 4 euros per month for each number, which would cost me 88 euros a year. That’s when I decided to create my own solution, practice serverless, and experiment with ways to minimize costs.
This solution uses the following workflow:
Scenario 1: Assign a new due date to a specific card.
- The user assigns a due date to a card.
- Trello webhooks send a POST request to our callback URL.
- The API Gateway receives the request and sends it to our Step Function.
- The Step Function filters the event for the 'update card' action and the 'due_date' sub-action.
- The Step Function triggers the Lambda function 'Create Event Scheduler'. This Lambda function creates a DynamoDB record with the card information and due date for the WhatsApp notification. It also uses the AWS SDK to create the EventBridge scheduler and stores the scheduler ARN in the DynamoDB.
- When the scheduler is triggered, it invokes the Notification WhatsApp lambda function, which uses a WhatsApp template that was previously created. This template invokes the Meta API to send the notification to my WhatsApp number.
Scenario 2: Update the due date for a specific card.
- The user updates the due date of a card.
- Trello webhooks send a POST request to our callback URL.
- The API Gateway receives the request and sends it to our Step Function.
- The Step Function filters the event for the 'update card' action and the 'due_date' sub-action.
- The Step Function triggers the Lambda function Create Event Scheduler, which checks if a record with a card ID already exists in DynamoDB. If so, only update the record with the new due date.
- The DynamoDB table is configured with DynamoDB Streams associated with the Lambda Update Scheduler EventBridge, which is triggered only when the specified attribute 'due_date' is modified. This Lambda updates the scheduler with the new date for the notification.
- When the scheduler is triggered, it invokes the Notification WhatsApp lambda function, which uses a WhatsApp template that was previously created and invokes the Meta API to send the notification to my WhatsApp number.
Pre-requirements
- Trello Board created and some cards for testing
- Whatsapp number to receive notifications
- Meta developer account
- AWS Account created and Admin Access.
Trello Configuration
To create a Power-Up, click on this link to access the Trello Power-Ups admin panel. On this page click on new.
Power-Up creation
Creating a New Power-Up
When creating a new Power-Up, you'll need to fill in the following fields:
- App name: Enter a name for your Power-Up (e.g., "Whatsapp Trello Notifications")
- Workspace: Select the workspace where you want to use this Power-Up (e.g., "work tasks")
- Email: Enter the email address associated with your Trello account
- Support contact: Provide a support email address where users can reach you. You can use the same email as above if preferred
- Author: Enter the name of the author or your company name
- Iframe connector URL: Leave this field empty since this solution does not use any graphical user interface
Once you've filled in all the required information, click the Create button to proceed with the Power-Up creation.
API Configuration
Once you've created the Power-Up, you will be redirected to the admin page where you can view your new Power-Up. Click on it to proceed with the next configuration steps.
When you are here please key in the API-KEY section.
Securely store your API Key and Secret—these credentials are required to interact with the Trello API. My recomendation use AWS Secret Manager
Webhook Setup
Now that you have the credentials, we need to create a Trello Webhook to receive all events from your Trello dashboard.
One of the requirements to create a webhook is having a callback URL. Trello will send a HEAD request to the endpoint you specify and will wait for a 200 response code to create and activate the webhook.
For more detailed information, please refer to the Trello Official Documentation.
To obtain your Callback URL, follow these steps:
- Import an AWS API Gateway
- Create an endpoint
/eventswith the HTTP HEAD method and integrate it with a Lambda function that returns a 200 response code.
Go to your AWS Account and select API Gateway, create a new API Gateway and select the import option in the section REST API. Use the following JSON specification:
To obtain your Callback URL, follow these steps:
- Go to your AWS Account and navigate to API Gateway
- Click on Create API and select REST API
- Choose Import from Swagger option
- Copy and paste the following JSON specification in the import dialog:
- Click on CREATE API
{
"swagger": "2.0",
"info": {
"description": "API created to send WhatsApp reminders for Trello tasks that need to be completed or to trigger reminders",
"version": "2026-01-01T10:58:10Z",
"title": "trelloWhatsapp notification"
},
"basePath": "/events",
"schemes": [
"https"
],
"paths": {
"/events": {
"head": {
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "200 response",
"schema": {
"$ref": "#/definitions/Empty"
}
}
}
},
"post": {
"consumes": [
"application/json"
],
"produces": [
"application/json"
],
"responses": {
"200": {
"description": "200 response",
"schema": {
"$ref": "#/definitions/Empty"
}
}
}
}
}
},
"definitions": {
"Empty": {
"type": "object",
"title": "Empty Schema"
}
},
"x-amazon-apigateway-security-policy": "SecurityPolicy_TLS13_1_3_2025_09",
"x-amazon-apigateway-endpoint-access-mode": "BASIC"
}
Now you need to create a Lambda function that will respond to the HEAD request from Trello with a 200 status code.
Follow these steps:
- Go to your AWS Account and navigate to Lambda
- Click on Create function
- Select Author from scratch
- Configure the following:
-
Function name: Enter a name for your Lambda function (e.g.,
lambda-response-head-trello) - Runtime: Select your preferred runtime language. This example uses Python 3.x, but you can choose any supported language
- Architecture: Select x86_64
-
Function name: Enter a name for your Lambda function (e.g.,
- Click Create function
Once the function is created, you'll use the default code provided by AWS, which is sufficient for responding to Trello's HEAD request verification.
Now you need to integrate the Lambda function you just created with the API Gateway HEAD endpoint.
Follow these steps:
- Go back to your API Gateway console
- Select the HEAD endpoint under
/events - Click on Edit integration
- In the integration configuration page:
- Select Lambda as the integration type
- Choose the Lambda function you created in the previous step (
lambda-response-head-trello) - Click Save
Once saved, your API Gateway HEAD endpoint is now integrated with your Lambda function. This will allow Trello to verify your webhook callback URL by receiving a 200 response.
Now you need to deploy your API Gateway to get the invoke URL that will be used as your Trello webhook callback URL.
Follow these steps:
- In your API Gateway console, click on Deploy API
- Configure the deployment:
- Stage: Select Create new stage
-
Stage name: Enter a name for your stage (e.g.,
development) - Click Deploy
Get Your Callback URL
Once deployed, you need to obtain your invoke URL to use as the Trello webhook callback URL.
Follow these steps:
- In your API Gateway console, click on the / (root)
- Click on /events
- Click on Invoke URL
- Copy and save this URL—this is your callback URL that you'll use in the Trello webhook configuration
Your invoke URL will look like this:
https://{api-id}.execute-api.us-east-1.amazonaws.com/development/events
Save this URL as you'll need it in the next step to create the Trello webhook.
To create the webhook and configure your automation, you need to obtain the Trello board ID.
Follow these steps:
- Go to your Trello board
- Select a card and click on it to open it
- Click on the three dots (•••) in the top right corner of the card
- Click on Share
- Click on Export as JSON
- Copy the JSON data and save it
From the exported JSON, you can find:
- Board ID: The unique identifier for your Trello board called idBoard save this value we will need it on the next step
Create Trello Webhook using Bruno
Now that you have all the necessary parameters, you'll create the Trello webhook using Bruno (an API client similar to Postman).
Parameters collected:
- Callback URL: Your API Gateway invoke URL
- API Key: Your Trello API Key
- API Token: Your Trello API Token
- Model ID: Your Trello board ID
Step 1: Create a Bruno Collection and Environment Variables
- Open Bruno
- Click on Create Collection and name it
trello-whatsapp-notifications - Right-click on the collection and select Settings
- Go to the Environments tab
- Click on Create Environment and name it
development - Add the following environment variables with the values you collected:
{
"CallbackUrl": "https://{api-id}.execute-api.us-east-1.amazonaws.com/development/events",
"APIKey": "YOUR_TRELLO_API_KEY",
"APIToken": "YOUR_TRELLO_API_TOKEN",
"ModelId": "YOUR_TRELLO_BOARD_ID"
}
- Click Save
Step 2: Create Webhook Request in Bruno
- In your Bruno collection, click on Create Request
- Name it
Create Trello Webhook - Set the request method to POST
- Enter the following URL:
https://api.trello.com/1/tokens/{{APIToken}}/webhooks/
- In the Body tab, select JSON and add:
{
"key": "{{APIKey}}",
"callbackURL": "{{CallbackUrl}}",
"idModel": "{{ModelId}}",
"description": "Trello board events"
}
DynamoDB
DynamoDB is used to store the state of all notifications. This database helps manage the status of notifications and provides the Lambda function that sends WhatsApp notifications with all the necessary information to build the message. The Create Event Scheduler Lambda function is responsible for populating the DynamoDB table.
The primary key for the DynamoDB table is cardID. Here is the DynamoDB table structure:
| Attribute | Type | Description |
|---|---|---|
| cardID | String (Primary Key) | Unique identifier for the Trello card |
| card_name | String | Name of the Trello card |
| board_name | String | Name of the Trello board |
| list_name | String | Name of the list containing the card |
| card_url | String | URL link to the Trello card |
| due_to_date | String | Due date in ISO 8601 format |
| member_creator | String | Name of the member who created the card |
| hours_before_due | Number | Hours before due date to send the notification |
| notification_type | String | Type of notification (e.g., 1_DAY_BEFORE) |
| notification_date | String | Calculated date/time when the notification should be sent |
| schedule_name | String | Name of the EventBridge scheduler |
| schedule_arn | String | ARN of the EventBridge scheduler |
| schedule_status | String | Status of the scheduler (e.g., ENABLED, DISABLED) |
| notification_sent | Boolean | Whether the notification has been sent |
| notification_sent_at | String | Timestamp when the notification was sent |
| timestamp | String | Creation timestamp of the record |
Example DynamoDB Item:
{
"cardID": "6956438536af465e3d40b380",
"card_name": "Programacion certificacion de terraform",
"board_name": "working tasks",
"list_name": "Lista de tareas",
"card_url": "https://trello.com/c/RscETyKh",
"due_to_date": "2026-02-02T10:58:00.000Z",
"member_creator": "Eliana Alejandro Morales Lopez",
"hours_before_due": 24,
"notification_type": "1_DAY_BEFORE",
"notification_date": "2026-02-01T10:58:00+00:00",
"schedule_name": "trello-reminder-6956438536af465e3d40b380-1769943480",
"schedule_arn": "arn:aws:scheduler:us-east-1:724772097129:schedule/default/trello-reminder-6956438536af465e3d40b380-1769943480",
"schedule_status": "ENABLED",
"notification_sent": true,
"notification_sent_at": "2026-01-07T14:23:54.799628",
"timestamp": "2026-01-01T10:59:09.672Z"
}
DynamoDB Streams Configuration
DynamoDB Streams is an AWS service that captures item-level modifications in a DynamoDB table and sends notifications about these changes. In this solution, DynamoDB Streams monitors changes to the due_to_date attribute. When this attribute is updated, DynamoDB Streams triggers the Lambda Update Scheduler EventBridge function.
This Lambda function receives the event from DynamoDB Streams containing the cardId of the modified item. It then:
- Retrieves the current scheduler ARN from DynamoDB
- Deletes the existing EventBridge scheduler
- Creates a new scheduler with the updated due date
- Updates the scheduler ARN in the DynamoDB table
This ensures that every time the due date is modified, the notification schedule is automatically updated with the new date.
To configure DynamoDB Streams with your Lambda function:
- Go to your DynamoDB table in the AWS console
- Click on the Exports and streams tab
- Under DynamoDB Streams, click Enable
- Select New and old images as the stream specification
AWS Lambda Functions
This solution uses three Lambda functions to handle different parts of the workflow:
Update Scheduler Lambda (DynamoDB Streams)
This Lambda function is triggered by DynamoDB Streams when the due_to_date attribute of a card is updated. It automatically updates the EventBridge scheduler with the new due date.
Workflow
- Receives an event from DynamoDB Streams containing the modified
cardID - Retrieves the current scheduler ARN and old due date from DynamoDB
- Deletes the existing EventBridge scheduler
- Calculates the new notification date based on the updated due date
- Creates a new EventBridge scheduler with the updated date
- Updates the DynamoDB record with the new scheduler ARN and schedule name
Environment Variables
The Update Scheduler Lambda function requires the following environment variables to be configured:
| Variable | Description |
|---|---|
TABLE_NAME |
The name of your DynamoDB table where card notification records are stored. Used to retrieve and update card information and scheduler details. |
SCHEDULE_ROLE_ARN |
The ARN of the IAM role that EventBridge Scheduler will assume when executing scheduled tasks. This role must have permissions to invoke the WhatsApp Notification Lambda function. |
TARGET_LAMBDA_ARN |
The ARN of the WhatsApp Notification Lambda function that will be invoked by the EventBridge scheduler when the notification time arrives. |
How to set environment variables:
- Go to your Lambda function in the AWS console
- Click on the Configuration tab
- Select Environment variables from the left menu
- Click on Edit
- Click on Add environment variable for each variable
- Enter the variable name and its corresponding value
- Click Save
IAM Policy
The Update Scheduler Lambda function requires permissions to interact with CloudWatch Logs, EventBridge Scheduler, and DynamoDB services. Attach the following policy to your Lambda execution role:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudWatchLogsPermissions",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:us-east-1:724772097129:*"
},
{
"Sid": "CloudWatchLogsStreamPermissions",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:724772097129:log-group:/aws/lambda/lambda-update-scheduler-eventbridge:*"
},
{
"Sid": "EventBridgeSchedulerPermissions",
"Effect": "Allow",
"Action": [
"scheduler:CreateSchedule",
"scheduler:GetSchedule",
"scheduler:DeleteSchedule",
"scheduler:UpdateSchedule"
],
"Resource": "arn:aws:scheduler:us-east-1:724772097129:schedule/default/trello-reminder-*"
},
{
"Sid": "DynamoDBPermissions",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:UpdateItem",
"dynamodb:Query"
],
"Resource": "arn:aws:dynamodb:us-east-1:724772097129:table/TrelloNotificationEvents"
},
{
"Sid": "DynamoDBStreamsPermissions",
"Effect": "Allow",
"Action": [
"dynamodb:GetRecords",
"dynamodb:GetShardIterator",
"dynamodb:DescribeStream",
"dynamodb:ListStreams",
"dynamodb:ListShards"
],
"Resource": "arn:aws:dynamodb:us-east-1:724772097129:table/TrelloNotificationEvents/stream/*"
},
{
"Sid": "PassRoleToScheduler",
"Effect": "Allow",
"Action": [
"iam:PassRole"
],
"Resource": "arn:aws:iam::724772097129:role/eventbridgeSchedulerRole",
"Condition": {
"StringEquals": {
"iam:PassedToService": "scheduler.amazonaws.com"
}
}
}
]
}
import json
import boto3
from datetime import datetime, timedelta
import os
# AWS Clients
scheduler_client = boto3.client('scheduler')
dynamodb = boto3.resource('dynamodb')
# Environment Variables
TABLE_NAME = os.environ.get('TABLE_NAME', 'TrelloNotificationEvents')
TARGET_LAMBDA_ARN = os.environ.get('TARGET_LAMBDA_ARN')
SCHEDULE_ROLE_ARN = os.environ.get('SCHEDULE_ROLE_ARN')
def lambda_handler(event, context):
"""
Triggered by DynamoDB Streams when due_to_date is updated.
Updates the EventBridge scheduler with the new due date.
"""
try:
print(f"📥 Received DynamoDB Streams event: {json.dumps(event)}")
# Process each DynamoDB stream record
for record in event.get('Records', []):
if record['eventName'] != 'MODIFY':
print(f"⏭️ Skipping non-MODIFY event: {record['eventName']}")
continue
# Extract the card ID and new data
dynamodb_record = record['dynamodb']
card_id = dynamodb_record['Keys']['cardID']['S']
# Get new and old image
new_image = dynamodb_record.get('NewImage', {})
old_image = dynamodb_record.get('OldImage', {})
# Check if due_to_date was actually changed
old_due_date_str = old_image.get('due_to_date', {}).get('S')
new_due_date_str = new_image.get('due_to_date', {}).get('S')
if old_due_date_str == new_due_date_str:
print(f"ℹ️ Due date not changed for card {card_id}, skipping")
continue
print(f"🔄 Due date updated for card: {card_id}")
print(f" Old due date: {old_due_date_str}")
print(f" New due date: {new_due_date_str}")
# Get the current scheduler information from the new image
old_schedule_arn = old_image.get('schedule_arn', {}).get('S')
old_schedule_name = old_image.get('schedule_name', {}).get('S')
card_name = new_image.get('card_name', {}).get('S', 'Unknown')
# Step 1: Delete the old schedule
if old_schedule_name:
try:
print(f"🗑️ Deleting old schedule: {old_schedule_name}")
scheduler_client.delete_schedule(
Name=old_schedule_name
)
print(f"✅ Old schedule deleted: {old_schedule_name}")
except scheduler_client.exceptions.ResourceNotFoundException:
print(f"⚠️ Schedule not found: {old_schedule_name}")
except Exception as e:
print(f"⚠️ Error deleting schedule: {str(e)}")
# Step 2: Parse the new due date
new_due_date = datetime.fromisoformat(new_due_date_str.replace('Z', '+00:00'))
now = datetime.now(new_due_date.tzinfo)
# Calculate time until due date
time_until_due = new_due_date - now
print(f"⏰ New due date: {new_due_date.isoformat()}")
print(f"📊 Time until due: {time_until_due}")
# Step 3: Determine notification date based on new due date
if time_until_due <= timedelta(hours=12):
print(f"⚠️ New due date is less than 12 hours away, schedule not updated")
# Update DynamoDB to mark schedule as deleted
table = dynamodb.Table(TABLE_NAME)
table.update_item(
Key={'cardID': card_id},
UpdateExpression='SET schedule_status = :status, notification_sent = :sent',
ExpressionAttributeValues={
':status': 'CANCELLED',
':sent': True
}
)
continue
elif time_until_due <= timedelta(days=1):
notification_date = new_due_date - timedelta(hours=12)
notification_type = "12_HOURS_BEFORE"
hours_before = 12
print(f"📅 New due date is within 24 hours, scheduling for 12 hours before")
else:
notification_date = new_due_date - timedelta(days=1)
notification_type = "1_DAY_BEFORE"
hours_before = 24
print(f"📅 New due date is more than 24 hours away, scheduling for 1 day before")
# Verify notification date is not in the past
if notification_date <= now:
print(f"⚠️ New notification date is in the past: {notification_date}")
table = dynamodb.Table(TABLE_NAME)
table.update_item(
Key={'cardID': card_id},
UpdateExpression='SET schedule_status = :status, notification_sent = :sent',
ExpressionAttributeValues={
':status': 'CANCELLED',
':sent': True
}
)
continue
# Step 4: Create a new schedule
new_schedule_name = f"trello-reminder-{card_id}-{int(notification_date.timestamp())}"
schedule_expression = notification_date.strftime('at(%Y-%m-%dT%H:%M:%S)')
target_payload = {
'cardID': card_id
}
try:
print(f"📅 Creating new schedule: {new_schedule_name}")
new_schedule_response = scheduler_client.create_schedule(
Name=new_schedule_name,
Description=f"Updated reminder for Trello card: {card_name} ({notification_type})",
ScheduleExpression=schedule_expression,
ScheduleExpressionTimezone='UTC',
FlexibleTimeWindow={
'Mode': 'OFF'
},
Target={
'Arn': TARGET_LAMBDA_ARN,
'RoleArn': SCHEDULE_ROLE_ARN,
'Input': json.dumps(target_payload)
},
State='ENABLED'
)
new_schedule_arn = new_schedule_response.get('ScheduleArn')
print(f"✅ New schedule created: {new_schedule_name}")
print(f"📍 New schedule ARN: {new_schedule_arn}")
# Step 5: Update DynamoDB with the new scheduler information
table = dynamodb.Table(TABLE_NAME)
table.update_item(
Key={'cardID': card_id},
UpdateExpression='SET schedule_name = :sname, schedule_arn = :sarn, schedule_status = :status, notification_date = :ndate, notification_type = :ntype, hours_before_due = :hours, due_to_date = :duedate',
ExpressionAttributeValues={
':sname': new_schedule_name,
':sarn': new_schedule_arn,
':status': 'ENABLED',
':ndate': notification_date.isoformat(),
':ntype': notification_type,
':hours': hours_before,
':duedate': new_due_date_str
}
)
print(f"✅ DynamoDB updated with new schedule: {card_id}")
except scheduler_client.exceptions.ConflictException as e:
print(f"⚠️ Schedule already exists: {str(e)}")
return {
'statusCode': 409,
'body': json.dumps({
'error': 'Schedule already exists',
'message': str(e)
})
}
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Schedules updated successfully',
'processedRecords': len(event.get('Records', []))
})
}
except Exception as e:
print(f"❌ Error processing DynamoDB Streams event: {str(e)}")
import traceback
traceback.print_exc()
return {
'statusCode': 500,
'body': json.dumps({
'error': str(e)
})
}
#### Associate lambda with the DynamoDB Streams
- Go to your Lambda Update Scheduler EventBridge function
- Click on Configuration → Triggers
- Click Add trigger and select DynamoDB
- Select your DynamoDB table and stream
- Click Add
2. Create Event Scheduler Lambda
This Lambda function processes incoming events from the Trello webhook. It uses the cardId as the primary key to check if a notification already exists in DynamoDB.
Workflow:
- Receives the JSON payload from the Trello webhook
- Checks if a record with the
cardIdalready exists in DynamoDB -
If the notification doesn't exist:
- Creates a new EventBridge scheduler with the card's due date
- Stores the scheduler ARN in DynamoDB
-
If the notification already exists:
- Updates the
due_to_dateattribute in the DynamoDB record (which triggers DynamoDB Streams)
- Updates the
Environment variables:
The Create Event Scheduler Lambda function requires the following environment variables to be configured:
| Variable | Description |
|---|---|
SCHEDULE_ROLE_ARN |
The ARN of the IAM role that EventBridge Scheduler will assume when executing scheduled tasks. This role must have permissions to invoke the WhatsApp Notification Lambda function. |
TABLE_NAME |
The name of your DynamoDB table where card notification records are stored. This is used to store and retrieve card information along with scheduler details. |
TARGET_LAMBDA_ARN |
The ARN of the WhatsApp Notification Lambda function that will be invoked by the EventBridge scheduler when the notification time arrives. |
Required IAM permissions:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudWatchLogsPermissions",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:us-east-1:724772097129:*"
},
{
"Sid": "CloudWatchLogsStreamPermissions",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:724772097129:log-group:/aws/lambda/lambda-create-record-notification-trello:*"
},
{
"Sid": "EventBridgeSchedulerPermissions",
"Effect": "Allow",
"Action": [
"scheduler:CreateSchedule",
"scheduler:GetSchedule",
"scheduler:DeleteSchedule"
],
"Resource": "arn:aws:scheduler:us-east-1:724772097129:schedule/default/trello-reminder-*"
},
{
"Sid": "DynamoDBPermissions",
"Effect": "Allow",
"Action": [
"dynamodb:PutItem",
"dynamodb:GetItem",
"dynamodb:Query",
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:724772097129:table/TrelloNotificationEvents"
},
{
"Sid": "PassRoleToScheduler",
"Effect": "Allow",
"Action": [
"iam:PassRole"
],
"Resource": "arn:aws:iam::724772097129:role/eventbridgeSchedulerRole",
"Condition": {
"StringEquals": {
"iam:PassedToService": "scheduler.amazonaws.com"
}
}
}
]
}
Create Event Scheduler Lambda Code
Here is the complete code for the Create Event Scheduler Lambda function:
import json
import boto3
from datetime import datetime, timedelta
import os
# AWS Clients
scheduler_client = boto3.client('scheduler')
dynamodb = boto3.resource('dynamodb')
# Environment Variables
TABLE_NAME = os.environ.get('TABLE_NAME', 'TrelloNotificationEvents')
TARGET_LAMBDA_ARN = os.environ.get('TARGET_LAMBDA_ARN') # ARN of the Lambda that will send notifications
SCHEDULE_ROLE_ARN = os.environ.get('SCHEDULE_ROLE_ARN') # Role for EventBridge Scheduler
def lambda_handler(event, context):
"""
Receives a Trello event and creates a schedule in EventBridge
to execute 1 day before the due date (or 12 hours if due date is very soon)
"""
try:
# Extract data from the event
action = event.get('action', {})
card_data = action.get('data', {}).get('card', {})
card_id = card_data.get('id')
card_name = card_data.get('name')
due_date_str = card_data.get('due')
if not card_id or not due_date_str:
return {
'statusCode': 400,
'body': json.dumps('Missing cardID or due date')
}
print(f"🔍 Processing card: {card_name} (ID: {card_id})")
# Parse the due date
due_date = datetime.fromisoformat(due_date_str.replace('Z', '+00:00'))
# Get the current time
now = datetime.now(due_date.tzinfo)
# Calculate the time difference between now and due date
time_until_due = due_date - now
print(f"⏰ Due date: {due_date.isoformat()}")
print(f"📊 Time until due: {time_until_due}")
# Determine when to send the notification
if time_until_due <= timedelta(hours=12):
# If due date is less than 12 hours away, don't schedule anything
print(f"⚠️ Due date is less than 12 hours away, notification not scheduled")
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Due date is too soon (< 12 hours), notification not scheduled',
'cardID': card_id,
'dueDate': due_date_str,
'hoursUntilDue': time_until_due.total_seconds() / 3600
})
}
elif time_until_due <= timedelta(days=1):
# If due date is less than 24 hours but more than 12, schedule for 12 hours before
notification_date = due_date - timedelta(hours=12)
notification_type = "12_HOURS_BEFORE"
print(f"📅 Due date is within 24 hours, scheduling for 12 hours before")
else:
# If due date is more than 24 hours away, schedule for 1 day before
notification_date = due_date - timedelta(days=1)
notification_type = "1_DAY_BEFORE"
print(f"📅 Due date is more than 24 hours away, scheduling for 1 day before")
# Verify that the notification date is not in the past
if notification_date <= now:
print(f"⚠️ Notification date is in the past: {notification_date}")
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Calculated notification date is in the past',
'cardID': card_id,
'notificationDate': notification_date.isoformat(),
'currentTime': now.isoformat()
})
}
# Calculate time until notification
time_until_notification = notification_date - now
hours_until_notification = time_until_notification.total_seconds() / 3600
print(f"⏰ Notification scheduled for: {notification_date.isoformat()}")
print(f"⏳ Time until notification: {hours_until_notification:.2f} hours")
# Create a unique name for the schedule
schedule_name = f"trello-reminder-{card_id}-{int(notification_date.timestamp())}"
# Date format for EventBridge Scheduler (at expression)
schedule_expression = notification_date.strftime('at(%Y-%m-%dT%H:%M:%S)')
# Payload to be sent to the notification Lambda
target_payload = {
'cardID': card_id
}
# 1️⃣ FIRST: Create the schedule in EventBridge
print(f"📅 Creating schedule: {schedule_name}")
schedule_response = scheduler_client.create_schedule(
Name=schedule_name,
Description=f"Reminder for Trello card: {card_name} ({notification_type})",
ScheduleExpression=schedule_expression,
ScheduleExpressionTimezone='UTC',
FlexibleTimeWindow={
'Mode': 'OFF'
},
Target={
'Arn': TARGET_LAMBDA_ARN,
'RoleArn': SCHEDULE_ROLE_ARN,
'Input': json.dumps(target_payload)
},
State='ENABLED'
)
# Get the ARN of the created schedule
schedule_arn = schedule_response.get('ScheduleArn')
print(f"✅ Schedule created: {schedule_name}")
print(f"📍 Schedule ARN: {schedule_arn}")
# 2️⃣ THEN: Save to DynamoDB with the schedule_arn
table = dynamodb.Table(TABLE_NAME)
timestamp = action.get('date')
# Build the card URL
card_short_link = card_data.get('shortLink')
card_url = f"https://trello.com/c/{card_short_link}" if card_short_link else ""
dynamodb_item = {
'cardID': card_id,
'timestamp': timestamp,
'card_name': card_name,
'card_url': card_url,
'due_to_date': due_date_str,
'notification_date': notification_date.isoformat(),
'notification_type': notification_type,
'hours_before_due': 12 if notification_type == "12_HOURS_BEFORE" else 24,
'board_name': action.get('data', {}).get('board', {}).get('name', ''),
'list_name': action.get('data', {}).get('list', {}).get('name', ''),
'member_creator': action.get('memberCreator', {}).get('fullName', ''),
'schedule_name': schedule_name,
'schedule_arn': schedule_arn,
'schedule_status': 'ENABLED'
}
table.put_item(Item=dynamodb_item)
print(f"✅ Saved to DynamoDB: {card_id}")
print(f"🎯 Payload for notification: {target_payload}")
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Schedule created and saved successfully',
'scheduleName': schedule_name,
'scheduleArn': schedule_arn,
'notificationDate': notification_date.isoformat(),
'notificationType': notification_type,
'hoursBeforeDue': 12 if notification_type == "12_HOURS_BEFORE" else 24,
'hoursUntilNotification': round(hours_until_notification, 2),
'cardID': card_id,
'cardName': card_name,
'dueDate': due_date_str
})
}
except scheduler_client.exceptions.ConflictException as e:
# If the schedule already exists
print(f"⚠️ Schedule already exists: {str(e)}")
return {
'statusCode': 409,
'body': json.dumps({
'error': 'Schedule already exists',
'message': str(e)
})
}
except Exception as e:
print(f"❌ Error: {str(e)}")
import traceback
traceback.print_exc()
return {
'statusCode': 500,
'body': json.dumps({
'error': str(e),
'cardID': card_id if 'card_id' in locals() else None
})
}
3. WhatsApp Notification Lambda
This Lambda function is invoked by the EventBridge scheduler when the due date is one day before. It retrieves the card information from DynamoDB and sends a WhatsApp notification using the Meta WhatsApp API.
Workflow
- Receives the
cardIDfrom the EventBridge scheduler - Retrieves the card information from DynamoDB using the cardID
- Fetches WhatsApp credentials from AWS Parameter Store (Phone ID, API Token, and recipient phone number)
- Formats the due date into a readable format (DD/MM/YYYY HH:MM)
- Constructs the WhatsApp message using the
trello_notificationtemplate with dynamic parameters - Sends the WhatsApp message via Meta WhatsApp API
- Updates the DynamoDB record to mark the notification as sent with the current timestamp
- Returns a success response with the message details
Environment Variables
The WhatsApp Notification Lambda function requires the following environment variables to be configured:
| Variable | Description |
|---|---|
TABLE_NAME |
The name of your DynamoDB table where card notification records are stored. Used to retrieve card information and update the notification status. |
WHATSAPP_PHONE_ID_PARAMETER |
The AWS Parameter Store key name for the WhatsApp Phone ID (e.g., /whatsappApiNumberId). This ID is required to send messages via the Meta WhatsApp API. |
WHATSAPP_TOKEN_PARAMETER |
The AWS Parameter Store key name for the WhatsApp API Token (e.g., /whatsappToken). This is the authentication token for the Meta WhatsApp API. |
RECIPIENT_PHONE_PARAMETER |
The AWS Parameter Store key name for the recipient phone number (e.g., /WhatsAppNumberEliana). This is the phone number that will receive the WhatsApp notifications in international format. |
IAM Policy
The WhatsApp Notification Lambda function requires permissions to interact with CloudWatch Logs, DynamoDB, and AWS Systems Manager Parameter Store. Attach the following combined policy to your Lambda execution role:
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "CloudWatchLogsPermissions",
"Effect": "Allow",
"Action": "logs:CreateLogGroup",
"Resource": "arn:aws:logs:us-east-1:724772097129:*"
},
{
"Sid": "CloudWatchLogsStreamPermissions",
"Effect": "Allow",
"Action": [
"logs:CreateLogStream",
"logs:PutLogEvents"
],
"Resource": "arn:aws:logs:us-east-1:724772097129:log-group:/aws/lambda/lambda-whatsapp-notification:*"
},
{
"Sid": "DynamoDBAccess",
"Effect": "Allow",
"Action": [
"dynamodb:GetItem",
"dynamodb:UpdateItem"
],
"Resource": "arn:aws:dynamodb:us-east-1:724772097129:table/TrelloNotificationEvents"
},
{
"Sid": "SSMParameterAccess",
"Effect": "Allow",
"Action": [
"ssm:GetParameter"
],
"Resource": [
"arn:aws:ssm:us-east-1:724772097129:parameter/whatsappApiNumberId",
"arn:aws:ssm:us-east-1:724772097129:parameter/whatsappToken",
"arn:aws:ssm:us-east-1:724772097129:parameter/WhatsAppNumberEliana"
]
}
]
}
import json
import boto3
import os
import requests
from datetime import datetime
from botocore.exceptions import ClientError
# AWS Clients
dynamodb = boto3.resource('dynamodb')
ssm_client = boto3.client('ssm')
# Environment Variables
TABLE_NAME = os.environ.get('TABLE_NAME')
WHATSAPP_PHONE_ID_PARAMETER = os.environ.get('WHATSAPP_PHONE_ID_PARAMETER')
WHATSAPP_TOKEN_PARAMETER = os.environ.get('WHATSAPP_TOKEN_PARAMETER')
RECIPIENT_PHONE_PARAMETER = os.environ.get('RECIPIENT_PHONE_PARAMETER')
def get_parameter(parameter_name):
"""
Retrieves a parameter from AWS Parameter Store
"""
try:
response = ssm_client.get_parameter(
Name=parameter_name,
WithDecryption=False
)
return response['Parameter']['Value']
except ClientError as e:
print(f"❌ Error getting parameter {parameter_name} from Parameter Store: {str(e)}")
raise
def get_whatsapp_phone_id():
"""
Gets the WhatsApp Phone ID from Parameter Store
"""
try:
return get_parameter(WHATSAPP_PHONE_ID_PARAMETER)
except ClientError:
print(f"⚠️ Using default WHATSAPP_PHONE_ID: 926715197193624")
return '926715197193624'
def get_recipient_phone():
"""
Gets the recipient phone number from Parameter Store
"""
try:
return get_parameter(RECIPIENT_PHONE_PARAMETER)
except ClientError:
print(f"⚠️ Using default RECIPIENT_PHONE: 573123234567")
return '573123234567'
def get_whatsapp_token():
"""
Gets the WhatsApp API token from Parameter Store
"""
try:
return get_parameter(WHATSAPP_TOKEN_PARAMETER)
except ClientError as e:
print(f"❌ Error getting token from Parameter Store: {str(e)}")
raise
def format_date(date_str):
"""
Formats ISO date to readable Spanish format
Example: 2026-02-02T10:58:00.000Z -> 02/02/2026 a las 10:58
"""
try:
date_obj = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
return date_obj.strftime('%d/%m/%Y a las %H:%M')
except Exception as e:
print(f"⚠️ Error formatting date: {str(e)}")
return date_str
def send_whatsapp_message(token, phone_number, card_name, board_name, list_name, due_date, card_url, whatsapp_phone_id):
"""
Sends a WhatsApp message using the trello_notification template with dynamic parameters
"""
url = f"https://graph.facebook.com/v23.0/{whatsapp_phone_id}/messages"
headers = {
'Authorization': f'Bearer {token}',
'Content-Type': 'application/json'
}
payload = {
"messaging_product": "whatsapp",
"to": phone_number,
"type": "template",
"template": {
"name": "trello_notification",
"language": {
"code": "es_CO"
},
"components": [
{
"type": "header",
"parameters": [
{"type": "text", "text": "Recordatorio de Trello"}
]
},
{
"type": "body",
"parameters": [
{"type": "text", "text": card_name},
{"type": "text", "text": board_name},
{"type": "text", "text": list_name},
{"type": "text", "text": due_date},
{"type": "text", "text": card_url}
]
}
]
}
}
try:
print(f"📤 Sending WhatsApp message to {phone_number}")
print(f"📤 Payload: {json.dumps(payload)}")
response = requests.post(url, headers=headers, json=payload, timeout=10)
print(f"📥 Response status: {response.status_code}")
print(f"📥 Response body: {response.text}")
response.raise_for_status()
return response.json()
except requests.exceptions.RequestException as e:
print(f"❌ Error sending WhatsApp message: {str(e)}")
if hasattr(e, 'response') and e.response is not None:
print(f"Response: {e.response.text}")
raise
def lambda_handler(event, context):
"""
Receives the cardID from the scheduler, queries DynamoDB, and sends a WhatsApp notification
"""
try:
print(f"📥 Received event: {json.dumps(event)}")
# Extract cardID from the event
card_id = event.get('cardID')
if not card_id:
print("❌ Missing cardID in payload")
return {
'statusCode': 400,
'body': json.dumps({'error': 'Missing cardID in payload'})
}
print(f"🔍 Getting item with cardID: {card_id}")
# Get the record from DynamoDB
table = dynamodb.Table(TABLE_NAME)
response = table.get_item(
Key={'cardID': card_id}
)
if 'Item' not in response:
print(f"⚠️ Card not found in database: {card_id}")
return {
'statusCode': 404,
'body': json.dumps({'error': 'Card not found in database'})
}
# Get card data
card_data = response['Item']
card_name = card_data.get('card_name', 'Sin nombre')
due_date_str = card_data.get('due_to_date', '')
card_url = card_data.get('card_url', '')
board_name = card_data.get('board_name', '')
list_name = card_data.get('list_name', '')
print(f"📋 Card found: {card_name}")
print(f"📅 Due date: {due_date_str}")
print(f"🔗 URL: {card_url}")
# Format the due date
due_date_formatted = format_date(due_date_str)
# Get parameters from Parameter Store
print("🔐 Getting parameters from Parameter Store...")
whatsapp_phone_id = get_whatsapp_phone_id()
recipient_phone = get_recipient_phone()
token = get_whatsapp_token()
# Send WhatsApp message with individual parameters
print(f"📤 Sending message to {recipient_phone}...")
result = send_whatsapp_message(
token,
recipient_phone,
card_name,
board_name,
list_name,
due_date_formatted,
card_url,
whatsapp_phone_id
)
print(f"✅ Message sent successfully: {result}")
# Update DynamoDB record to mark notification as sent
table.update_item(
Key={'cardID': card_id},
UpdateExpression='SET notification_sent = :val, notification_sent_at = :date',
ExpressionAttributeValues={
':val': True,
':date': datetime.utcnow().isoformat()
}
)
print(f"✅ DynamoDB updated with notification status for cardID: {card_id}")
return {
'statusCode': 200,
'body': json.dumps({
'message': 'Notification sent successfully',
'cardID': card_id,
'recipientPhone': recipient_phone,
'result': result
})
}
except Exception as e:
print(f"❌ Error in lambda_handler: {str(e)}")
import traceback
traceback.print_exc()
return {
'statusCode': 500,
'body': json.dumps({'error': str(e)})
}
WhatsApp API Configuration
Create and Verify Meta Business Account
To use the WhatsApp API, you need to create and verify a Meta Business Account. This account will allow you to access the WhatsApp Cloud API and manage your WhatsApp Business integration.
Step 1: Access Meta Business Platform
- Go to the Meta Business Platform Login
- If you don't have a Meta account, click on Create Account
- Enter your email address and create a secure password
- Click Create Account
Step 2: Set Up Your Business Account
- After logging in, you'll be prompted to create a Meta Business Account
- Fill in the following information:
- Business Account Name: Enter a name for your business (e.g., "Trello WhatsApp Notifications")
- Your Name: Enter your full name
- Business Email: Use a business email address
- Business Phone Number: Enter your phone number
- Country: Select your country
- Click Create Account
Step 3: Verify Your Business Information
- Once your account is created, go to Settings → Business Information
-
Complete the following verification steps:
- Verify your email address: Check your email inbox for a verification link from Meta and click it
- Add a phone number: Enter and verify your phone number
-
Upload business documents (if required):
- Government-issued ID
- Business registration documents
- Proof of address
Click Submit for Review once all information is complete
Step 4: Wait for Approval
- Meta will review your information (usually takes 24-48 hours)
- You'll receive an email notification once your account is verified
- Once approved, your business account will be fully active
Step 5: Access the Developer Platform
- After verification, go to Meta Developers
- Log in with your Meta Business Account credentials
- Click on My Apps in the top menu
- Click Create App to create a new app for WhatsApp integration
Step 6: Create a New App
Create a Meta App for WhatsApp Integration
Follow these steps to create a Meta App that will be used for WhatsApp Cloud API integration.
Step 1: App Details
- In the App Details section, fill in the following information:
- Email: Enter a valid email address associated with your Meta Business Account
- App Name: Enter a name for your app (e.g., "Trello WhatsApp Notifications")
- Click Next
Step 2: Select Use Case
- In the Use Cases section, you'll see different options for what you want to do with your app
- Select Connect with customers through WhatsApp
- This option allows you to use the WhatsApp Cloud API to send messages
- Click Next
Step 3: Select Business Portfolio
- In the Business section, select your business portfolio
- This is the Meta Business Account you created and verified in the previous steps
- Make sure to select the correct portfolio associated with your business
- Click Next
Step 4: Check Requirements
- In the Requirements section, Meta will display any requirements you need to fulfill
- Review each requirement carefully:
- Business Account Verification: Make sure your business account is verified
- Phone Number: Ensure you have a valid phone number registered
- Other Requirements: Complete any additional requirements listed
- If all requirements are met, you can proceed directly to the next step
- If there are unmet requirements, complete them first
- Click Next once all requirements are satisfied
Step 5: Summary and Complete Setup
- In the Summary section, you'll see a review of all the information you provided:
- App Name
- Use Case
- Business Portfolio
- Requirements Status
- Review all the details to ensure they are correct
- Click Go to Dashboard to complete the app creation and access your app dashboard
Configure Use Case for WhatsApp Integration
Now that you've created your Meta App, you need to configure the WhatsApp use case to enable the WhatsApp Cloud API integration.
Step 1: Access Your App
- Go to Meta Developers
- Click on My Apps in the top menu
- Select the app you just created (e.g., "Trello WhatsApp Notifications")
- You'll be taken to your app dashboard
Step 2: Navigate to Use Cases
- In your app dashboard, look for the Use Cases section in the left sidebar
- Click on Use Cases
- You'll see a list of available use cases for your app
Step 3: Add a New Use Case
- Click on Add Use Case button
- A list of available use cases will appear
Step 4: Select WhatsApp Use Case
- From the list of available use cases, find and select Connect with customers through WhatsApp
- This use case enables you to send messages to your customers via WhatsApp using the Cloud API
- Click Add to add this use case to your app
Step 4: Create System User and Generate Permanent Access Token
Go to this link Click here to access your Meta Business Settings
You need to create a permanent access token to use in the WhatsApp Business platform. This token will be used to authenticate API requests to send WhatsApp messages.
Step 1: Navigate to System Users
- Go to Business Settings in your Meta Business Account
- In the left sidebar, click on System Users
- You'll see a list of existing system users (if any)
Step 2: Create a New System User
- Click on the Add + button in the top right corner
- A dialog will appear asking for system user details
- Fill in the following information:
- System User Name: Enter a name for your system user (e.g., "Trello WhatsApp Bot")
- System User Role: Select Admin
- Click Create System User
Step 3: Assign Assets to the System User
- Select the system user you just created
- Click on Assign Assets button
- You'll be taken to the asset assignment page
Step 4: Assign Your App
- In the Apps section:
- Select your WhatsApp app (e.g., "Trello WhatsApp Notifications")
- Enable Manage app with Full Control permissions
- Click Assign Assets
Step 5: Assign Your WhatsApp Business Account
- In the WhatsApp Business Accounts section:
- Select your WhatsApp Business Account
- Enable Manage WhatsApp Business Accounts with Full Control permissions
- Click Assign Assets
Step 6: Generate Access Token
- Go back to the system user you created
- Click on Generate Token button
- A dialog will appear with token generation options
- Select your app
- Set the token expiration (choose Never for a permanent token)
- Select the following scopes (permissions) for your token:
- ✅ business_management: Allows management of your business account
- ✅ whatsapp_business_messaging: Allows sending WhatsApp messages
- ✅ whatsapp_business_management: Allows management of WhatsApp Business Account
- Click Generate Token
Step 7: Copy and Secure Your Token
- Copy the generated token from the dialog
- Important: Save this token in a secure location immediately
- Store it in AWS Parameter Store as you'll need it for the Lambda function
- Never share this token publicly or commit it to version control
Security Best Practices:
⚠️ Secure Storage: Always store your token in AWS Parameter Store or AWS Secrets Manager with encryption enabled
⚠️ Token Expiration: Consider setting an expiration date for your token and rotating it periodically
⚠️ Access Control: Restrict who has access to this token. Never expose it in logs or error messages
⚠️ Revocation: If your token is compromised, immediately revoke it and generate a new one
Step 8: Store Token in AWS Parameter Store
Once you have your access token, store it securely in AWS Parameter Store:
- Go to AWS Systems Manager → Parameter Store
- Click Create parameter
- Fill in the following details:
-
Name:
/whatsappToken -
Type:
SecureString(for encryption) - Value: Paste your access token here
-
Name:
- Click Create parameter
This token will be used by the WhatsApp Notification Lambda function to authenticate API requests to the Meta WhatsApp API.
Token Validation
To verify your token is working correctly, you can test it by making a simple API request:
curl -X GET "https://graph.facebook.com/v18.0/me?fields=name,email&access_token=YOUR_ACCESS_TOKEN"
If successful, you'll receive a JSON response with your business account information.
Create WhatsApp Message Template
You need to create a WhatsApp message template that will be used to send Trello notifications. This template must be approved by Meta before you can use it to send messages.
Step 1: Access Message Templates
- Click here to access the WhatsApp Message Templates manager
- You'll see a list of your existing message templates (if any)
- Click on Create Template button to create a new template
Step 2: Select Template Category
- In the Category section, select Utility
- Utility templates are used for transactional messages like notifications, reminders, and alerts
- This is the appropriate category for Trello notification reminders
- Click Next
Step 3: Name Your Template and Select Language
- Fill in the template details:
-
Template Name: Enter a name for your template (e.g.,
trello_notification) - Language: Select your preferred language (e.g., Spanish - es_CO, English - en_US)
-
Template Name: Enter a name for your template (e.g.,
- The template name should be descriptive and in lowercase with underscores
- Click Next
Step 4: Create Template Body with Variables
Now you'll create the message body that will be sent to users. This template will include 5 dynamic variables that will be replaced with actual Trello card information.
Variables:
-
{{1}}- Task name (tarea) -
{{2}}- Board name (tablero) -
{{3}}- List name (lista) -
{{4}}- Due date (vence) -
{{5}}- Card URL (url)
In the Message Body section, enter the following template text:
🎯 *Trello Reminder Notification*
Task: {{1}}
Board: {{2}}
List: {{3}}
Due Date: {{4}}
View Task: {{5}}
Please complete this task on time!
Example with Sample Values:
For Meta's review process, provide example values:
- Task: "Complete project documentation"
- Board: "Working Tasks"
- List: "In Progress"
- Due Date: "02/02/2026 at 10:58 AM"
- Card URL: "https://trello.com/c/RscETyKh"
The final message will look like:
🎯 *Trello Reminder Notification*
Task: Complete project documentation
Board: Working Tasks
List: In Progress
Due Date: 02/02/2026 at 10:58 AM
View Task: https://trello.com/c/RscETyKh
Please complete this task on time!
Click Next to continue
Step 6: Review and Submit for Approval
- Review all template details:
- Template Name:
trello_notification - Category: Utility
- Language: Your selected language
- Body with variables
- Header (if added)
- Footer (if added)
- Template Name:
- Make sure all the information is correct
- Click Submit for Review to send your template to Meta for approval
Important Notes About Template Approval
⏱️ Approval Time: Meta's review process typically takes 24-48 hours, but can take up to 2 days in some cases
📋 Review Criteria: Meta will review your template to ensure it:
- Complies with WhatsApp's messaging policies
- Doesn't contain prohibited content
- Uses appropriate language for the category
- Follows WhatsApp's brand guidelines
✅ Approval Status: You'll receive a notification when your template is approved. You can also check the status in the Message Templates manager
Step 5: Get WhatsApp Phone Number ID
Meta provides a test phone number for your WhatsApp Business Account. You need to retrieve the Phone Number ID and store it securely in AWS Parameter Store.
Step 1: Navigate to Phone Numbers Section
- Go to your WhatsApp Business Account dashboard
- In the left sidebar, click on Phone Numbers
- You'll see the test phone number that Meta provides for your account
- It will be displayed in a format like:
+1 (XXX) XXX-XXXXor similar
Step 2: Select Your WhatsApp Test Phone Number
- Click on the phone number to view its details
- A panel will open showing the phone number configuration and information
- Look for the Phone Number ID field in the details panel
- This is the unique identifier you need to store in AWS Parameter Store
Step 3: Copy the Phone Number ID
- In the phone number details, locate the Phone Number ID field
- This is a numeric ID (e.g.,
926715197193624) - Click on the copy icon next to the Phone Number ID or manually select and copy it
- Save this value temporarily as you'll need it in the next step
Step 4: Store Phone Number ID in AWS Parameter Store
Now you'll store the Phone Number ID securely in AWS Parameter Store:
- Go to AWS Systems Manager in your AWS console
- Click on Parameter Store in the left sidebar
- Click Create parameter
- Fill in the following details:
-
Name:
/whatsappApiNumberId -
Type:
String - Description: "WhatsApp Business Phone Number ID for Trello notifications"
-
Value: Paste the Phone Number ID you copied from Meta (e.g.,
9267151971935456)
-
Name:
- Click Create parameter
Step 5: Verify Parameter Storage
- You should see a success message confirming the parameter was created
- Navigate to Parameter Store to verify your parameter appears in the list
- You should see:
- Parameter Name:
/whatsappApiNumberId - Type:
String - Last Modified Date: Current date
- Parameter Name:
AWS Parameter Store Configuration
Your AWS Parameter Store should now contain:
| Parameter Name | Type | Value | Description |
|---|---|---|---|
/whatsappApiNumberId |
String | Your Phone Number ID | WhatsApp Business Phone Number ID |
/whatsappToken |
SecureString | Your Access Token | WhatsApp API Access Token |
/WhatsAppNumberEliana |
String | Recipient Phone Number | Recipient phone number for notifications |
Important Notes
⚠️ Phone Number ID Format: The Phone Number ID is a numeric string without any special characters or formatting
⚠️ Test vs Production: Meta provides a test phone number for development and testing. For production use, you'll need to set up a dedicated phone number
⚠️ Recipient Phone Number: Make sure the recipient phone number is also stored in Parameter Store as /WhatsAppNumberEliana in international format (e.g., 573123234567)
⚠️ Security: Keep your Phone Number ID and Access Token secure. These credentials authenticate all API requests to the WhatsApp API
Next Steps
Now that you have all three credentials stored in AWS Parameter Store:
- ✅ Phone Number ID:
/whatsappApiNumberId - ✅ Access Token:
/whatsappToken - ✅ Recipient Phone Number:
/WhatsAppNumberEliana
Step 6: Authorize WhatsApp Number for Testing
Now you need to authorize your WhatsApp number to receive and review messages from your assigned test number. This step is required to test the WhatsApp notification integration.
Step 1: Access Use Cases Configuration
- Go to your Meta App Dashboard
- In the left sidebar, click on Use Cases
- Find and click on Connect with customers through WhatsApp
- Click on API Setup or Configuration
- You'll see the WhatsApp API configuration page
Step 2: Add a Phone Number
- In the API configuration page, look for the section Phone Numbers or Add Phone Number
- Click on the dropdown menu that says Select a phone number to add or Add Phone Number
- This dropdown will show available phone numbers or an option to add a new one
Step 3: Enter Your Phone Number
- In the dropdown or input field, enter your phone number in international format
- Format: Country code + phone number
- Make sure to include:
- Country code (without the + symbol)
- Phone number without spaces or special characters
- Click Next or Add
Step 4: Enter Verification Code
- Meta will send a verification code to the phone number you provided
- You'll receive an whatsapp message with a code (usually 6 digits)
- Enter the verification code in the dialog box that appears
- Click Verify to confirm your phone number
Create Step Function
Template Definition
# filepath: template.yaml
Resources:
StateMachine80541e5e:
Type: AWS::StepFunctions::StateMachine
Properties:
Definition:
Comment: A description of my state machine
StartAt: trello event type
States:
trello event type:
Type: Choice
Choices:
- Next: Lambda Invoke
Condition: >-
{% (($states.input.action.type) = ("updateCard") and
($states.input.action.display.translationKey) =
("action_added_a_due_date")) %}
Default: Success
Lambda Invoke:
Type: Task
Resource: arn:aws:states:::lambda:invoke
Output: '{% $states.result.Payload %}'
Arguments:
FunctionName: ${lambdainvoke_FunctionName_ccca2377}
Payload: '{% $states.input %}'
Retry:
- ErrorEquals:
- Lambda.ServiceException
- Lambda.AWSLambdaException
- Lambda.SdkClientException
- Lambda.TooManyRequestsException
IntervalSeconds: 1
MaxAttempts: 3
BackoffRate: 2
JitterStrategy: FULL
End: true
Success:
Type: Succeed
QueryLanguage: JSONata
DefinitionSubstitutions:
lambdainvoke_FunctionName_ccca2377: >-
arn:aws:lambda:us-east-1:724772097129:function:lambda-create-record-notification-trello:$LATEST
RoleArn:
Fn::GetAtt:
- Role470f0f3f
- Arn
StateMachineName: StateMachine80541e5e
StateMachineType: STANDARD
EncryptionConfiguration:
Type: AWS_OWNED_KEY
LoggingConfiguration:
Level: 'OFF'
IncludeExecutionData: false
Role470f0f3f:
Type: AWS::IAM::Role
Properties:
RoleName: StepFunctions_IAM_ROLE_trelloWhatsappStateMachine78bed1bd
AssumeRolePolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Principal:
Service: states.amazonaws.com
Action: sts:AssumeRole
MaxSessionDuration: 3600
Policy2bd1232d:
Type: AWS::IAM::RolePolicy
Properties:
PolicyName: DynamoDBTableContentScopedAccessPolicycf373a3d
RoleName:
Ref: Role470f0f3f
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- dynamodb:GetItem
- dynamodb:PutItem
- dynamodb:UpdateItem
- dynamodb:DeleteItem
Resource:
- >-
arn:aws:dynamodb:us-east-1:724772097129:table/TrelloNotificationEvents
Policyb218aea1:
Type: AWS::IAM::RolePolicy
Properties:
PolicyName: LambdaInvokeScopedAccessPolicyff7103aa
RoleName:
Ref: Role470f0f3f
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource:
- >-
arn:aws:lambda:us-east-1:724772097129:function:lambda-create-record-notification-trello:*
- Effect: Allow
Action:
- lambda:InvokeFunction
Resource:
- >-
arn:aws:lambda:us-east-1:724772097129:function:lambda-create-record-notification-trello
Policy0c0067d6:
Type: AWS::IAM::RolePolicy
Properties:
PolicyName: XRayAccessPolicya9ae136e
RoleName:
Ref: Role470f0f3f
PolicyDocument:
Version: '2012-10-17'
Statement:
- Effect: Allow
Action:
- xray:PutTraceSegments
- xray:PutTelemetryRecords
- xray:GetSamplingRules
- xray:GetSamplingTargets
Resource:
- '*'
Outputs:
StateMachineArn:
Description: ARN of the Step Functions State Machine
Value:
Ref: StateMachine80541e5e
Export:
Name: TrelloWhatsappStateMachineArn
StateMachineName:
Description: Name of the Step Functions State Machine
Value:
Ref: StateMachine80541e5e
Add the arn lambda for create lambda create scheduler.
Deploy CloudFormation Stack
aws cloudformation create-stack --stack-name trello-whatsapp-state-machine --template-body template.yaml --region us-east-1 --capabilities CAPABILITY_NAMED_IAM
Step Functions Workflow
Once you deploy the CloudFormation stack, your Step Functions State Machine will be created and will appear in the AWS Step Functions console. The state machine implements a filtering mechanism to process only relevant Trello events.
State Machine Purpose
The Step Functions State Machine evaluates incoming Trello webhook events and determines whether they should be processed by the Lambda function. It acts as a gatekeeper, filtering events based on specific criteria:
-
Action Type: Must be
updateCard -
Action Display Translation Key: Must be
action_added_a_due_date
If the event meets both conditions, it is passed to the Lambda function for processing. If not, the execution terminates successfully without further processing.
State Machine Workflow
Trello Webhook Event
↓
┌─────────────────┐
│ Trello event │
│ type │
└────────┬────────┘
↓
┌─────────────────────────────────────────┐
│ Check: │
│ - action.type == "updateCard" │
│ - action.display.translationKey == │
│ "action_added_a_due_date" │
└────────┬─────────────────────┬──────────┘
│ YES │ NO
↓ ↓
┌──────────────────┐ ┌──────────────┐
│ Lambda Invoke │ │ Success │
│ (Process Card) │ │ (Terminate) │
└──────────────────┘ └──────────────┘
↓
┌──────────────────────────────────┐
│ Create EventBridge Schedule │
│ Send WhatsApp Notification │
└──────────────────────────────────┘
State Machine Components
Choice State: "trello event type"
This state evaluates the incoming Trello webhook event against two conditions:
-
Condition 1:
action.type == "updateCard"- Ensures the event is for a card update action
- Filters out other Trello actions like board updates, list changes, etc.
-
Condition 2:
action.display.translationKey == "action_added_a_due_date"- Ensures the card update is specifically a due date change
- Filters out other card updates like name changes, description changes, etc.
Logic:
- If BOTH conditions are TRUE → Route to "Lambda Invoke" state
- If EITHER condition is FALSE → Route to "Success" state (terminate execution)
Lambda Invoke State
This state invokes the Lambda function that will:
- Create an EventBridge scheduler for the notification
- Store the card information in DynamoDB
- Calculate the notification timing based on the due date
Success State
This state terminates the execution successfully when:
- The event is not a card update
- The event is a card update but not a due date change
- Any other non-matching scenarios
Example Scenarios
Scenario 1: Due Date Added to Card (✅ PROCESSES)
{
"action": {
"type": "updateCard",
"display": {
"translationKey": "action_added_a_due_date"
},
"data": {
"card": {
"id": "5f8c7d4e3c2b1a9d",
"name": "Complete project documentation",
"due": "2026-02-02T10:58:00.000Z"
}
}
}
}
Result: ✅ Event passes both conditions → Lambda is invoked
Scenario 2: Card Name Changed (❌ DOES NOT PROCESS)
{
"action": {
"type": "updateCard",
"display": {
"translationKey": "action_changed_the_name"
},
"data": {
"card": {
"id": "5f8c7d4e3c2b1a9d",
"name": "Updated card name"
}
}
}
}
Result: ❌ Event fails translation key condition → Execution terminates
Scenario 3: Board Created (❌ DOES NOT PROCESS)
{
"action": {
"type": "createBoard",
"display": {
"translationKey": "action_created_board"
},
"data": {
"board": {
"id": "5f8c7d4e3c2b1a9d",
"name": "New Board"
}
}
}
}
Result: ❌ Event fails action type condition → Execution terminates
Benefits of This Filtering Approach
✅ Efficient: Only processes relevant due date events
✅ Cost-effective: Reduces unnecessary Lambda invocations
✅ Reliable: Prevents processing incomplete or irrelevant data
✅ Scalable: Handles high volume of Trello webhook events
✅ Maintainable: Clear, declarative logic for event filtering
Monitoring the State Machine
You can monitor your Step Functions State Machine executions in the AWS console:
- Go to Step Functions in the AWS console
- Click on StateMachine80541e5e
- View execution history and logs
- Check which events passed the filter and which were rejected
- Debug any issues with event structure or conditions
Integration with Step Functions
The POST endpoint is integrated with AWS Step Functions, which orchestrates the workflow by filtering events and routing them to the appropriate Lambda function.
Configure API Gateway with Step Functions Integration
Now you need to configure the API Gateway POST endpoint to integrate directly with your Step Functions State Machine instead of using a Lambda function.
Step 1: Delete the Existing POST Method
- Go to your API Gateway console
- Select the POST method under
/events - Click Delete Method
- Confirm the deletion
Step 2: Create a New POST Method
- In your API Gateway console, select the
/eventsresource - Click on Create Method
- Select POST from the dropdown menu
- Click Create
Step 3: Configure POST Method Integration
- In the integration setup page, fill in the following:
- Integration type: Select AWS Service
- AWS Service: Select Step Functions
- HTTP Method: Select POST
- Action Type: Select StartSyncExecution (for synchronous execution)
-
Action: Enter
StartSyncExecution - Execution Role ARN: Select or create an IAM role that allows API Gateway to invoke Step Functions
- In the Request section:
- Set Request Body Passthrough to When there are no templates defined (recommended)
- Click Save
Step 4: Configure Request Mapping
- Click on the POST method you just created
- Click on Integration Request
- Expand the Mapping Templates section
- Click on Add mapping template
- Enter
application/jsonas the content type - In the template body, enter:
{
"stateMachineArn": "arn:aws:states:us-east-1:724772097129:stateMachine:StateMachine80541e5e",
"input": "$input.json('$')"
click create y and deploy the api changes
Test the Integration with Trello
Now that your API Gateway is configured and deployed, you can test the complete integration by setting a due date on a Trello card.
Step 1: Add a Due Date to a Trello Card
- Go to your Trello Board
- Select a card you want to test with
- Click on the card to open it
- In the card details panel, click on Due Date
- Set a due date for the card (e.g., tomorrow at 10:00 AM)
- Click Save
Screenshot
Step 2: Receive WhatsApp Notification
When the scheduled notification time arrives based on your due date, you will receive a WhatsApp message on your authorized phone number with the following information:
- 🎯 Task Name: The name of your Trello card
- Board: The board the card belongs to
- List: The list containing the card
- Due Date: The formatted due date (DD/MM/YYYY HH:MM)
- Link: Direct URL to the Trello card
Screenshot
How It Works
- Set Due Date in Trello: You add or update a due date on a Trello card
- Webhook Triggered: Trello sends a webhook event to your API Gateway endpoint
- Step Functions Filter: The Step Functions State Machine evaluates the event
- Lambda Processing: If the event matches the criteria, the Lambda function is invoked
- EventBridge Scheduler Created: An EventBridge scheduler is created for the notification time
- DynamoDB Storage: The card information is stored in DynamoDB
- Scheduled Execution: At the scheduled time, another Lambda function is triggered
- WhatsApp Message Sent: The WhatsApp notification is sent to your phone
- Status Updated: The DynamoDB record is updated to mark the notification as sent
Notification Timing
The notification will be sent based on the due date:
- If due date is > 24 hours away: Notification sent 1 day before the due date
- If due date is 12-24 hours away: Notification sent 12 hours before the due date
- If due date is < 12 hours away: No notification is scheduled (event may have already passed)
Example Scenario
Card Details:
- Card Name: "Complete project documentation"
- Board: "Working Tasks"
- List: "In Progress"
- Due Date: February 5, 2026 at 10:00 AM
Notification Timing:
- If set on February 3, 2026: Notification sent on February 4, 2026 at 10:00 AM (1 day before)
- If set on February 4, 2026 at 11:00 AM: Notification sent on February 5, 2026 at 10:00 AM (< 24 hours, so 12 hours before if applicable)
WhatsApp Message Received:
🎯 Trello Reminder Notification
Task: Complete project documentation
Board: Working Tasks
List: In Progress
Due Date: 05/02/2026 at 10:00
View Task: https://trello.com/c/RscETyKh
Please complete this task on time!
Update Due Date
You can also test the update functionality:
- Go back to the same Trello card
- Click on the due date to edit it
- Change it to a different date/time
- The DynamoDB Streams will trigger the Update Lambda
- The EventBridge scheduler will be updated with the new date
- You'll receive a WhatsApp notification at the new scheduled time
Bibliography and References
This document contains all the reference materials and documentation used in the development of the Trello WhatsApp Notifications system.
AWS Step Functions
- Tutorial: Creating a Lambda State Machine
- Tutorial: Using API Gateway with Step Functions
- Getting Started with AWS Step Functions
- Choice State in Step Functions
AWS DynamoDB
Trello API Documentation
- Trello REST API Action Types
- Trello Authorization and Allowed Origins
- Managing Your Trello API Key
- Trello Webhooks Guide
Meta WhatsApp Business API
Video Resources
Additional Resources
For more information about the technologies and services used in this project:

















































Top comments (0)