Use Custom Operations to Send Bulk Email With SendGrid
Operations allow you to trigger your own code in a Flow. This guide will show you how to use the SendGrid SDK to bulk send emails as an operation in Flows.
Install Dependencies
To follow this guide, you will need a SendGrid API Key and access to SendGrid Dynamic Templates.
Open a console to your preferred working directory and initialize a new extension, which will create the boilerplate code for your operation.
npx create-directus-extension@latest
npx create-directus-extension@latest
A list of options will appear (choose operation), and type a name for your extension (for example, directus-operation-bulk-sendgrid
). For this guide, select JavaScript.
Now the boilerplate has been created, install the @sendgrid/mail
package, and then open the directory in your code editor.
cd directus-operation-bulk-sendgrid
npm install @sendgrid/mail
cd directus-operation-bulk-sendgrid
npm install @sendgrid/mail
Build the Operation UI
Operations have 2 parts - the api.js
file that performs logic, and the app.js
file that describes the front-end UI for the operation.
Open app.js
and change the id
, name
, icon
, and description
.
id: 'operation-bulk-sendgrid',
name: 'SendGrid Bulk Email',
icon: 'mail',
description: 'Send bulk emails using SendGrid API.',
id: 'operation-bulk-sendgrid',
name: 'SendGrid Bulk Email',
icon: 'mail',
description: 'Send bulk emails using SendGrid API.',
Make sure the id
is unique between all extensions including ones created by 3rd parties - a good practice is to include a professional prefix. You can choose an icon from the library here.
With the information above, the operation will appear in the list like this:
options
are the fields presented in the frontend when adding this operation to the Flow. To send an email with SendGrid, you will need the sending address, a recipient address, a subject and the template id. Replace the placeholder options with the following:
options: [
{
field: 'from',
name: 'From Address',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'recipients',
name: 'Recipients Object',
type: 'json',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'email_key',
name: 'Recipients Email Key',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'subject',
name: 'Subject',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'template_data',
name: 'Dynamic Template Data',
type: 'json',
meta: {
width: 'full',
interface: 'list',
options: {
template: '{{var}}: {{key}}',
fields: [
{
field: 'var',
name: 'Email Variable',
type: 'string',
meta: {
width: 'half',
interface: 'input',
options: {
font: 'monospace',
},
},
},
{
field: 'key',
name: 'Recipient Object Key',
type: 'string',
meta: {
width: 'half',
interface: 'input',
options: {
font: 'monospace',
},
},
},
],
},
},
},
{
field: 'template_id',
name: 'SendGrid Template ID',
type: 'string',
meta: {
width: 'full',
interface: 'input',
},
},
],
options: [
{
field: 'from',
name: 'From Address',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'recipients',
name: 'Recipients Object',
type: 'json',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'email_key',
name: 'Recipients Email Key',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'subject',
name: 'Subject',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'template_data',
name: 'Dynamic Template Data',
type: 'json',
meta: {
width: 'full',
interface: 'list',
options: {
template: '{{var}}: {{key}}',
fields: [
{
field: 'var',
name: 'Email Variable',
type: 'string',
meta: {
width: 'half',
interface: 'input',
options: {
font: 'monospace',
},
},
},
{
field: 'key',
name: 'Recipient Object Key',
type: 'string',
meta: {
width: 'half',
interface: 'input',
options: {
font: 'monospace',
},
},
},
],
},
},
},
{
field: 'template_id',
name: 'SendGrid Template ID',
type: 'string',
meta: {
width: 'full',
interface: 'input',
},
},
],
from
andsubject
interfaces use an input interface for a simple string.recipients
is set to JSON so it can receive all the recipient’s data such as email address, name, job title or anything else that is needed for the email.email
is required for fetching the email address from the recipient object.template_data
uses a list interface (also known as a repeater interface) where the user can specify what variables are within the email and what to replace them with. The var value needs to be the object key relative to a single recipient object. For example,first_name
orbuilding.name
.template_id
field uses a standard input field for the ID from SendGrid. This can be a static ID or dynamically added through the workflow.
The overview
section defines what is visible inside the operation’s card on the Flow canvas. An overview object contains 2 parameters, label
and text
. The label can be any string and does not need to match the field name. The text parameter can be a variable or just another string.
It will be useful to see the subject and the from address on the card. To do this you must include the fields value from the options (subject
and from
) as properties. Replace the placeholder objects with the following:
overview: ({ subject, from }) => [
{
label: 'Subject',
text: subject,
},
{
label: 'From',
text: from,
},
],
overview: ({ subject, from }) => [
{
label: 'Subject',
text: subject,
},
{
label: 'From',
text: from,
},
],
Now, the overview of the operation looks like this:
Build the API Function
Open the api.js
file, update the id
to match the one used in the app.js
file, and import the SendGrid package at the very top:
import sgMail from '@sendgrid/mail'
import sgMail from '@sendgrid/mail'
The handler needs to include the fields from the app.js
options and the environment variables from Directus. Replace the handler definition with the following:
handler: ({ from, recipients, substitutions, subject, template_id }, { env }
) => {
handler: ({ from, recipients, substitutions, subject, template_id }, { env }
) => {
Set up the SendGrid API and message object with the following code:
sgMail.setApiKey(env.SENDGRID_API_KEY);
let msg = {
from: { email: from },
personalizations: [],
template_id: template_id,
};
sgMail.setApiKey(env.SENDGRID_API_KEY);
let msg = {
from: { email: from },
personalizations: [],
template_id: template_id,
};
Read more about the SendGrid Dynamic Template API request in the SendGrid documentation.
The recipients
need to be added to the personalization list. Each item needs to follow this structure:
{
"to": [{ "email": "example@sendgrid.net" }],
"dynamic_template_data": {
"key": "value"
}
}
{
"to": [{ "email": "example@sendgrid.net" }],
"dynamic_template_data": {
"key": "value"
}
}
Note: do not add multiple email addresses inside the to
object. Instead, each recipient must have their own personalization object where you can include dynamic template data relevant to that person.
Convert both the recipients and dynamic variables to objects if they are submitted as JSON, and create a function to parse values from the recipients and return the final value:
const rec = Array.isArray(recipients) ? recipients : JSON.parse(recipients);
const dyn = Array.isArray(template_data) ? template_data : JSON.parse(template_data);
function parseValues(recipient, key) {
if (key.includes('.')) {
let value = recipient;
let fields = key.split('.');
fields.forEach((f) => {
if (value != null) value = value[f];
});
return value;
} else {
return recipient[key];
}
}
const rec = Array.isArray(recipients) ? recipients : JSON.parse(recipients);
const dyn = Array.isArray(template_data) ? template_data : JSON.parse(template_data);
function parseValues(recipient, key) {
if (key.includes('.')) {
let value = recipient;
let fields = key.split('.');
fields.forEach((f) => {
if (value != null) value = value[f];
});
return value;
} else {
return recipient[key];
}
}
For each recipient, build the personalization object and add it to the msg
variable. This will include the subject as a dynamic variable. To use this, make sure to change the subject in SendGrid to {{{subject}}}
to receive this value.
rec.forEach((recipient) => {
let email_address = parseValues(recipient, email_key);
let personalization = {
to: [{ email: email_address }],
dynamic_template_data: {
subject: subject,
},
};
dyn.forEach((s) => {
personalization.dynamic_template_data[s.var] = parseValues(recipient, s.key);
});
msg.personalizations.push(personalization);
});
rec.forEach((recipient) => {
let email_address = parseValues(recipient, email_key);
let personalization = {
to: [{ email: email_address }],
dynamic_template_data: {
subject: subject,
},
};
dyn.forEach((s) => {
personalization.dynamic_template_data[s.var] = parseValues(recipient, s.key);
});
msg.personalizations.push(personalization);
});
The msg
variable is now ready to be sent to SendGrid. Use the following to send the data and return the response to the workflow:
return sgMail.send(msg);
return sgMail.send(msg);
Both files are now complete. Build the operation with the latest changes.
npm run build
npm run build
Add Operation to Directus
When Directus starts, it will look in the extensions
directory for any subdirectory starting with directus-extension-
, and attempt to load them.
To install an extension, copy the entire directory with all source code, the package.json
file, and the dist
directory into the Directus extensions
directory. Make sure the directory with your extension has a name that starts with directus-extension
. In this case, you may choose to use directus-extension-operation-bulk-sendgrid
.
Ensure the .env
file has SENDGRID_API_KEY
variable.
Restart Directus to load the extension.
Required files
Only the package.json
and dist
directory are required inside of your extension directory. However, adding the source code has no negative effect.
Use the Operation
In the Directus Data Studio, open the Flows section in Settings. Create a new flow with a manual trigger. Select the collection(s) to include this button on.
Fetch some recipients by adding a Read Data operation and include the required fields and filters. Add a new step (operation) after the Data Read operation by clicking the tick/plus on the card, then choose SendGrid Bulk Email from the list.
- For the From Address, type
no-reply@company.com
. This must be the domain registered with SendGrid - For the Recipients Object, use a previous flow operation such as Read Data to fetch all the required recipients as an array of objects. Make sure to include fields that will be used in the email. In this example, you will use
{{$last}}
. - For the Recipients Email Key, type the field name that contains the recipient's email address and make sure it’s included in the Recipients Object.
- For the Subject, add a catchy subject for the email. If you want to personalize the subject, this is best done on the template with SendGrid.
- To add Dynamic Template Data, click Create New and set the Email Variable to the key used in the template and the Recipient Object Key to the field from the recipient object. In this example, use their first name from a field called
first_name
and use it to replace{{{name}}}
in the SendGrid template. - The
SendGrid Template ID
must be supplied. This can be added as a static string or dynamically added from the Flow.
Save the operation, save the Flow, and then trigger the flow by opening the chosen collection, then trigger the manual flow from the right side toolbar.
Summary
This operation will send an SendGrid API request to use an email template inside SendGrid along with the supplied personal information to bulk send an email to the recipients. The response is captured in the logs for reference. Now that you know how to interact with a third party API, you can investigate other services that can be used in your workflows.
Complete Code
app.js
export default {
id: 'operation-bulk-sendgrid',
name: 'Sendgrid Bulk Email',
icon: 'mail',
description: 'Send bulk emails using SendGrid API.',
overview: ({ subject, from }) => [
{
label: 'Subject',
text: subject,
},
{
label: 'From',
text: from,
},
],
options: [
{
field: 'from',
name: 'From Address',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'recipients',
name: 'Recipients Object',
type: 'json',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'email_key',
name: 'Recipent Email Key',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'subject',
name: 'Subject',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'template_data',
name: 'Dynamic Template Data',
type: 'json',
meta: {
width: 'full',
interface: 'list',
options: {
template: '{{var}}: {{key}}',
fields: [
{
field: 'var',
name: 'Email Variable',
type: 'string',
meta: {
width: 'half',
interface: 'input',
options: {
font: 'monospace',
},
},
},
{
field: 'key',
name: 'Recipient Object Key',
type: 'string',
meta: {
width: 'half',
interface: 'input',
options: {
font: 'monospace',
},
},
},
],
},
},
},
{
field: 'template_id',
name: 'SendGrid Template ID',
type: 'text',
meta: {
width: 'full',
interface: 'input',
},
},
],
};
export default {
id: 'operation-bulk-sendgrid',
name: 'Sendgrid Bulk Email',
icon: 'mail',
description: 'Send bulk emails using SendGrid API.',
overview: ({ subject, from }) => [
{
label: 'Subject',
text: subject,
},
{
label: 'From',
text: from,
},
],
options: [
{
field: 'from',
name: 'From Address',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'recipients',
name: 'Recipients Object',
type: 'json',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'email_key',
name: 'Recipent Email Key',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'subject',
name: 'Subject',
type: 'string',
meta: {
width: 'half',
interface: 'input',
},
},
{
field: 'template_data',
name: 'Dynamic Template Data',
type: 'json',
meta: {
width: 'full',
interface: 'list',
options: {
template: '{{var}}: {{key}}',
fields: [
{
field: 'var',
name: 'Email Variable',
type: 'string',
meta: {
width: 'half',
interface: 'input',
options: {
font: 'monospace',
},
},
},
{
field: 'key',
name: 'Recipient Object Key',
type: 'string',
meta: {
width: 'half',
interface: 'input',
options: {
font: 'monospace',
},
},
},
],
},
},
},
{
field: 'template_id',
name: 'SendGrid Template ID',
type: 'text',
meta: {
width: 'full',
interface: 'input',
},
},
],
};
api.js
import sgMail from '@sendgrid/mail';
export default {
id: 'operation-bulk-sendgrid',
handler: ({ from, email_key, recipients, template_data, subject, template_id }, { env }) => {
sgMail.setApiKey(env.SENDGRID_API_KEY);
let msg = {
from: {
email: from,
},
personalizations: [],
template_id: template_id,
};
const rec = Array.isArray(recipients) ? recipients : JSON.parse(recipients);
const dyn = Array.isArray(template_data) ? template_data : JSON.parse(template_data);
function parseValues(recipient, key) {
if (key.includes('.')) {
let value = recipient;
let fields = key.split('.');
fields.forEach((f) => {
if (value != null) {
value = value[f];
}
});
return value;
} else {
return recipient[key];
}
}
rec.forEach((recipient) => {
let email_address = parseValues(recipient, email_key);
let personalization = {
to: [{ email: email_address }],
dynamic_template_data: {
subject: subject,
},
};
dyn.forEach((s) => {
personalization.dynamic_template_data[s.var] = parseValues(recipient, s.key);
});
msg.personalizations.push(personalization);
});
return sgMail.send(msg);
},
};
import sgMail from '@sendgrid/mail';
export default {
id: 'operation-bulk-sendgrid',
handler: ({ from, email_key, recipients, template_data, subject, template_id }, { env }) => {
sgMail.setApiKey(env.SENDGRID_API_KEY);
let msg = {
from: {
email: from,
},
personalizations: [],
template_id: template_id,
};
const rec = Array.isArray(recipients) ? recipients : JSON.parse(recipients);
const dyn = Array.isArray(template_data) ? template_data : JSON.parse(template_data);
function parseValues(recipient, key) {
if (key.includes('.')) {
let value = recipient;
let fields = key.split('.');
fields.forEach((f) => {
if (value != null) {
value = value[f];
}
});
return value;
} else {
return recipient[key];
}
}
rec.forEach((recipient) => {
let email_address = parseValues(recipient, email_key);
let personalization = {
to: [{ email: email_address }],
dynamic_template_data: {
subject: subject,
},
};
dyn.forEach((s) => {
personalization.dynamic_template_data[s.var] = parseValues(recipient, s.key);
});
msg.personalizations.push(personalization);
});
return sgMail.send(msg);
},
};