Email marketing
Send one email to one or more lists, now or at a set time; run a sequence of emails for everyone who joins a list; and post announcements to your users. Every send respects consent and unsubscribes.
What it is
| Tool | What it does | Admin screen |
|---|---|---|
| Broadcasts | One email to the people on chosen lists, now or scheduled | Email, Broadcasts (/admin/email/broadcasts) |
| Drip campaigns | A sequence of emails, each a set number of hours after the one before, for everyone who joins a list | Email, Drip Campaigns (/admin/email/drip-campaigns) |
| Announcements | An in-app notification to users, with an optional email copy | Email, Announcements (/admin/email/announcements) |
All three send to Audience lists, including App users (live) lists for broadcasts and sequences (App audience). Each list's page also shows its broadcasts and its sequence. The email underneath, such as the provider, templates, the composer, the brand kit, the logs and the unsubscribe link, is the Email engine.
Turn it on or off
The feature id is email-marketing. It is on by default and needs audience. To turn it off, in src/custom/features.ts:
export const CUSTOM_FEATURES: FeatureChoice = {
off: ['email-marketing'],
};
When it is off:
- Broadcasts, Drip Campaigns and Announcements leave the Email menu, their addresses show the not-found page, and list pages lose their Broadcasts and Sequence tabs.
- The functions in
functions/src/features/email-marketing.tsare not built, so the next deploy of all functions removes them, the scheduled jobs included. - Joining or leaving a list does not start or end a sequence, and an unsubscribe does not touch sequences.
- Campaigns, enrollments and past broadcasts stay in Firestore.
A lighter switch exists too: Settings, Email Settings has a Broadcasts and a Drip campaigns switch that pause those sends without removing anything (see Email engine). See Feature ids for every feature.
Set it up
Set up an email provider and turn email on in Settings, Email Settings (Email engine). Until then every send is logged as skipped with
email_disabled.Set the brand kit, so every email carries your logo, colors and footer with the unsubscribe link.
Have at least one list with subscribed people (Audience).
Open Email, Announcements once. It seeds the default templates, notification types, event mappings and the system lists. Seeding only adds what is missing.
Use it
Send a broadcast
Open Email, Broadcasts (or a list's page, Broadcasts tab). Write the subject.
Pick Send to lists. Optionally pick Exclude lists (anyone on them is skipped) and a premiumType filter (only contacts whose user account has that plan).
Click Preview count for the number of subscribed people it will reach.
Build the email from blocks and send yourself a test.
Click Send broadcast, or turn on Schedule for later, pick a time and click Schedule broadcast. A scheduled broadcast can be cancelled until it starts.
How it sends:
- Recipients are read from the lists when the send runs, not when you click, so consent and membership are always current.
- Someone on several of the chosen lists gets it once. An address that is both a contact and an app user gets it once.
- Every email is marketing:
pending,unsubscribed, disabled and suppressed people are skipped and counted as skipped. - It sends at the provider's rate limit. When the run reaches its time limit or the daily or hourly quota, the broadcast is
pausedand carries on in a new run from where it stopped. - A scheduled broadcast starts within 5 minutes of its time. One more than 24 hours overdue (for example because email was off) is marked
failedinstead of sent late.
Broadcast states: scheduled, queued, processing, paused, completed, failed, cancelled. Each email is also in Email Logs.
Run a drip sequence
Open Email, Drip Campaigns and create a campaign: a name and the list it runs on. The list cannot change afterwards.
Add steps. Each step is a template and a Delay (h): the hours after the previous step (for the first step, after joining). Pick an existing template, or create a new email for the step and refine it later in the Composer.
Choose Enroll existing members on activate if people already on the list should get it too. Otherwise only people who join after activation do.
Click Activate. A campaign needs at least one step.
How it runs:
- Joining the list enrolls the person. A step at 0 hours goes out at once; later steps are sent by a job that runs every 15 minutes.
- Before every step the person is checked again. Leaving the list, unsubscribing, being suppressed or being erased ends their sequence. An archived campaign ends everyone's.
- A person never enters the same campaign twice, even after leaving and rejoining the list.
- A step that cannot go out yet is held, not skipped: a paused campaign, email or the Drip campaigns switch turned off, an inactive template, or a contact still
pending. When a form sign-up confirms, their held step goes out at once. - On a form's list, steps can use the form's tags, such as
##WAITLIST##, the form's name (Email merge tags).
A template made with New email (in the Composer or from a step) has no sender of its own, and a step sends with its template's sender. Give each such template a sender before activating: see Email engine.
Pause and Resume stop and restart sending without losing anyone's place, but people who join the list while it is paused are not enrolled. Archive ends the campaign for good.
To make each signup form's welcome email the first step of a sequence on its list, run the migrateWelcomeToSequences admin callable once. It creates the campaign per form and marks the form so the welcome is not also sent directly (Signup forms).
Post an announcement
Open Email, Announcements, tab Send announcement. Write a title, a body and an optional link.
Choose the audience: All users, By role (type the role, such as
user), or By list. Optionally pick Exclude lists: anyone on them is skipped.Turn on Also send email for an email copy, then click Publish announcement.
An announcement is an in-app notification, so only people with an account get it: a contact on a list without an account is skipped. The email copy is marketing, so it respects unsubscribes. One announcement reads at most 2,000 people from each list, or 2,000 users for All users or a role. The other two tabs, Notification types and Event mappings, belong to Notifications and Automations.
Extend it
- Start a sequence from your code: add the person to the campaign's list with
addContactToLists(see Audience). Joining a list enrolls them in its active campaigns. - Start one from an event without code: an automation rule can add people to a list (Automations).
- Start one from your app's data: put the campaign on an App users (live) list; people join when a change makes them match (App audience).
- Send one email from your code: use the email engine's queue, not a broadcast (Email engine).
What it stores
| Firestore | Holds |
|---|---|
BroadcastEmails/{id} | One broadcast: subject, HTML, sender, audience (include, exclude, filters), scheduledAt, status, and sent, skipped and failed counts |
_broadcast_continue/{id} | A request to carry on a paused broadcast (Cloud Functions only) |
DripCampaigns/{id} | A campaign: name, listId, status (draft, active, paused, archived), steps (template id and delay in hours), enrollExistingOnActivate, and enrolled, completed and exited counts |
DripEnrollments/{campaignId}_{contactId} | One person in one campaign: currentStep, nextSendAt, status (active, completed, exited) and why they left. For an app user the contact id is app_ followed by their App audience id |
Announcements/{id} | One announcement, its audience and its counts |
Every collection is also in Data model. The notifications an announcement creates are in Notifications (Notifications); every email is in EmailLogs (Email engine). No Storage paths.
Cloud Functions (each deploys as arccms- followed by its name):
| Function | Kind | Does |
|---|---|---|
processBroadcast | Trigger on BroadcastEmails | Starts a broadcast created as queued |
continueBroadcast | Trigger on _broadcast_continue | Carries on a paused broadcast, and starts a scheduled one that is due |
processScheduledBroadcasts | Every 5 minutes | Queues due scheduled broadcasts |
previewBroadcastAudience | Admin callable | The eligible count |
processDripQueue | Every 15 minutes | Sends up to 100 due steps per run |
activateDripCampaign, archiveDripCampaign | Admin callables | Activate (and enroll existing members); archive (and end every enrollment) |
migrateWelcomeToSequences | Admin callable | Turns each form's welcome into a sequence step |
sendAnnouncement | Admin callable | Creates the announcement and its notifications |
Who can do what
| Data | Admins | Everyone else |
|---|---|---|
BroadcastEmails, DripCampaigns | Read and write | Nothing |
DripEnrollments, Announcements | Read; Cloud Functions write | Nothing |
_broadcast_continue | Nothing (Cloud Functions only) | Nothing |
The callables refuse anyone without the admin role (Roles and claims). A member only ever sees the notifications addressed to them.
Troubleshooting
| Symptom | Cause and fix |
|---|---|
| Preview count is lower than the list's member count | Only subscribed people count. Pending and unsubscribed contacts, and people on an excluded list, are left out. |
| A broadcast completed with everyone skipped | Email is off or the Broadcasts switch is off: open Email Logs for the skip reason. |
A scheduled broadcast shows failed, "overdue by more than 24h" | It could not start within a day of its time. Create it again. |
| A broadcast fails with "Exceeded maximum processing chunks (200)" | It paused and carried on 200 times without finishing, usually a very low rate limit or an exhausted quota. Raise the provider limits in Settings, Email Settings and send it again. |
| People on the list got nothing after activation | Only people who join after activation are enrolled, unless Enroll existing members on activate was on when the campaign was activated. Activate is offered only once, on a draft. |
| People who joined the list got nothing | They joined while the campaign was paused or a draft: only an active campaign enrolls people. |
| An announcement reached fewer people than the list | Contacts without an account are skipped. |
A step whose template was deleted holds every enrollment at that step, and they retry every 15 minutes without end. Before deleting a template, archive the campaigns that use it or give their steps another template.