The Gmail Multi-Step Email Campaign template sends a drip sequence (intro plus follow-ups) to contacts in a Google Sheet. Follow-ups only go out when the contact hasn't replied to the Gmail thread. This guide covers everything the template needs to work: the sheet layout, the emailSequence variable, workflow variables, and how step tracking behaves.
How it works
Each scheduled run:
- Reads all contact rows from your sheet.
- Filters out replied / unsubscribed / bounced contacts.
- Keeps only rows due today for their current step (based on
stepandfirst_emailed). - For follow-ups, checks the Gmail thread — if the contact replied, marks them
repliedand skips. - Sends the email for the contact's current step, then writes the send date, thread id, status, and the next step back to the sheet.
One run sends one step per contact. The next step goes out on a later run once its day offset is due.
1. Google Sheet setup
Create a sheet with a header row using these exact column names (the workflow matches columns by header, so spelling matters):
| Column | Filled by | Purpose |
|---|---|---|
email | You | Contact address (required) |
name | You | Used in {{name}} placeholders |
company | You | Used in {{company}} placeholders |
first_emailed | Workflow | Date of the first send — all follow-up timing is measured from this |
thread_id | Workflow | Gmail thread id, used for reply detection and threading |
status | Workflow | active after first send, replied when a reply is detected |
step | Workflow | Current sequence index (0-based) |
Warning: Use
thread_id, notthreadId. A mismatched header means the workflow can't find the thread — reply detection silently stops working.
New leads should have step empty or 0, and first_emailed, thread_id, status all blank. The workflow fills them in.
2. Workflow variables
Open the workflow's Variables and set:
| Variable | Example | Purpose |
|---|---|---|
spreadsheetUrl | your sheet URL | Used by Read Contacts and Update Contact Row |
campaignId | outreach-v1 | Tag added to each send for tracking |
emailSequence | see below | The sequence copy and timing |
contactList | [] | Runtime scratch state — leave as empty array |
scanIndex | 0 | Runtime scratch state — leave as 0 |
sentEmails | [] | Runtime scratch state — leave as empty array |
Warning: Published workflows run the published definition. After editing any variable (including
emailSequence), you must Publish again for scheduled runs to pick up the change.
3. The emailSequence array
This variable defines every email in the sequence. Each entry is an object with exactly these properties:
[
{
"day": 0,
"subject": "Nice to meet you",
"body": "Hi {{name}} — was wondering whether {{company}} would be interested in a quick call?"
},
{
"day": 3,
"subject": "Re: Nice to meet you",
"body": "Hi {{name}}, just wanted to check in on this. Let me know if there's any interest!"
},
{
"day": 7,
"subject": "Re: Nice to meet you",
"body": "{{name}}, just wanted to give this one last try."
}
]
| Property | Type | Meaning |
|---|---|---|
day | number | Days after first_emailed when this entry becomes due. The first entry should be 0. |
subject | string | Email subject. Supports {{field}} placeholders from the sheet row. |
body | string | Email body. Supports the same placeholders. Use \n for line breaks in JSON. |
Rules that matter:
- The timing property must be named
day. Entries with astepproperty instead ofdayfall back today: 0and break follow-up timing. dayvalues are anchored tofirst_emailed, not to the previous send.0, 3, 7means day 0, then 3 days after the first email, then 7 days after the first email. Keep them increasing.- Add as many entries as you like. The sequence is generic: a contact advances one entry per send and drops out automatically after the last entry (
stepreaches the array length). - Placeholders (
{{name}},{{company}},{{email}}, or any sheet column) are replaced per contact.
4. Step and status lifecycle
step | status | first_emailed | What happens |
|---|---|---|---|
| 0 / blank | blank | blank | Intro (entry 0) sends once |
| 0 | active | set | Skipped — intro already sent, never resent |
| 1+ | active | set | Follow-up sends when its day offset is due |
| any | replied | — | Skipped forever (reply check also sets this) |
After every successful send, Update Contact Row writes back:
| Sheet column | Written value |
|---|---|
first_emailed | Send date (kept from the first send onward) |
thread_id | Gmail thread id from the send |
status | active |
step | nextStep — the step just sent plus one |
Warning: If you customize Update Contact Row, the step column must use the pick node's
nextStepoutput (e.g.{{context.pick-next-contact.output.contact.nextStep}}), notsteporsequenceStep. Writing the current step back causes the same email to send on every run and the sequence never finishes.
5. Advance step rules (safety net)
The Due for Sequence and Scan Sequence Contact nodes include an Advance step rules config:
[
{
"atStep": 0,
"toStep": 1,
"whenAnyFieldSet": ["first_emailed"],
"whenStatusEquals": [{ "field": "status", "value": "active" }]
}
]
This handles rows where the intro clearly went out (first_emailed set or status is active) but the sheet step is still 0 — the row is treated as step 1 instead of being stuck or double-sent. Keep toStep at 1; jumping further skips sequence entries.
6. Testing the sequence
- Set every entry's
dayto0so all steps are due immediately (restore real offsets afterwards). - Reset your test row:
step→0, clearfirst_emailed,thread_id, andstatus. - Publish (required after variable edits).
- Run the workflow once per step. Each run should send the next entry and increment
stepby 1.
With real day offsets, remember: the third email in a 0 / 3 / 7 sequence cannot send until 7 days after first_emailed, no matter how many times the workflow runs.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Follow-ups never send | emailSequence entries use step instead of day | Rename the property to day |
Same email sends on every run; step doesn't advance | Update Contact Row writes step instead of nextStep | Point the step column at contact.nextStep |
first_emailed stays blank | Update Contact Row's first value references a field the pick node doesn't output | Use contact.firstEmailedForUpdate from the Scan Sequence Contact node |
| Reply detection never triggers | Sheet header is threadId (or anything other than the configured thread_id) | Rename the header to match threadIdField |
| Variable edits have no effect | Workflow not republished | Publish after every variable change |
| A step is skipped | toStep in advance rules set too high | Set toStep: 1 for the atStep: 0 rule |
| Contact stuck, nothing sends | Row's status is replied/unsubscribed/bounced, or the next entry's day isn't due yet | Check status and first_emailed date math |
Related
- Transform catalog — Sheet & email sequences for the underlying transform nodes
- Workflow variables for how variables work in general