Schedule emails without polling a database using Step Functions
Tags: stepfunctions, lambda, serverless, aws
Published: 2018-11-22 11:23:28

---
title: Schedule emails without polling a database using Step Functions
tags: Step Functions, Lambda, Serverless, AWS
description: A walkthrough of using AWS Step Functions, Lambda and the Serverless Framework to schedule email deliveries without needing to use a database or cron jobs.
canonical_url: https://winterwindsoftware.com/serverless-email-scheduler/
cover_image: https://winterwindsoftware.com/img/blog-images/boy-ladder-clouds.jpeg
published: true
---
Sending emails is a pretty standard feature of a web app. Email service providers offer simple APIs allowing you to send an email with just a few lines of code.

But scheduling them to be delivered at some time in the future is a lot more hassle. Say you want an onboarding sequence for new users over the course of a few days, or perhaps you need to send a reminder about a task or event that a user has created in your app. Now you have to:

- create a `ScheduledEmails` table in your database to store future-dated messages
- create a cron job to poll this table for due emails and then clear them out once they're delivered
- maintain the server that the cron job and is running on

That's a lot of yak shaving for something which should be much simpler.

But there is a nicer way...

By using AWS [Lambda](https://aws.amazon.com/lambda/) and [Step Functions](https://aws.amazon.com/step-functions/) you get a solution that:
- Doesn't need a database
- Involves no polling
- You only pay for how much it's used
- Provides your app with a simple API call to invoke a Lambda (which can be treated as a microservice of its own)

If you want to jump to the full codebase, [go here](https://github.com/paulswail/serverless-email-scheduler).
If you just want the key points, stick with me and I'll walk you through how to implement this using Node.js and the Serverless Framework.

📖  __Related: *[Why serverless newbies should use a deployment framework](https://winterwindsoftware.com/serverless-newbies-should-use-framework/).*__

_Sidebar: while I use emails here, this approach could be applied to any future scheduled task that your app needs to perform._

## Getting set up
First off, [install the Serverless Framework](https://serverless.com/framework/docs/providers/aws/guide/installation/).
We will also be using the [`serverless-step-functions`](https://github.com/horike37/serverless-step-functions) plugin to allow us to easily configure our Step Function within the `serverless.yml` file.

## Configuring the Step Functions sequence
![Send Email Step Function State Machine](https://winterwindsoftware.com/img/blog-images/send-email-step-function.png)

There are 2 states in the sequence:
1. `WaitForDueDate`: this is simply a [Wait](https://docs.aws.amazon.com/step-functions/latest/dg/amazon-states-language-wait-state.html) step which reads a `dueDate` field from the input object and waits until that time is reached before proceeding to the next state.
2. `SendEmail`: a Lambda function which makes the API call to send the email.

These are configured as follows:

```yml
# serverless.yml
# ...
stepFunctions:
  stateMachines:
    EmailSchedulingStateMachine:
      name: EmailSchedulingStateMachine
      definition:
        Comment: "Schedules an email to be sent at a future date"
        StartAt: WaitForDueDate
        States:
          WaitForDueDate:
            Type: Wait
            TimestampPath: "$.dueDate"
            Next: SendEmail
          SendEmail:
            Type: Task
            Resource: "arn:aws:lambda:#{AWS::Region}:#{AWS::AccountId}:function:${self:service}-${opt:stage}-SendEmail"
            End: true
```
## Create the SendEmail function
The following code defines a handler for the Lambda function `SendEmail` which is referenced by the SendEmail State in the Step Functions config. The function uses [AWS SES](https://aws.amazon.com/ses/) to send an email.

``` javascript
const AWS = require('aws-sdk');
const ses = new AWS.SES();
const { EMAIL_SENDER_ADDRESS } = process.env;

module.exports.handle = async (event) => {
    const result = await sendEmail(event.email);
    console.log('Sent email successfully', result);
    return result;
};

function sendEmail(email) {
    const params = {
        Destination: {
            ToAddresses: email.to,
        },
        Message: {
            Subject: {
                Data: email.subject,
            },
            Body: {
                Html: {
                    Data: email.htmlBody || email.textBody,
                },
                Text: {
                    Data: email.textBody || email.htmlBody,
                },
            },
        },
        Source: EMAIL_SENDER_ADDRESS,
    };
    return ses.sendEmail(params).promise();
}
```

## Initiating the scheduling
So now the SendEmail logic is implemented and hooked up to the Step Functions state machine, we need a way of starting the step function from your app. This is done by using the [`startExecution`](https://docs.aws.amazon.com/step-functions/latest/apireference/API_StartExecution.html) function in the Step Functions API.

```javascript
// schedule-email.js
const AWS = require('aws-sdk');
const stepfunctions = new AWS.StepFunctions();

module.exports.scheduleEmail = async (dueDate, email) => {
    const stateMachineArn = process.env.STATEMACHINE_ARN;
    const result = await stepfunctions.startExecution({
        stateMachineArn,
        input: JSON.stringify({
            dueDate, email
        }),
    }).promise();
    console.log(`State machine ${stateMachineArn} executed successfully`, result);
    return result;
}

// ----------------------------------------------
// your-app-module.js
const scheduleEmail = require('./schedule-email');
const email = {
    to: ['test@example.com'],
    subject: 'MyApp Onboarding Day 1 - Setting up your project',
    textBody: 'TEXT body.\nsecond line.',
    htmlBody: '<strong>HTML</strong> body<br>second line.'
};
const dueDate = '2018-11-16T13:55:25.000Z';
const result = await scheduleEmail(dueDate, email);

// result :
// {
//     "executionArn": "arn:aws:states:eu-west-1:123456789:execution:EmailSchedulingStateMachine:84b1f498-ab4d-4f69-a635-ab1fa5199e16",
//     "startDate": "2018-11-16T16:19:23.560Z"
// }
```

You will need to update the IAM role which your app is running under to allow the `states:StartExecution` action on the `EmailSchedulingStateMachine` state machine.

## What if I need to cancel a scheduled email?
The `startExecution` call returns an object which contains a unique `executionArn` string for this execution. If you need the ability to cancel emails, you can store this ARN and then later pass it to the [`stopExecution`](https://docs.aws.amazon.com/step-functions/latest/apireference/API_StopExecution.html) function to cancel the execution before it proceeds to the SendEmail state (assuming it hasn't already ended).

## Try it out for yourself
If you think this would be useful to you then [clone this repo](https://github.com/paulswail/serverless-email-scheduler) and deploy it yourself.

Some potential enhancements which you could make:
- Add retry logic if sending email fails ([supported by the `serverless-step-functions` plugin](https://github.com/horike37/serverless-step-functions#retry-failture))
- Support email providers other than SES

---
💌 __*If you enjoyed this article, you can sign up [to my weekly newsletter on building serverless web apps in AWS](https://winterwindsoftware.com/newsletter/).*__
You also might enjoy:
- [Concerns that serverless takes away](https://winterwindsoftware.com/concerns-that-serverless-takes-away/)
- [Case studies of AWS serverless apps in production](https://winterwindsoftware.com/real-world-serverless-case-studies/)
- [The differing definitions of “serverless”](https://winterwindsoftware.com/serverless-definitions/)

_Originally published at **[winterwindsoftware.com](https://winterwindsoftware.com/serverless-email-scheduler/)**_.