Automations
Set up trigger-based email sequences that run automatically based on subscriber behavior
Automations let you create multi-step email sequences triggered by subscriber events. Define a trigger, build a sequence of steps, and MailLand executes the flow automatically whenever the trigger fires.
Automation Triggers
Each automation starts with a single trigger:
| Trigger | Value | Description |
|---|---|---|
SubscriberJoins | 0 | Subscriber is added to a mailing list |
SubscriberUpdated | 1 | Subscriber profile is updated |
CampaignOpened | 2 | Subscriber opens a specific campaign |
CampaignClicked | 3 | Subscriber clicks a link in a campaign |
DateBased | 4 | Birthday-only trigger: evaluates Subscriber.Birthday (see callout below) |
Manual | 5 | Triggered manually via API |
DateBased only ever evaluates Subscriber.Birthday, not an arbitrary recurring date field.
Birthday is settable through POST/PUT /v1/marketing/subscribers (see
Subscribers) and through
CSV import, which accepts an optional birthday
column, so a DateBased automation enrols any subscriber whose birthday was set either way.
Automation Statuses
| Status | Value | Description |
|---|---|---|
Draft | 0 | Editable, not running |
Active | 1 | Running: new subscribers are enrolled on trigger |
Paused | 2 | Paused: no new enrollments, and further steps stop for subscribers already enrolled. Reactivating (Active) resumes them from exactly where they held, and delivers everything that fell due while paused |
Archived | 3 | Archived: hidden from default views. Like Paused, this stops new enrolments; further steps also stop for subscribers already enrolled. There is no unarchive endpoint, so this hold is permanent for those enrolments |
Step Types
Each automation contains an ordered sequence of steps:
| Step Type | Value | Description |
|---|---|---|
SendEmail | 0 | Send an email to the subscriber |
Wait | 1 | Wait for a duration before continuing |
Condition | 2 | Evaluates a subscriber field; a gate that continues on true and ends the enrolment on false (see Condition step below) |
AddToList | 3 | Add subscriber to a mailing list |
RemoveFromList | 4 | Remove subscriber from a mailing list |
UpdateSubscriber | 5 | Update subscriber fields |
Webhook | 6 | Send a webhook to an external URL |
Personalization
Automation email steps support {{variable}} merge tags, substituted per recipient
from the enrolled subscriber's own record.
| Tag | Value |
|---|---|
{{firstName}} | Subscriber's first name |
{{lastName}} | Subscriber's last name |
{{fullName}} | First and last name joined |
{{email}} | Subscriber's email address |
{{subscriberId}} | Opaque subscriber identifier |
{{custom_<field>}} | Any custom field on the subscriber, also available as {{<field>}} |
<p>Hi {{firstName}}, here's what's new this month.</p>A tag with no value is left exactly as written: {{firstName}} for a subscriber with no first
name stays {{firstName}} in the delivered email rather than becoming an empty gap. Write content
that reads correctly either way, e.g. Hi there{{firstName}} is a poor fit but "Your account" is
safe. Use a fallback greeting when you are not sure every subscriber has the field.
Values are escaped for the part they land in: HTML content is HTML-escaped (so a name containing
& or < cannot break your markup), while the subject line and plain-text body are never
escaped, Ben & Jerry's stays exactly that.
The older snake_case spellings ({{first_name}}, {{last_name}}, {{full_name}},
{{subscriber_id}}) continue to work as aliases.
Create an Automation
POST https://api.mail.land/v1/marketing/automations{
"name": "Welcome Series",
"description": "3-email welcome sequence for new subscribers",
"trigger": 0,
"mailingListId": "7h3n5k8w2q9m1t",
"campaignId": null,
"steps": [
{
"type": 0,
"order": 1,
"templateId": "2p9x4k7m3q8n1z",
"subject": "Welcome to our newsletter!"
},
{
"type": 1,
"order": 2,
"delayMinutes": 1440
},
{
"type": 0,
"order": 3,
"templateId": "9k4p2x7m1q3n8z"
}
]
}| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Automation name (unique per workspace) |
description | string | No | Automation description |
trigger | int | Yes | Trigger type (see triggers table) |
mailingListId | string | No | Target mailing list for trigger |
campaignId | string | No | Optional for CampaignOpened / CampaignClicked triggers: when omitted, the trigger fires on any campaign, not just one |
steps | array | Yes | Ordered sequence of steps |
There is no segmentId field on an automation: that field does not exist here (segments are used elsewhere, e.g. campaigns). SendEmail step content comes from templateId, not inline htmlContent/textContent/fromName/fromEmail.
campaignId is optional for the open/click triggers, not required as earlier versions of this page stated. It also fires on machine opens (Apple Mail Privacy Protection prefetch, security link scanners) that MailLand does not filter out today.
Step Fields
Every step is actually shaped { order, type, delayMinutes, templateId, subject, settings: {} }: there is no automation-level segmentId, and steps do not have the per-type fields shown in older versions of this page (conditionField, targetMailingListId, updateField, webhookUrl, fromName, fromEmail, replyTo are not top-level step fields). The tables below reflect the real shape.
SendEmail step (type: 0):
| Field | Type | Required | Description |
|---|---|---|---|
type | int | Yes | 0 |
order | int | Yes | Step position in sequence |
delayMinutes | int | No | Minutes to wait before this step runs: including when it is the first step: the delay before the very first send is now honoured at enrolment (it used to be ignored) |
templateId | string | Yes | Template supplying the email content: content comes only from the template |
subject | string | No | Overrides the template's subject if set |
There is no inline htmlContent/textContent/fromName/fromEmail/replyTo on a step. The sender is fixed to noreply@<first fully verified domain>, shown with the workspace name: it is not configurable per step.
templateId is required on every SendEmail step. Create/update requests without one are refused
with 400 VALIDATION_FAILED; a templateId that does not resolve to a template in your tenant is
refused with 400 TEMPLATE_NOT_FOUND, both before anything is written. If a template is deleted
after the automation is saved, the next subscriber to reach that step has their enrolment marked
Failed instead of the step being silently skipped, and it counts toward the automation's
totalFailed (see Get Automation Details).
Wait step (type: 1):
| Field | Type | Required | Description |
|---|---|---|---|
type | int | Yes | 1 |
order | int | Yes | Step position |
delayMinutes | int | Yes | Minutes to wait before next step |
Condition step (type: 2):
A Condition step is a gate, not a branch: there is no second path in a linear step list. When
it evaluates true, the enrolment continues to the next step. When it evaluates false, the
enrolment ends there (Completed) and no later step runs for that subscriber.
| Field | Type | Required | Description |
|---|---|---|---|
type | int | Yes | 2 |
order | int | Yes | Step position |
settings.conditionField | string | Yes | The subscriber field to evaluate: email, firstName, lastName, phone, company, source, or custom_<name> for a custom field |
settings.conditionOperator | string | Yes | equals, notEquals, contains, notContains, startsWith, endsWith, isEmpty, isNotEmpty (case-insensitive) |
settings.conditionValue | string | Required unless the operator is isEmpty or isNotEmpty | The value to compare against |
Comparisons are case-insensitive string comparisons, the same operators
segments already offer. 400 VALIDATION_FAILED is returned at create/update time when
conditionField is unknown, conditionOperator is not one of the values above, or conditionValue
is missing for an operator that needs one.
An automation whose Condition step was saved before this validation existed (so its settings
do not evaluate) keeps passing through unchanged, for continuity: it is only new create/update
requests that are validated.
AddToList / RemoveFromList steps (type: 3, 4):
| Field | Type | Required | Description |
|---|---|---|---|
type | int | Yes | 3 or 4 |
order | int | Yes | Step position |
settings.mailingListId | string | Yes | Mailing list to add/remove the subscriber, read from settings |
UpdateSubscriber step (type: 5):
| Field | Type | Required | Description |
|---|---|---|---|
type | int | Yes | 5 |
order | int | Yes | Step position |
settings | object | Yes | Each key in settings is written as a custom field on the subscriber |
Webhook step (type: 6):
| Field | Type | Required | Description |
|---|---|---|---|
type | int | Yes | 6 |
order | int | Yes | Step position |
settings.url | string | Yes | URL to send the POST request to, read from settings |
Webhook step requests are signed with a secret of the step's receiver (X-MLND-Signature, verified like a customer webhook): each distinct step URL has its own secret, so no receiver can sign for another. Read them with GET /v1/marketing/automations/{id}/webhook-signing-secrets and rotate them with POST /v1/marketing/automations/{id}/webhook-signing-secrets/regenerate, see Automation Webhook steps. Send { "webhookUrl": "…" } to rotate one receiver only.
curl -X POST https://api.mail.land/v1/marketing/automations \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome Series",
"trigger": 0,
"mailingListId": "7h3n5k8w2q9m1t",
"steps": [
{
"type": 0, "order": 1,
"subject": "Welcome!",
"templateId": "2p9x4k7m3q8n1z"
},
{ "type": 1, "order": 2, "delayMinutes": 1440 },
{
"type": 0, "order": 3,
"subject": "Getting started",
"templateId": "9k4p2x7m1q3n8z"
}
]
}'Response
{
"id": "4f8k2m9p1q6z3s",
"name": "Welcome Series",
"description": "3-email welcome sequence for new subscribers",
"trigger": "subscriberJoins",
"triggerName": "SubscriberJoins",
"status": "draft",
"statusName": "Draft",
"mailingListId": "7h3n5k8w2q9m1t",
"mailingListName": "Product Updates",
"campaignId": null,
"campaignName": null,
"stepsCount": 3,
"totalEnrolled": 0,
"totalCompleted": 0,
"activatedAt": null,
"createdAt": "2026-02-17T12:00:00Z",
"updatedAt": "2026-02-17T12:00:00Z"
}There is no segmentId/segmentName field on an automation, and the enrollment counters are totalEnrolled/totalCompleted (not totalEnrollments/activeEnrollments/completedEnrollments). The detail response also adds steps (the full step list), completionRate, and totalFailed: enrolments that reached a SendEmail step with no template in the tenant (missing or since deleted).
List Automations
GET https://api.mail.land/v1/marketing/automations| Parameter | Type | Default | Description |
|---|---|---|---|
cursor | string | - | Opaque cursor from the previous page's nextCursor. Omit for the first page. |
limit | int | 20 | Items per page |
search | string | - | Search by name |
status | int | - | Filter by automation status |
trigger | int | - | Filter by trigger type |
mailingListId | string | - | Filter by mailing list |
sortBy | string | - | Sort field |
sortDesc | bool | false | Sort descending |
curl https://api.mail.land/v1/marketing/automations \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Get Automation Details
GET https://api.mail.land/v1/marketing/automations/{id}Returns the full automation including all steps:
curl https://api.mail.land/v1/marketing/automations/4f8k2m9p1q6z3s \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."{
"id": "4f8k2m9p1q6z3s",
"name": "Welcome Series",
"description": "3-email welcome sequence for new subscribers",
"trigger": "subscriberJoins",
"triggerName": "SubscriberJoins",
"status": "active",
"statusName": "Active",
"mailingListId": "7h3n5k8w2q9m1t",
"mailingListName": "Product Updates",
"campaignId": null,
"campaignName": null,
"stepsCount": 3,
"totalEnrolled": 230,
"totalCompleted": 200,
"totalFailed": 4,
"activatedAt": "2026-01-10T08:05:00Z",
"steps": [
{
"order": 1,
"type": "sendEmail",
"typeName": "SendEmail",
"delayMinutes": null,
"templateId": "2p9x4k7m3q8n1z",
"subject": "Welcome to our newsletter!",
"settings": null
},
{
"order": 2,
"type": "wait",
"typeName": "Wait",
"delayMinutes": 1440,
"templateId": null,
"subject": null,
"settings": null
},
{
"order": 3,
"type": "sendEmail",
"typeName": "SendEmail",
"delayMinutes": null,
"templateId": "9k4p2x7m1q3n8z",
"subject": "Getting started with MailLand",
"settings": null
}
],
"completionRate": 86.96,
"createdAt": "2026-01-10T08:00:00Z",
"updatedAt": "2026-02-17T12:00:00Z"
}Update an Automation
PUT https://api.mail.land/v1/marketing/automations/{id}Any status except Active can be updated (not only Draft/Paused). The request body matches the create request.
curl -X PUT https://api.mail.land/v1/marketing/automations/4f8k2m9p1q6z3s \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{
"name": "Welcome Series",
"trigger": 0,
"mailingListId": "7h3n5k8w2q9m1t",
"steps": [
{
"type": 0, "order": 1,
"subject": "Welcome!",
"templateId": "2p9x4k7m3q8n1z"
},
{ "type": 1, "order": 2, "delayMinutes": 1440 },
{
"type": 0, "order": 3,
"subject": "Getting started",
"templateId": "9k4p2x7m1q3n8z"
}
]
}'Activate an Automation
POST https://api.mail.land/v1/marketing/automations/{id}/activateTransitions the automation from Draft or Paused to Active. Once active, the automation begins enrolling subscribers when the trigger fires.
Activation itself is not checked against domain verification. A SendEmail step reached with no
fully verified domain is held, not skipped: it is re-checked on a growing interval (15 minutes,
then up to every 6 hours) and sends as soon as a domain verifies. If none verifies within 7 days of
the step first becoming due, the enrolment is marked Failed (counted in totalFailed) instead of
being held forever, so activating with no verified domain succeeds, but nothing delivers until you
verify one, and enrolments that are still waiting after a week stop rather than hold indefinitely.
curl -X POST https://api.mail.land/v1/marketing/automations/4f8k2m9p1q6z3s/activate \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Pause an Automation
POST https://api.mail.land/v1/marketing/automations/{id}/pausePauses the automation. No new subscribers are enrolled, and subscribers already in the sequence stop advancing: each enrolment holds at its current step. Activating the automation again resumes every held enrolment from where it stopped, delivering everything that fell due while paused.
curl -X POST https://api.mail.land/v1/marketing/automations/4f8k2m9p1q6z3s/pause \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Archive an Automation
POST https://api.mail.land/v1/marketing/automations/{id}/archiveArchives the automation, which can be done from any status including Active. Archived automations are hidden from default list views.
Archiving stops both new enrolments and further steps for subscribers already enrolled: each
enrolment holds at its current step, the same as Pause. Unlike pause, there
is no endpoint that reactivates an Archived automation, so this hold does not resolve on its own.
curl -X POST https://api.mail.land/v1/marketing/automations/4f8k2m9p1q6z3s/archive \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Duplicate an Automation
POST https://api.mail.land/v1/marketing/automations/{id}/duplicate?name=Copy%20of%20Welcome%20SeriesCreates a new Draft automation with cloned steps and settings. The name query parameter is optional.
curl -X POST "https://api.mail.land/v1/marketing/automations/4f8k2m9p1q6z3s/duplicate?name=Copy%20of%20Welcome%20Series" \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Trigger Manually
POST https://api.mail.land/v1/marketing/automations/{id}/trigger{
"subscriberIds": ["3m8k1p9x4q7n2z", "6q2n9k4x1p8m3z", "1x7m3q8k2n9p4z"]
}Manually enroll specific subscribers into the automation. The automation must be Active and its trigger type must be Manual (5).
curl -X POST https://api.mail.land/v1/marketing/automations/4f8k2m9p1q6z3s/trigger \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..." \
-H "Content-Type: application/json" \
-d '{"subscriberIds": ["3m8k1p9x4q7n2z", "6q2n9k4x1p8m3z", "1x7m3q8k2n9p4z"]}'Enrollment Tracking
Each subscriber's progress through the automation is tracked as an enrollment:
| Status | Value | Description |
|---|---|---|
Active | 0 | Currently moving through steps |
Paused | 1 | Enrollment paused |
Completed | 2 | Finished all steps |
Failed | 3 | A SendEmail step has no template in this tenant: missing from the start, or since deleted. Counted in the automation's totalFailed |
Unsubscribed | 4 | Subscriber unsubscribed during sequence |
There is no Cancelled enrollment status. The automation detail response includes aggregate enrollment counts as totalEnrolled and totalCompleted (not totalEnrollments/activeEnrollments/completedEnrollments/failedEnrollments).
Delete an Automation
DELETE https://api.mail.land/v1/marketing/automations/{id}Permanently deletes the automation, its steps, and enrollment data.
Update and delete are allowed for any status except Active, not only Draft/Paused (update) or Draft/Archived (delete) as earlier versions of this page stated.
curl -X DELETE https://api.mail.land/v1/marketing/automations/4f8k2m9p1q6z3s \
-H "Authorization: Bearer eyJhbGciOiJIUzI1NiIs..."Error Responses
| Status | Code | Description |
|---|---|---|
400 | VALIDATION_FAILED | Missing required fields or invalid step configuration: including a SendEmail step with no templateId or a Condition step with unevaluable settings |
400 | TEMPLATE_NOT_FOUND | A SendEmail step's templateId does not resolve to a template in your tenant |
400 | INVALID_REQUEST | Automation name already exists |
403 | FORBIDDEN | Insufficient permissions |
404 | RESOURCE_NOT_FOUND | Automation not found, or the status transition is invalid (update/activate/pause of the wrong status, delete of an Active automation) |
The automation endpoints never return 422.
Next Steps
- Campaigns: send one-time campaigns
- Subscribers: manage contacts that automations target
- Lists: organize subscribers into lists for triggers