Outreach Sequencer
Cold email outreach sequencer that sends personalized 3-step sequences via SMTP, SendGrid, or Mailgun — with automated day-3 and day-7 follow-ups. Built for sales teams, marketing agencies, and recruiters who need structured, scalable cold email campaigns without a subscription to heavyweight outreach platforms. Sequence state persists across runs in Apify KV Store, so follow-ups fire exactly on schedule regardless of when you run the actor.
Maintenance Pulse
90/100Documentation
Cold email outreach sequencer that sends personalized 3-step sequences via SMTP, SendGrid, or Mailgun — with automated day-3 and day-7 follow-ups. Built for sales teams, marketing agencies, and recruiters who need structured, scalable cold email campaigns without a subscription to heavyweight outreach platforms. Sequence state persists across runs in Apify KV Store, so follow-ups fire exactly on schedule regardless of when you run the actor.
This actor handles the full sequence lifecycle: send the initial email, wait three days, send the first follow-up, wait four more days, send the final touch. All three emails are personalized per contact using {{firstName}}, {{companyName}}, {{topService}}, and {{summarySnippet}} template variables. No code required to get started — configure your provider credentials, paste your templates, and run.
What data can you extract?
Every email dispatch produces a structured output record you can download as JSON or CSV.
| Data Point | Source | Example |
|---|---|---|
| 📧 Recipient email | Lead input | [email protected] |
| 👤 First name | Lead input | James |
| 🏢 Company name | Lead input | Pinnacle Tech |
| ✅ Send status | Provider response | sent |
| 🔢 Sequence step | Actor logic | 1 (initial), 2 (day 3), 3 (day 7) |
| 🔑 Sequence ID | UUID v4 generated per contact | a3f7c219-84bb-4e12-9c3d-ff217a8b10cd |
| 📝 Rendered subject | Template engine | Quick question for Pinnacle Tech |
| 📅 Follow-up scheduled | Sequence state | true |
| 🕐 Next follow-up timestamp | Sequence state | 2024-11-18T09:14:22.000Z |
| ⚠️ Error message | Provider response | null |
| 🧪 Dry run flag | Input | false |
| 🕓 Processed at | Actor run | 2024-11-15T09:14:22.000Z |
Why use Outreach Sequencer?
Setting up cold email sequences manually means copy-pasting personalized emails, tracking who received which step in a spreadsheet, and remembering to send follow-ups three or seven days later. Mistakes are routine: duplicates go out, follow-ups reach people who replied, contacts get skipped entirely. Dedicated outreach platforms charge $37–149/month per seat and lock you into their sending infrastructure with limited provider choice.
This actor automates the entire sequence lifecycle. You bring your own sending credentials — any SMTP-capable provider, SendGrid API v3, or Mailgun API. The actor tracks every sequence in Apify KV Store and handles all the timing logic for you.
- Scheduling — run on a daily Apify schedule;
advancemode detects and sends only the follow-ups that are actually due - API access — trigger runs from Python, JavaScript, or any HTTP client via the Apify API
- Provider flexibility — use Brevo, Gmail Workspace, AWS SES, SendGrid, Mailgun, or any SMTP server with no lock-in
- Monitoring — get Slack or email alerts when runs fail or email counts drop unexpectedly
- Integrations — push output records to Google Sheets, HubSpot, or webhooks via Zapier or Make
Features
- Three sending providers — SMTP (nodemailer, port 587 STARTTLS or 465 SSL), SendGrid REST API v3 (
POST /v3/mail/send, 202-acknowledged), and Mailgun Messages API (HTTP Basic auth,application/x-www-form-urlencoded) — all return the same output shape - Two-mode operation —
startmode sends step 1 to new leads;advancemode scans the KV store for sequences wherenextFollowUpAt <= now()and dispatches the next due step - Persistent sequence state — each contact's state (
step1SentAt,step2SentAt,step3SentAt,repliedAt,unsubscribedAt,nextFollowUpAt) is stored in a named Apify KV Store, keyed by base64url-encoded email address — survives run restarts and scheduling gaps - UUID v4 sequence IDs — every contact gets a stable, unique
sequenceIdgenerated at first contact; used in unsubscribe URLs for per-contact opt-out tracking - Day-3 and day-7 follow-up scheduling — step 2 fires 3 days after step 1; step 3 fires 7 days after step 1, calculated from the original send baseline so the sequence always spans exactly 7 days from first contact
{{variable}}template engine — five substitution variables available in all templates:{{firstName}},{{companyName}},{{topService}},{{summarySnippet}},{{unsubscribeLink}}; unsubscribe URL renders before insertion so{{sequenceId}}and{{email}}inside the URL resolve correctly- Automatic deduplication — contacts with
step1SentAtalready set are skipped instartmode; unsubscribed or replied contacts are skipped inadvancemode - Per-run rate limiting — hard cap via
rateLimitPerRun(max 10,000); excess leads getstatus: deferredin the dataset with no charge - Dry-run mode — renders templates and logs output without dispatching any emails or incurring PPE charges; use to verify personalization before going live
- Spending limit compliance — stops immediately when the Apify per-run charge limit is reached; dataset record is written before each charge event so no data is lost
- Summary records — each run appends a
type: "summary"record with totals (sent,failed,deferred,skipped) for easy monitoring - Multi-campaign isolation — set a unique
kvStoreNameper campaign so sequences from different campaigns never collide - Environment variable credential fallback — SMTP credentials and API keys fall back to
SMTP_USER,SMTP_PASS,EMAIL_API_KEYenvironment variables if not provided in input, enabling secrets management through Apify environment variables
Use cases for cold email outreach sequencer
Sales prospecting and SDR outreach
Sales development reps building outbound pipelines spend hours manually sending initial emails and tracking follow-up timing in CRMs. Feed a list of enriched leads — with firstName, companyName, and a summarySnippet from research — into start mode, then schedule advance mode to run daily. The actor sends each contact's next due step automatically, freeing reps to focus on replies rather than logistics.
Marketing agency lead generation
Agencies building prospect lists for clients need to run outreach across multiple simultaneous campaigns. Use a separate kvStoreName per campaign to isolate sequences. Combine with Google Maps Email Extractor to source local business contacts, then pipe directly into this actor's leads array for immediate sequencing.
Recruiting and talent sourcing
Recruiters reaching out to passive candidates need structured multi-touch sequences as much as sales teams do. Personalize summarySnippet with the role or opportunity context, and use {{topService}} (populated from the lead's servicesOffered array) to reference the candidate's primary skill. The followUpTemplate3 and followUpTemplate7 fields let you craft softer nudges as the sequence progresses.
B2B lead enrichment pipelines
Teams using Waterfall Contact Enrichment or Website Contact Scraper to build contact databases can feed enriched records directly into this actor's leads array. The output dataset links back to sequenceId, making it straightforward to join send activity against your CRM.
Conference and event follow-up
After collecting leads from a conference or trade show, use this actor to execute a personalized post-event sequence. The summarySnippet field carries context (e.g., what was discussed at the booth), and the three-step cadence matches standard conference follow-up best practice: day-of intro, day-3 value add, day-7 final ask.
Competitive intelligence outreach
Market researchers and intelligence teams at companies using Company Deep Research to profile target accounts can execute outreach to identified contacts with company-specific personalization pre-populated in the leads array.
How to send cold email sequences
-
Choose your email provider and enter credentials — Select SMTP, SendGrid, or Mailgun. For SMTP, enter your host (e.g.,
smtp-relay.brevo.com), port (587), username, and password. For SendGrid or Mailgun, enter your API key and, for Mailgun, your verified sending domain. -
Paste your lead list — In the
leadsfield, provide an array of contacts. Each contact needs at minimum anemailaddress. AddfirstName,companyName,servicesOffered, andsummarySnippetto enable personalization in your templates. -
Write your templates — Enter your step-1 email body and subject. Use
{{firstName}},{{companyName}},{{topService}}, and{{summarySnippet}}for personalization. Add{{unsubscribeLink}}in every template (required for CAN-SPAM compliance). Optionally fill infollowUpTemplate3andfollowUpTemplate7for automated follow-ups. -
Run in
startmode, then scheduleadvancemode — Click "Start" withmode: startto send the initial emails. Set up a daily scheduled run withmode: advance(no leads array needed) to send day-3 and day-7 follow-ups as they come due. Download results from the Dataset tab in JSON or CSV.
Input parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
mode | string (enum) | Yes | start | start — sends step 1 to new leads; advance — scans KV store and sends due follow-ups |
leads | array | Yes (start mode) | [] | Array of lead objects: { email, firstName, companyName, servicesOffered[], summarySnippet } |
emailProvider | string (enum) | Yes | smtp | smtp, sendgrid, or mailgun |
smtpHost | string | SMTP only | — | SMTP server hostname (e.g., smtp-relay.brevo.com, smtp.gmail.com) |
smtpPort | integer | SMTP only | 587 | 587 for STARTTLS (recommended), 465 for SSL |
smtpUser | string | SMTP only | — | SMTP login username (usually your email address or API key username) |
smtpPass | string | SMTP only | — | SMTP password or SMTP-specific API key. Stored encrypted. |
apiKey | string | SendGrid / Mailgun | — | SendGrid: "Mail Send" API key. Mailgun: private API key starting with key-. Stored encrypted. |
mailgunDomain | string | Mailgun only | — | Verified Mailgun sending domain (e.g., mail.yourdomain.com) |
fromName | string | Yes | — | Sender display name shown in the From field (e.g., Jane at Acme Corp) |
fromEmail | string | Yes | — | Verified sender email address with SPF/DKIM/DMARC configured |
unsubscribeUrl | string | Yes | — | Opt-out endpoint URL. Supports {{sequenceId}} and {{email}} substitutions. |
rateLimitPerRun | integer | Yes | 100 | Hard cap on emails per run. Excess leads get status: deferred. Range: 1–10,000. |
emailTemplate | string | Yes | (see prefill) | Step-1 email body. Supports {{firstName}}, {{companyName}}, {{topService}}, {{summarySnippet}}, {{unsubscribeLink}}. |
subjectLine | string | Yes | Quick question for {{companyName}} | Step-1 subject. Supports {{companyName}} and {{firstName}}. |
followUpTemplate3 | string | No | "" | Day-3 follow-up body template. Leave blank to disable. |
followUpTemplate7 | string | No | "" | Day-7 follow-up body template. Leave blank to disable. |
followUpSubject3 | string | No | "" | Day-3 subject. Defaults to Re: {subjectLine} if blank. |
followUpSubject7 | string | No | "" | Day-7 subject. Defaults to Re: {subjectLine} if blank. |
kvStoreName | string | No | outreach-sequences | Named KV store for sequence state. Use a unique name per campaign. |
dryRun | boolean | No | false | When true, templates render and log but no emails are sent and no charges apply. |
Input examples
Simple single-step campaign via SMTP (most common):
{
"mode": "start",
"emailProvider": "smtp",
"smtpHost": "smtp-relay.brevo.com",
"smtpPort": 587,
"smtpUser": "[email protected]",
"smtpPass": "your-smtp-password",
"fromName": "Sarah at Pinnacle Digital",
"fromEmail": "[email protected]",
"unsubscribeUrl": "https://pinnacledigital.io/unsubscribe?sid={{sequenceId}}&e={{email}}",
"rateLimitPerRun": 50,
"subjectLine": "Quick question for {{companyName}}",
"emailTemplate": "Hi {{firstName}},\n\nI came across {{companyName}} and was impressed by your work in {{topService}}.\n\n{{summarySnippet}}\n\nWould you be open to a quick 15-minute call this week?\n\nBest,\nSarah\n\nOpt out: {{unsubscribeLink}}",
"kvStoreName": "campaign-nov-2024",
"dryRun": false,
"leads": [
{
"email": "[email protected]",
"firstName": "James",
"companyName": "Acme Corp",
"servicesOffered": ["SEO", "PPC"],
"summarySnippet": "Your recent case study on reducing CPA by 40% for e-commerce brands caught my attention."
},
{
"email": "[email protected]",
"firstName": "Linda",
"companyName": "Beta Industries",
"servicesOffered": ["Content Marketing"],
"summarySnippet": "I noticed Beta Industries has been expanding into B2B SaaS content — an area where we've driven strong pipeline results."
}
]
}
Full 3-step sequence via SendGrid API:
{
"mode": "start",
"emailProvider": "sendgrid",
"apiKey": "SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
"fromName": "Marcus at GrowthLab",
"fromEmail": "[email protected]",
"unsubscribeUrl": "https://growthlab.co/opt-out?sid={{sequenceId}}&e={{email}}",
"rateLimitPerRun": 100,
"subjectLine": "Idea for {{companyName}}",
"emailTemplate": "Hi {{firstName}},\n\nI've been following {{companyName}}'s work in {{topService}} and had an idea I wanted to share.\n\n{{summarySnippet}}\n\nWorth a few minutes?\n\nBest,\nMarcus\n\nUnsubscribe: {{unsubscribeLink}}",
"followUpTemplate3": "Hi {{firstName}},\n\nJust bumping this to the top of your inbox in case it got buried.\n\nStill happy to share the idea — only takes 5 minutes.\n\nBest,\nMarcus\n\nUnsubscribe: {{unsubscribeLink}}",
"followUpSubject3": "",
"followUpTemplate7": "Hi {{firstName}},\n\nLast nudge on this — I know inboxes get hectic.\n\nIf the timing isn't right, no worries at all. I'll check back in a few months.\n\nBest,\nMarcus\n\nUnsubscribe: {{unsubscribeLink}}",
"followUpSubject7": "",
"kvStoreName": "growthlab-q4-outreach",
"dryRun": false,
"leads": [
{
"email": "[email protected]",
"firstName": "Priya",
"companyName": "Delta Systems",
"servicesOffered": ["Performance Marketing"],
"summarySnippet": "Delta Systems has been scaling paid acquisition aggressively — I wanted to share a framework we've used to offset rising CPCs."
}
]
}
Advance mode (daily scheduled run — no leads array needed):
{
"mode": "advance",
"emailProvider": "smtp",
"smtpHost": "smtp-relay.brevo.com",
"smtpPort": 587,
"smtpUser": "[email protected]",
"smtpPass": "your-smtp-password",
"fromName": "Sarah at Pinnacle Digital",
"fromEmail": "[email protected]",
"unsubscribeUrl": "https://pinnacledigital.io/unsubscribe?sid={{sequenceId}}&e={{email}}",
"rateLimitPerRun": 100,
"subjectLine": "Quick question for {{companyName}}",
"emailTemplate": "",
"followUpTemplate3": "Hi {{firstName}},\n\nJust following up on my note from a few days ago.\n\nWould love to connect — does 15 minutes work this week?\n\nBest,\nSarah\n\nOpt out: {{unsubscribeLink}}",
"followUpTemplate7": "Hi {{firstName}},\n\nOne last note — if the timing isn't right, completely understand. I'll reach out again next quarter.\n\nBest,\nSarah\n\nOpt out: {{unsubscribeLink}}",
"kvStoreName": "campaign-nov-2024",
"dryRun": false
}
Input tips
- Test with
dryRun: truefirst — templates render fully and log to the actor console, but no emails are sent and no charges apply. Verify personalization looks correct before going live. - Use a unique
kvStoreNameper campaign — sequences from different campaigns share state if they share a KV store name. Name each campaign distinctly (e.g.,campaign-q4-2024-saas) to prevent collisions. - Schedule
advancemode daily — set up a recurring Apify schedule withmode: advancerunning every 24 hours. It only sends emails that are actually due, so running it daily has no side effects on contacts not yet ready for a follow-up. - Respect your provider's daily sending limits — Brevo free tier allows 300 emails/day; SendGrid free tier allows 100/day. Set
rateLimitPerRunbelow your provider's daily cap. Deferred leads remain in the dataset withstatus: deferred. - Leave
emailTemplateblank inadvancemode — the step-1 template is not used when scanning for follow-ups. OnlyfollowUpTemplate3andfollowUpTemplate7are dispatched in advance mode.
Output example
{
"email": "[email protected]",
"companyName": "Acme Corp",
"firstName": "James",
"status": "sent",
"step": 1,
"sequenceId": "a3f7c219-84bb-4e12-9c3d-ff217a8b10cd",
"subject": "Quick question for Acme Corp",
"followUpScheduled": true,
"nextFollowUpAt": "2024-11-18T09:14:22.000Z",
"error": null,
"dryRun": false,
"processedAt": "2024-11-15T09:14:22.000Z"
}
Each run also appends a summary record:
{
"type": "summary",
"mode": "start",
"totalLeads": 50,
"sent": 47,
"failed": 1,
"deferred": 2,
"skipped": 0,
"dryRun": false,
"completedAt": "2024-11-15T09:19:04.000Z"
}
Output fields
| Field | Type | Description |
|---|---|---|
email | string | Recipient email address (normalized to lowercase) |
companyName | string | Company name from lead input |
firstName | string | First name from lead input |
status | string | sent, failed, deferred, dry-run, or skipped |
step | integer | Sequence step: 1 (initial), 2 (day-3 follow-up), 3 (day-7 follow-up) |
sequenceId | string | UUID v4 assigned to this contact at step 1; stable across all steps |
subject | string | Fully rendered subject line with all variables substituted |
followUpScheduled | boolean | true if a future follow-up step has been scheduled in KV store |
nextFollowUpAt | string or null | ISO 8601 timestamp when the next follow-up becomes due |
error | string or null | Provider error message if status is failed; otherwise null |
dryRun | boolean | Whether this record was produced in dry-run mode |
processedAt | string | ISO 8601 timestamp when this record was created |
type | string | Present only on summary records: "summary" |
mode | string | Present only on summary records: "start" or "advance" |
totalLeads | integer | Present only on start-mode summary: total leads in input array |
totalSequencesScanned | integer | Present only on advance-mode summary: KV store keys examined |
sent | integer | Present only on summary records: emails successfully dispatched |
failed | integer | Present only on summary records: emails that returned a provider error |
deferred | integer | Present only on start-mode summary: leads not sent due to rate limit |
skipped | integer | Present only on summary records: contacts skipped (duplicate, unsubscribed, replied) |
How much does it cost to send cold email sequences?
Outreach Sequencer uses pay-per-event pricing — you pay $0.05 per email successfully dispatched to any provider. Dry-run mode is not charged. Platform compute costs are included.
| Scenario | Emails sent | Cost per email | Total cost |
|---|---|---|---|
| Quick test (10 contacts, step 1 only) | 10 | $0.05 | $0.50 |
| Small campaign (50 contacts, step 1) | 50 | $0.05 | $2.50 |
| Medium campaign (200 contacts, all 3 steps) | 600 | $0.05 | $30.00 |
| Large campaign (500 contacts, all 3 steps) | 1,500 | $0.05 | $75.00 |
| Enterprise (2,000 contacts, all 3 steps) | 6,000 | $0.05 | $300.00 |
You can set a maximum spending limit per run to control costs. The actor stops when your budget is reached; contacts that did not receive an email get status: deferred in the output dataset.
Compare this to dedicated outreach platforms: Lemlist charges $59/month per seat, Instantly charges $37/month, Reply.io charges $60/month — all with annual commitments and platform lock-in. With this actor, a 200-contact, 3-step campaign costs $30 with no subscription and no seat fees. Most users spend $5–40/month depending on campaign volume.
Send cold email sequences using the API
Python
from apify_client import ApifyClient
client = ApifyClient("YOUR_API_TOKEN")
run = client.actor("ryanclinton/outreach-sequencer").call(run_input={
"mode": "start",
"emailProvider": "smtp",
"smtpHost": "smtp-relay.brevo.com",
"smtpPort": 587,
"smtpUser": "[email protected]",
"smtpPass": "your-smtp-password",
"fromName": "Sarah at Pinnacle Digital",
"fromEmail": "[email protected]",
"unsubscribeUrl": "https://pinnacledigital.io/unsubscribe?sid={{sequenceId}}&e={{email}}",
"rateLimitPerRun": 50,
"subjectLine": "Quick question for {{companyName}}",
"emailTemplate": "Hi {{firstName}},\n\nI noticed {{companyName}}'s work in {{topService}}.\n\n{{summarySnippet}}\n\nWould a quick call be worth your time?\n\nBest,\nSarah\n\nOpt out: {{unsubscribeLink}}",
"kvStoreName": "campaign-api-test",
"dryRun": False,
"leads": [
{
"email": "[email protected]",
"firstName": "James",
"companyName": "Acme Corp",
"servicesOffered": ["SEO"],
"summarySnippet": "Your recent blog post on organic growth caught my attention."
}
]
})
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
if item.get("type") == "summary":
print(f"Summary — sent: {item['sent']}, failed: {item['failed']}, deferred: {item['deferred']}")
else:
print(f"{item['email']} | step {item['step']} | status: {item['status']} | next follow-up: {item.get('nextFollowUpAt')}")
JavaScript
import { ApifyClient } from "apify-client";
const client = new ApifyClient({ token: "YOUR_API_TOKEN" });
const run = await client.actor("ryanclinton/outreach-sequencer").call({
mode: "start",
emailProvider: "sendgrid",
apiKey: "SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
fromName: "Marcus at GrowthLab",
fromEmail: "[email protected]",
unsubscribeUrl: "https://growthlab.co/opt-out?sid={{sequenceId}}&e={{email}}",
rateLimitPerRun: 100,
subjectLine: "Idea for {{companyName}}",
emailTemplate: "Hi {{firstName}},\n\nI had an idea for {{companyName}} in {{topService}}.\n\n{{summarySnippet}}\n\nWorth 10 minutes?\n\nBest,\nMarcus\n\nUnsubscribe: {{unsubscribeLink}}",
followUpTemplate3: "Hi {{firstName}},\n\nJust bumping this up — let me know if the timing works.\n\nBest,\nMarcus\n\nUnsubscribe: {{unsubscribeLink}}",
kvStoreName: "growthlab-q4",
dryRun: false,
leads: [
{
email: "[email protected]",
firstName: "Priya",
companyName: "Delta Systems",
servicesOffered: ["Performance Marketing"],
summarySnippet: "Delta Systems' recent expansion into B2B SaaS caught my attention — we've driven strong pipeline results in that space."
}
]
});
const { items } = await client.dataset(run.defaultDatasetId).listItems();
for (const item of items) {
if (item.type === "summary") {
console.log(`Summary — sent: ${item.sent}, failed: ${item.failed}`);
} else {
console.log(`${item.email} | step ${item.step} | ${item.status} | followUp: ${item.nextFollowUpAt ?? "none"}`);
}
}
cURL
# Start the actor run (start mode — sends step 1)
curl -X POST "https://api.apify.com/v2/acts/ryanclinton~outreach-sequencer/runs?token=YOUR_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"mode": "start",
"emailProvider": "smtp",
"smtpHost": "smtp-relay.brevo.com",
"smtpPort": 587,
"smtpUser": "[email protected]",
"smtpPass": "your-smtp-password",
"fromName": "Sarah at Pinnacle Digital",
"fromEmail": "[email protected]",
"unsubscribeUrl": "https://pinnacledigital.io/unsubscribe?sid={{sequenceId}}&e={{email}}",
"rateLimitPerRun": 50,
"subjectLine": "Quick question for {{companyName}}",
"emailTemplate": "Hi {{firstName}},\n\nI noticed {{companyName}} is growing in {{topService}}.\n\n{{summarySnippet}}\n\nWould a brief call be worth your time?\n\nBest,\nSarah\n\nOpt out: {{unsubscribeLink}}",
"kvStoreName": "campaign-curl-test",
"dryRun": false,
"leads": [{"email": "[email protected]","firstName": "James","companyName": "Acme Corp","servicesOffered": ["SEO"],"summarySnippet": "Your work in organic growth is impressive."}]
}'
# Fetch results (replace DATASET_ID from the run response)
curl "https://api.apify.com/v2/datasets/DATASET_ID/items?token=YOUR_API_TOKEN&format=json"
How Outreach Sequencer works
Mode: start — initial email dispatch
When mode is start, the actor iterates the leads array in order. For each lead, it normalizes the email to lowercase and checks the named KV store for an existing sequence state keyed by base64url(email). If step1SentAt is already set, the contact is skipped. Otherwise, a new SequenceState is created with a fresh UUID v4 sequenceId and the lead's personalization data (firstName, companyName, topService from servicesOffered[0], summarySnippet).
The template engine performs ordered {{token}} substitution using a split-join method (no regex) to avoid escaping issues with special characters. Data variables substitute first. The unsubscribe URL renders separately — with {{sequenceId}} and {{email}} resolved inside it — before being inserted as {{unsubscribeLink}} in the email body. The rendered email then dispatches to the configured provider: SMTP via nodemailer with 30-second connection and socket timeouts, SendGrid via POST /v3/mail/send expecting HTTP 202, or Mailgun via POST /{domain}/messages with HTTP Basic auth.
State is persisted to the KV store only on sent or dry-run status. The output dataset record is pushed before the PPE charge event to ensure data is never lost if charging fails.
Mode: advance — follow-up scheduling
When mode is advance, the actor uses the Apify client to paginate all KV store keys in batches of 1,000. For each key it loads the sequence state and calls dueStep(): the function returns 2 if step1SentAt is set, step2SentAt is null, and nextFollowUpAt <= now(); returns 3 if step2SentAt is set, step3SentAt is null, and nextFollowUpAt <= now(); returns null otherwise. Contacts with unsubscribedAt or repliedAt set are always skipped.
Day-3 follow-up timing is set as step1SentAt + 3 days when step 1 is dispatched. Day-7 follow-up timing is calculated from the original step1SentAt baseline, not the step-2 send time — so the total sequence always spans 7 days from first contact regardless of when the actor last ran. After step 3, nextFollowUpAt is set to null and the sequence is complete.
KV store state management
Each contact's sequence state is a JSON object stored at a base64url-encoded key derived from the normalized email address. The schema tracks sequenceId, email, companyName, all three step timestamps, and the repliedAt and unsubscribedAt fields. You can set these fields externally via the Apify KV Store API to suppress future emails for any contact. Multiple campaigns run in parallel using separate kvStoreName values with complete isolation.
Tips for best results
-
Verify SPF, DKIM, and DMARC before your first send. Deliverability depends entirely on your domain configuration, not the actor. Use mail-tester.com or a similar tool to score your setup before running a live campaign.
-
Start with
rateLimitPerRun: 10anddryRun: truefirst. Check that templates render correctly for each lead, then disable dry run and increase the limit for the live run. Catching a broken template after 200 sends is expensive to recover from. -
Keep
summarySnippetunder 2 sentences. The variable substitutes inline — long snippets make emails feel templated. Aim for one specific observation about the company, not a generic pitch paragraph. -
Match
rateLimitPerRunto your provider's daily limit minus a buffer. If your Brevo account sends 300/day across all campaigns, set this actor's limit to 200 to leave headroom for transactional email. -
Use a unique
kvStoreNameper campaign cadence. Running a new campaign to a partially overlapping list? Use a new KV store name so prior sequence state from the old campaign does not affect new sequences. -
Combine with Bulk Email Verifier before sending. Sending to invalid addresses damages sender reputation. Verify your list first and remove hard bounces before they reach this actor.
-
Set the advance-mode schedule to run at the same time each day. The
dueStep()check is a simplenextFollowUpAt <= now()comparison. Running the schedule at 9 AM daily means follow-ups send within 24 hours of becoming due. -
Mark replies and unsubscribes in the KV store promptly. Use the Apify KV Store API to set
repliedAtorunsubscribedAton a contact's state record when you receive a reply or opt-out. Theadvancemode checks these flags before every dispatch.
Combine with other Apify actors
| Actor | How to combine |
|---|---|
| Website Contact Scraper | Scrape emails and company information from prospect websites, then pipe directly into this actor's leads array for immediate step-1 outreach |
| Google Maps Email Extractor | Extract local business contacts from Google Maps searches, then sequence them as a geographically targeted outreach campaign |
| Bulk Email Verifier | Verify emails via MX and SMTP checks before sending to avoid hard bounces that damage sender reputation |
| Waterfall Contact Enrichment | Enrich a list of company names into full contact records (email, name, role) ready for the leads array |
| B2B Lead Qualifier | Score leads 0–100 from 30+ signals before sequencing — filter to high-scorers only for premium outreach batches |
| HubSpot Lead Pusher | Push output dataset records into HubSpot after each run to sync sequence activity against your CRM contacts |
| Email Pattern Finder | Detect the email naming convention for a target company domain before building your lead list |
Limitations
- Plain text emails only — the actor sends
text/plainbodies, not HTML. This is intentional for cold outreach (plain text has better deliverability for cold contact and avoids spam filters triggered by heavy HTML), but means no formatted layouts, images, or styled links. - No reply detection — the actor does not monitor inboxes. You must set
repliedAton a contact's KV store record manually (or via a webhook from your email provider) to prevent follow-ups after a reply. - No unsubscribe endpoint provided — the actor generates
{{unsubscribeLink}}URLs withsequenceIdandemailembedded, but you must build and host the opt-out endpoint yourself. SettingunsubscribedAtin KV store state must also be handled by your system. - Mailgun US region only — the Mailgun provider uses
api.mailgun.net(US region). EU-region Mailgun customers should use the SMTP provider withsmtpHost: smtp.eu.mailgun.orginstead. - No built-in scheduling — the actor does not self-schedule. You must create an Apify schedule manually to run
advancemode daily for follow-ups to fire automatically. - No attachment support — email attachments are not supported. Link to hosted files in the email body instead.
- Rate limits are per-run, not per-day — if you trigger
startmode multiple times in a day, each run has its ownrateLimitPerRuncap. You are responsible for staying within your provider's daily sending limits across all runs. - No A/B testing at the actor level — for split-testing subject lines or templates, run the actor twice with different configurations and separate
kvStoreNamevalues.
Integrations
- Zapier — trigger a Zap when a run completes to push
sentrecords into a Google Sheet or create HubSpot deals for new sequences - Make — build a scenario that watches the output dataset and sets
repliedAtin KV store when your inbox integration detects a reply - Google Sheets — export the output dataset after each run to maintain a live campaign tracker spreadsheet
- Apify API — trigger
startandadvanceruns programmatically from your CRM, marketing automation platform, or lead management system - Webhooks — fire a webhook when each run finishes to notify your team in Slack or trigger downstream data pipeline steps
- LangChain / LlamaIndex — generate personalized
summarySnippetvalues per lead using an LLM agent, then pass the enriched lead array into this actor's input
Troubleshooting
-
Emails show
status: failedwith SMTP credentials error — confirm thatsmtpHost,smtpUser, andsmtpPassare all provided. For Brevo, the SMTP user is your Brevo login email and the password is a Brevo SMTP key (not your Brevo account password). For SendGrid SMTP, useapikeyas the username and your API key as the password. -
SendGrid returns a non-202 status — the actor surfaces error messages from the SendGrid API response body. Common causes: API key missing "Mail Send" permission; sender email not verified in your SendGrid account; or recipient address on SendGrid's suppression list. Check your SendGrid Activity Feed for per-message detail.
-
Follow-ups not sending in
advancemode — confirm thatfollowUpTemplate3orfollowUpTemplate7is non-empty in the advance-mode run input. The actor requires the template to be present in the run that sends the follow-up, not just the run that sent step 1. Also confirm enough time has elapsed — day-3 follow-ups only fire oncenextFollowUpAt <= now(). -
Contacts being skipped unexpectedly — in
startmode, contacts with an existingstep1SentAtin KV store are always skipped to prevent duplicates. To re-enqueue a contact after a bounced first send, delete their KV store record via the Apify console or API before re-running. -
Template renders with blank
{{topService}}— this field is populated from the first item of the lead'sservicesOfferedarray. IfservicesOfferedis absent or empty in the lead object,{{topService}}renders as an empty string. Ensure the field is included in your leads input.
Responsible use
- This actor sends emails on your behalf using credentials you provide. You are responsible for compliance with all applicable laws in your jurisdiction and the recipient's jurisdiction.
- Include a working
{{unsubscribeLink}}in every template. CAN-SPAM requires a functioning opt-out mechanism in every commercial email, and you must honor opt-outs within 10 business days. - Comply with GDPR, CASL, and other regional data protection and anti-spam regulations when sending to contacts in those jurisdictions.
- Only send to contacts with a legitimate basis for outreach. Do not use purchased email lists without proper consent documentation.
- Respect opt-outs immediately — set
unsubscribedAtin KV store state as soon as a contact opts out, and do not send further emails to that contact. - For guidance on email outreach legality, see Apify's guide on web scraping and data use.
FAQ
How does the cold email sequencer handle follow-up timing?
After step 1 is sent, the actor stores nextFollowUpAt = step1SentAt + 3 days in the KV store. When advance mode runs and finds nextFollowUpAt <= now(), it sends the day-3 follow-up and updates nextFollowUpAt = step1SentAt + 7 days. The day-7 timestamp is calculated from the original step-1 baseline, not the step-2 send time, so the total sequence always spans exactly 7 days from first contact.
Can I use this actor with Gmail SMTP?
Yes. Set smtpHost: smtp.gmail.com, smtpPort: 587, smtpUser to your Gmail address, and smtpPass to a Gmail App Password — not your Google account password. You must enable 2-Step Verification and generate an App Password in Google Account Security settings. Note that Gmail has daily sending limits; check your organization's settings if using Google Workspace.
What happens if a contact replies? Will follow-ups still send?
The actor does not monitor inboxes. To suppress follow-ups after a reply, set repliedAt on the contact's KV store record via the Apify KV Store API or a webhook from your email provider. The advance mode skips any contact where repliedAt is set.
How many contacts can I sequence in one run?
There is no hard limit beyond rateLimitPerRun (max 10,000 per run) and the actor's 1-hour timeout. Each email dispatch takes 1–3 seconds including provider round-trip time. At 100 emails/run, expect 2–5 minutes. Large batches approaching 1,000 may get close to the timeout; split them across multiple scheduled runs if needed.
Can I run multiple campaigns simultaneously without them interfering?
Yes. Set a unique kvStoreName for each campaign (e.g., campaign-q4-saas, campaign-nov-agencies). KV stores are completely isolated. Contacts in one campaign's store have no effect on contacts in another, even if the same email address appears in both.
How is this different from Lemlist, Instantly, or Reply.io? Dedicated outreach platforms charge $37–149/month per seat and require you to use their sending infrastructure. This actor charges $0.05 per email sent, accepts your own sending credentials (any SMTP provider, SendGrid, or Mailgun), runs inside Apify's scheduling and monitoring infrastructure, and produces structured output you can integrate with any downstream tool. There are no subscriptions, no seat limits, and no platform lock-in.
Is it legal to send cold outreach emails with this actor? Cold email legality depends on your jurisdiction and the recipient's location. In the US, CAN-SPAM permits cold B2B outreach if you include a physical address, a working opt-out link, and honor opt-outs promptly. In the EU, GDPR and ePrivacy rules are stricter — you generally need a documented legitimate interest basis before contacting individuals. You are responsible for your own compliance. See Apify's legal guide for more context.
Can I disable follow-ups and use this as a single-email sender?
Yes. Leave both followUpTemplate3 and followUpTemplate7 blank. No nextFollowUpAt is scheduled after step 1 and no follow-ups will ever fire. The actor behaves as a personalized bulk email sender with deduplication and per-run rate limiting.
What happens if the actor hits the spending limit mid-run?
The actor stops immediately when the Apify spending limit is reached. All contacts processed before the limit are recorded in the dataset with their status. Contacts not reached remain in the input list. Re-run the actor with the same lead list — contacts with step1SentAt already set will be skipped automatically, so only uncontacted leads are sent to.
Can I schedule this actor to run automatically?
Yes. Create an Apify schedule for advance mode to run daily. For start mode, trigger it via the Apify API from your CRM or lead pipeline whenever a new batch of contacts is ready. See the Apify scheduling documentation for setup instructions.
Does the actor support HTML emails?
No. All three providers send text/plain only. Plain text is deliberate — it avoids spam filter penalties that heavily templated HTML triggers and produces higher inbox placement for cold outreach. If you need HTML, you can extend the actor's source code or use your provider's template API separately.
What template variables are available?
Five variables work in all templates: {{firstName}}, {{companyName}}, {{topService}} (first item in servicesOffered), {{summarySnippet}}, and {{unsubscribeLink}}. Subject lines support {{companyName}} and {{firstName}}. The unsubscribeUrl input field itself supports {{sequenceId}} and {{email}} for building per-contact opt-out links.
Help us improve
If you encounter issues, you can help us debug faster by enabling run sharing in your Apify account:
- Go to Account Settings > Privacy
- Enable Share runs with public Actor creators
This lets us see your run details when something goes wrong, so we can fix issues faster. Your data is only visible to the actor developer, not publicly.
Support
Found a bug or have a feature request? Open an issue in the Issues tab on this actor's page. For custom solutions or enterprise integrations, reach out through the Apify platform.
How it works
Configure
Set your parameters in the Apify Console or pass them via API.
Run
Click Start, trigger via API, webhook, or set up a schedule.
Get results
Download as JSON, CSV, or Excel. Integrate with 1,000+ apps.
Use cases
Sales Teams
Build targeted lead lists with verified contact data.
Marketing
Research competitors and identify outreach opportunities.
Data Teams
Automate data collection pipelines with scheduled runs.
Developers
Integrate via REST API or use as an MCP tool in AI workflows.
Related actors
GitHub Repository Search
Search GitHub repositories by keyword, language, topic, stars, forks. Sort by stars, forks, or recently updated. Returns metadata, topics, license, owner info, URLs. Free API, optional token for higher limits.
Weather Forecast Search
Get weather forecasts for any location worldwide using the free Open-Meteo API. Returns current conditions, daily and hourly forecasts with temperature, precipitation, wind, UV index, and more. No API key needed.
EUIPO EU Trademark Search
Search EU trademarks via official EUIPO database. Find registered and pending trademarks by name, Nice class, applicant, or status. Returns full trademark details and filing history.
Nominatim Address Geocoder
Geocode addresses to GPS coordinates and reverse geocode coordinates to addresses using OpenStreetMap Nominatim. Batch geocoding with rate limiting. Free, no API key needed.
Ready to try Outreach Sequencer?
Start for free on Apify. No credit card required.
Open on Apify Store