Drip sequences work. A timed series of emails consistently outperforms one-off blasts for engagement and conversion. The problem isn't the concept. It's the enrollment.
Most platforms treat enrollment as a manual step. You build the sequence, then remember to enroll every new contact via API call or dashboard action. If you forget, or if the integration breaks, contacts fall through the cracks.
PushMail sequences support automatic triggers. When a contact is added to a specific list or receives a specific tag, they're enrolled immediately. No extra API call. No contacts missed.
The limitation of manual enrollment
The enrollment step is integration logic that lives in your application, separate from the sequence. If your signup flow changes, if you add a new contact source, or if someone imports contacts without enrolling them, the sequence breaks silently. You end up with contacts who should have received your welcome series but didn't.
Trigger types in PushMail
PushMail sequences support three trigger types:
manual — the default. Contacts are enrolled explicitly via the API or dashboard. This is the traditional model, and it's still useful for one-off enrollment of specific segments.
list_add — the sequence fires whenever a contact is added to a specified list. Add a contact to the "Newsletter Subscribers" list, and they're automatically enrolled in the welcome sequence attached to that list.
tag_add — the sequence fires whenever a contact receives a specified tag. Tag a contact as "trial-started" from your application, and they're enrolled in the trial onboarding sequence.
The trigger is configured on the sequence itself, not on the list or tag.
Example: welcome sequence on list add
Create a sequence with a list_add trigger so every new subscriber gets a welcome series:
curl -X POST https://pushmail.dev/api/v1/sequences \
-H "Authorization: Bearer pm_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"name": "Newsletter Welcome Series",
"triggerType": "list_add",
"triggerConfig": "{\"listId\": 3}"
}'The triggerConfig specifies which list to watch. When the sequence is active, any contact added to list 3 — via API, CSV import, or dashboard — is automatically enrolled.
Example: onboarding sequence on tag add
Tag triggers watch for tag assignments instead of list additions. Say you want an onboarding sequence when a user starts a trial. Your app just tags the contact:
curl -X POST https://pushmail.dev/api/v1/sequences \
-H "Authorization: Bearer pm_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"name": "Trial Onboarding",
"triggerType": "tag_add",
"triggerConfig": "{\"tagId\": 5}"
}'When your app adds tag ID 5 to a contact, the sequence starts. Your application doesn't need to know about the sequence — it just tags the contact and moves on. PushMail translates tags and list memberships into email sequences, keeping enrollment logic out of your codebase.
Sequence steps: templates, delays, and position
Each step has a templateId, a delayMinutes value (0 for immediate), and a position in the sequence. Delays are in minutes for fine-grained control: 0 (immediate), 1440 (1 day), 4320 (3 days), 10080 (1 week).
curl -X POST https://pushmail.dev/api/v1/sequences/1/steps \
-H "Authorization: Bearer pm_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{ "templateId": 10, "position": 1, "type": "email", "delayMinutes": 0 }'When a contact is enrolled, the enrollment record tracks currentStepId and nextProcessAt — the timestamp when the cron worker should process the next step.
Conversion goals
Sequences are more useful when you can measure whether they're working. PushMail supports conversion goals at the sequence level — a condition that marks an enrollment as successfully converted.
Configure goals via the goalConfig field:
curl -X POST https://pushmail.dev/api/v1/sequences \
-H "Authorization: Bearer pm_live_your_api_key" \
-H "Content-Type: application/json" \
-d '{
"siteId": 1,
"name": "Upgrade Nurture",
"triggerType": "tag_add",
"triggerConfig": "{\"tagId\": 8}",
"goalConfig": "{\"type\": \"click\", \"urlPattern\": \"/pricing\", \"action\": \"complete\"}"
}'This sequence watches for enrolled contacts who click any link containing "/pricing" in a sequence email. When they do, the enrollment is immediately marked as completed with a goal_completed reason. No more emails are sent from this sequence.
Goal types include open and click (with URL pattern matching). The action field controls the outcome: complete marks conversion, cancel stops the sequence without marking it. Goal evaluation happens in real-time — when a webhook event arrives, PushMail updates the enrollment immediately with no cron delay.
How the cron worker processes enrollments
PushMail runs a cron worker every minute that queries for enrollments where nextProcessAt is in the past. For each due enrollment, the worker looks up the current step, renders the template, queues the email via Cloudflare Queues, and advances the enrollment to the next step. If there are no more steps, the enrollment is marked completed with reason sequence_end.
The one-minute interval means at most a 60-second delay between when a step is due and when the email is queued. For drip delays measured in hours or days, this is negligible.
A unique constraint on sequenceId + contactId prevents duplicate enrollments. Re-adding a contact to the trigger list or re-applying the trigger tag won't enroll them twice.
Putting it together
The workflow in practice is straightforward. You set up your sequences once — define the steps, choose a trigger, optionally set a conversion goal, and activate the sequence. After that, your application just manages contacts: add them to lists, apply tags based on user actions. PushMail handles the rest.
A new user signs up, your app adds them to the "Active Users" list, PushMail enrolls them in the welcome sequence. The user starts a trial, your app tags them "trial-started", PushMail enrolls them in the onboarding sequence. The user clicks the pricing link in an onboarding email, PushMail marks the goal as completed and stops the sequence.
No enrollment API calls scattered through your codebase. No integration logic to maintain. Just lists, tags, and the sequences that watch them.