# Cold Email Sequencer — SMTP, SendGrid & Mailgun (`ryanclinton/outreach-sequencer`) Actor

Send personalized cold email sequences via SMTP, SendGrid, or Mailgun. Automated day-3 and day-7 follow-ups. Sequence state persisted in KV Store across runs. CAN-SPAM compliant. $0.05/email.

- **URL**: https://apify.com/ryanclinton/outreach-sequencer.md
- **Developed by:** [Ryan Clinton](https://apify.com/ryanclinton) (community)
- **Categories:** Other
- **Stats:** 7 total users, 3 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $100.00 / 1,000 sequence createds

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are a software tools running on the Apify platform, for all kinds of web data extraction and automation use cases.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.
Actors are written with capital "A".

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.
The best way to integrate Actors is as follows.

In JavaScript/TypeScript projects, use official [JavaScript/TypeScript client](https://docs.apify.com/api/client/js.md):

```bash
npm install apify-client
```

In Python projects, use official [Python client library](https://docs.apify.com/api/client/python.md):

```bash
pip install apify-client
```

In shell scripts, use [Apify CLI](https://docs.apify.com/cli/docs.md):

````bash
# MacOS / Linux
curl -fsSL https://apify.com/install-cli.sh | bash
# Windows
irm https://apify.com/install-cli.ps1 | iex
```bash

In AI frameworks, you might use the [Apify MCP server](https://docs.apify.com/platform/integrations/mcp.md).

If your project is in a different language, use the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).


# README

## Outreach Sequencer — Multi-Provider Email Sequences

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 | `james.walker@pinnacletech.io` |
| 👤 **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; `advance` mode 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** — `start` mode sends step 1 to new leads; `advance` mode scans the KV store for sequences where `nextFollowUpAt <= 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 `sequenceId` generated 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 `step1SentAt` already set are skipped in `start` mode; unsubscribed or replied contacts are skipped in `advance` mode
- **Per-run rate limiting** — hard cap via `rateLimitPerRun` (max 10,000); excess leads get `status: deferred` in 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 `kvStoreName` per 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_KEY` environment 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](https://apify.com/ryanclinton/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](https://apify.com/ryanclinton/waterfall-contact-enrichment) or [Website Contact Scraper](https://apify.com/ryanclinton/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](https://apify.com/ryanclinton/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

1. **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.

2. **Paste your lead list** — In the `leads` field, provide an array of contacts. Each contact needs at minimum an `email` address. Add `firstName`, `companyName`, `servicesOffered`, and `summarySnippet` to enable personalization in your templates.

3. **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 in `followUpTemplate3` and `followUpTemplate7` for automated follow-ups.

4. **Run in `start` mode, then schedule `advance` mode** — Click "Start" with `mode: start` to send the initial emails. Set up a daily scheduled run with `mode: 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):**
```json
{
    "mode": "start",
    "emailProvider": "smtp",
    "smtpHost": "smtp-relay.brevo.com",
    "smtpPort": 587,
    "smtpUser": "yourlogin@example.com",
    "smtpPass": "your-smtp-password",
    "fromName": "Sarah at Pinnacle Digital",
    "fromEmail": "sarah@pinnacledigital.io",
    "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": "james.walker@acmecorp.com",
            "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": "linda.chen@betaindustries.com",
            "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:**

```json
{
    "mode": "start",
    "emailProvider": "sendgrid",
    "apiKey": "SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    "fromName": "Marcus at GrowthLab",
    "fromEmail": "marcus@growthlab.co",
    "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": "priya.nair@deltasystems.com",
            "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):**

```json
{
    "mode": "advance",
    "emailProvider": "smtp",
    "smtpHost": "smtp-relay.brevo.com",
    "smtpPort": 587,
    "smtpUser": "yourlogin@example.com",
    "smtpPass": "your-smtp-password",
    "fromName": "Sarah at Pinnacle Digital",
    "fromEmail": "sarah@pinnacledigital.io",
    "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: true` first** — 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 `kvStoreName` per 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 `advance` mode daily** — set up a recurring Apify schedule with `mode: advance` running 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 `rateLimitPerRun` below your provider's daily cap. Deferred leads remain in the dataset with `status: deferred`.
- **Leave `emailTemplate` blank in `advance` mode** — the step-1 template is not used when scanning for follow-ups. Only `followUpTemplate3` and `followUpTemplate7` are dispatched in advance mode.

### Output example

```json
{
    "email": "james.walker@acmecorp.com",
    "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:

```json
{
    "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

```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": "yourlogin@example.com",
    "smtpPass": "your-smtp-password",
    "fromName": "Sarah at Pinnacle Digital",
    "fromEmail": "sarah@pinnacledigital.io",
    "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": "james.walker@acmecorp.com",
            "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

```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: "marcus@growthlab.co",
    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: "priya.nair@deltasystems.com",
            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

```bash
## 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": "yourlogin@example.com",
    "smtpPass": "your-smtp-password",
    "fromName": "Sarah at Pinnacle Digital",
    "fromEmail": "sarah@pinnacledigital.io",
    "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": "james.walker@acmecorp.com","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

1. **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.

2. **Start with `rateLimitPerRun: 10` and `dryRun: true` first.** 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.

3. **Keep `summarySnippet` under 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.

4. **Match `rateLimitPerRun` to 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.

5. **Use a unique `kvStoreName` per 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.

6. **Combine with [Bulk Email Verifier](https://apify.com/ryanclinton/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.

7. **Set the advance-mode schedule to run at the same time each day.** The `dueStep()` check is a simple `nextFollowUpAt <= now()` comparison. Running the schedule at 9 AM daily means follow-ups send within 24 hours of becoming due.

8. **Mark replies and unsubscribes in the KV store promptly.** Use the Apify KV Store API to set `repliedAt` or `unsubscribedAt` on a contact's state record when you receive a reply or opt-out. The `advance` mode checks these flags before every dispatch.

### Combine with other Apify actors

| Actor | How to combine |
|---|---|
| [Website Contact Scraper](https://apify.com/ryanclinton/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](https://apify.com/ryanclinton/google-maps-email-extractor) | Extract local business contacts from Google Maps searches, then sequence them as a geographically targeted outreach campaign |
| [Bulk Email Verifier](https://apify.com/ryanclinton/bulk-email-verifier) | Verify emails via MX and SMTP checks before sending to avoid hard bounces that damage sender reputation |
| [Waterfall Contact Enrichment](https://apify.com/ryanclinton/waterfall-contact-enrichment) | Enrich a list of company names into full contact records (email, name, role) ready for the `leads` array |
| [B2B Lead Qualifier](https://apify.com/ryanclinton/b2b-lead-qualifier) | Score leads 0–100 from 30+ signals before sequencing — filter to high-scorers only for premium outreach batches |
| [HubSpot Lead Pusher](https://apify.com/ryanclinton/hubspot-lead-pusher) | Push output dataset records into HubSpot after each run to sync sequence activity against your CRM contacts |
| [Email Pattern Finder](https://apify.com/ryanclinton/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/plain` bodies, 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 `repliedAt` on 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 with `sequenceId` and `email` embedded, but you must build and host the opt-out endpoint yourself. Setting `unsubscribedAt` in 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 with `smtpHost: smtp.eu.mailgun.org` instead.
- **No built-in scheduling** — the actor does not self-schedule. You must create an Apify schedule manually to run `advance` mode 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 `start` mode multiple times in a day, each run has its own `rateLimitPerRun` cap. 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 `kvStoreName` values.

### Integrations

- [Zapier](https://apify.com/integrations/zapier) — trigger a Zap when a run completes to push `sent` records into a Google Sheet or create HubSpot deals for new sequences
- [Make](https://apify.com/integrations/make) — build a scenario that watches the output dataset and sets `repliedAt` in KV store when your inbox integration detects a reply
- [Google Sheets](https://apify.com/integrations/google-sheets) — export the output dataset after each run to maintain a live campaign tracker spreadsheet
- [Apify API](https://docs.apify.com/api/v2) — trigger `start` and `advance` runs programmatically from your CRM, marketing automation platform, or lead management system
- [Webhooks](https://docs.apify.com/platform/integrations/webhooks) — fire a webhook when each run finishes to notify your team in Slack or trigger downstream data pipeline steps
- [LangChain / LlamaIndex](https://docs.apify.com/platform/integrations) — generate personalized `summarySnippet` values per lead using an LLM agent, then pass the enriched lead array into this actor's input

### Troubleshooting

- **Emails show `status: failed` with SMTP credentials error** — confirm that `smtpHost`, `smtpUser`, and `smtpPass` are 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, use `apikey` as 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 `advance` mode** — confirm that `followUpTemplate3` or `followUpTemplate7` is 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 once `nextFollowUpAt <= now()`.

- **Contacts being skipped unexpectedly** — in `start` mode, contacts with an existing `step1SentAt` in 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's `servicesOffered` array. If `servicesOffered` is 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 `unsubscribedAt` in 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](https://blog.apify.com/is-web-scraping-legal/).

### 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](https://blog.apify.com/is-web-scraping-legal/) 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](https://docs.apify.com/platform/schedules) 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:

1. Go to [Account Settings > Privacy](https://console.apify.com/account/privacy)
2. 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](https://console.apify.com/actors/outreach-sequencer/issues) on this actor's page. For custom solutions or enterprise integrations, reach out through the Apify platform.

# Actor input Schema

## `mode` (type: `string`):

start — sends the first email to every lead in the leads array. advance — scans the KV store for sequences where nextFollowUpAt <= now() and sends the next follow-up.

## `leads` (type: `array`):

Array of lead objects. Required in start mode. Each lead: { email, firstName, companyName, servicesOffered\[], summarySnippet }.

## `emailProvider` (type: `string`):

smtp — works with any provider that supports SMTP. sendgrid — uses the SendGrid HTTP API v3 (requires apiKey). mailgun — uses the Mailgun Messages API (requires apiKey and mailgunDomain).

## `smtpHost` (type: `string`):

Required when emailProvider is smtp. Your provider's SMTP server hostname. Examples: smtp-relay.brevo.com, smtp.sendgrid.net, smtp.mailgun.org, email-smtp.us-east-1.amazonaws.com, smtp.gmail.com.

## `smtpPort` (type: `integer`):

Required when emailProvider is smtp. 587 for STARTTLS (recommended for most providers), 465 for SSL.

## `smtpUser` (type: `string`):

Required when emailProvider is smtp. Usually the login email address or username for your provider account.

## `smtpPass` (type: `string`):

Required when emailProvider is smtp. Your SMTP password or SMTP-specific API key. Stored encrypted.

## `apiKey` (type: `string`):

Required when emailProvider is sendgrid or mailgun. SendGrid: API key with 'Mail Send' permission. Mailgun: private API key starting with 'key-'. Stored encrypted.

## `mailgunDomain` (type: `string`):

Required when emailProvider is mailgun. Your verified Mailgun sending domain (e.g. mail.yourdomain.com). Must be verified in your Mailgun account.

## `fromName` (type: `string`):

The name that appears in the From field of every email. Use a real, recognizable name — e.g. 'Jane at Acme Corp'. Required.

## `fromEmail` (type: `string`):

The email address all emails are sent from. Must be verified and authorized on your email provider. SPF, DKIM, and DMARC records must be configured for good deliverability. Required.

## `unsubscribeUrl` (type: `string`):

Your unsubscribe endpoint URL. The actor substitutes {{sequenceId}} and {{email}} before inserting as {{unsubscribeLink}} in every template. Example: https://yourapp.com/unsubscribe?sid={{sequenceId}}\&e={{email}}. Required — CAN-SPAM mandates a working opt-out in every commercial email. You own this endpoint and your compliance.

## `rateLimitPerRun` (type: `integer`):

Hard cap on emails sent per actor run. Set conservatively based on your provider's daily limit: Brevo free = 300/day, SendGrid free = 100/day, AWS SES sandbox = 200/day, Mailgun = account-dependent. Leads that hit the cap get status: deferred. Required.

## `emailTemplate` (type: `string`):

Body template for the initial email. Available variables: {{firstName}}, {{companyName}}, {{topService}}, {{summarySnippet}}, {{unsubscribeLink}}. Plain text only. Required.

## `subjectLine` (type: `string`):

Subject line for the initial email. Supports {{companyName}} and {{firstName}}. Required.

## `followUpTemplate3` (type: `string`):

Body template for the day-3 follow-up email. Same variables available. Leave blank to disable day-3 follow-up.

## `followUpTemplate7` (type: `string`):

Body template for the day-7 follow-up email. Same variables available. Leave blank to disable day-7 follow-up.

## `followUpSubject3` (type: `string`):

Subject line for the day-3 follow-up. Leave blank to re-use step 1 subject prefixed with 'Re: '.

## `followUpSubject7` (type: `string`):

Subject line for the day-7 follow-up. Leave blank to re-use step 1 subject prefixed with 'Re: '.

## `kvStoreName` (type: `string`):

Name of the Apify Key-Value Store used to persist sequence state across runs. Each distinct campaign should use a unique name so sequences don't collide.

## `dryRun` (type: `boolean`):

When true, templates are rendered and logged but no emails are dispatched and no charges are made. Use to verify template output before going live.

## Actor input object example

```json
{
  "mode": "start",
  "leads": [
    {
      "email": "jane@acmecorp.com",
      "firstName": "Jane",
      "companyName": "Acme Corp",
      "servicesOffered": [
        "SEO",
        "PPC"
      ],
      "summarySnippet": "Full-service digital marketing agency specializing in performance campaigns for e-commerce brands."
    }
  ],
  "emailProvider": "smtp",
  "smtpHost": "smtp-relay.brevo.com",
  "smtpPort": 587,
  "smtpUser": "yourlogin@example.com",
  "smtpPass": "your-smtp-password",
  "apiKey": "SG.xxxxxxxxxxxxxxxxxxxx",
  "mailgunDomain": "mail.yourdomain.com",
  "fromName": "Jane at Acme Corp",
  "fromEmail": "jane@acmecorp.com",
  "unsubscribeUrl": "https://yourapp.com/unsubscribe?sid={{sequenceId}}&e={{email}}",
  "rateLimitPerRun": 100,
  "emailTemplate": "Hi {{firstName}},\n\nI came across {{companyName}} and was impressed by your work in {{topService}}.\n\n{{summarySnippet}}\n\nI'd love to explore how we might work together. Would you be open to a quick 15-minute call this week?\n\nBest,\n[Your name]\n\nTo opt out: {{unsubscribeLink}}",
  "subjectLine": "Quick question for {{companyName}}",
  "followUpTemplate3": "",
  "followUpTemplate7": "",
  "followUpSubject3": "",
  "followUpSubject7": "",
  "kvStoreName": "outreach-sequences",
  "dryRun": false
}
```

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "mode": "start",
    "leads": [
        {
            "email": "jane@acmecorp.com",
            "firstName": "Jane",
            "companyName": "Acme Corp",
            "servicesOffered": [
                "SEO",
                "PPC"
            ],
            "summarySnippet": "Full-service digital marketing agency specializing in performance campaigns for e-commerce brands."
        }
    ],
    "emailProvider": "smtp",
    "smtpHost": "smtp-relay.brevo.com",
    "smtpUser": "yourlogin@example.com",
    "smtpPass": "your-smtp-password",
    "apiKey": "SG.xxxxxxxxxxxxxxxxxxxx",
    "mailgunDomain": "mail.yourdomain.com",
    "fromName": "Jane at Acme Corp",
    "fromEmail": "jane@acmecorp.com",
    "unsubscribeUrl": "https://yourapp.com/unsubscribe?sid={{sequenceId}}&e={{email}}",
    "rateLimitPerRun": 100,
    "emailTemplate": `Hi {{firstName}},

I came across {{companyName}} and was impressed by your work in {{topService}}.

{{summarySnippet}}

I'd love to explore how we might work together. Would you be open to a quick 15-minute call this week?

Best,
[Your name]

To opt out: {{unsubscribeLink}}`,
    "subjectLine": "Quick question for {{companyName}}",
    "kvStoreName": "outreach-sequences"
};

// Run the Actor and wait for it to finish
const run = await client.actor("ryanclinton/outreach-sequencer").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "mode": "start",
    "leads": [{
            "email": "jane@acmecorp.com",
            "firstName": "Jane",
            "companyName": "Acme Corp",
            "servicesOffered": [
                "SEO",
                "PPC",
            ],
            "summarySnippet": "Full-service digital marketing agency specializing in performance campaigns for e-commerce brands.",
        }],
    "emailProvider": "smtp",
    "smtpHost": "smtp-relay.brevo.com",
    "smtpUser": "yourlogin@example.com",
    "smtpPass": "your-smtp-password",
    "apiKey": "SG.xxxxxxxxxxxxxxxxxxxx",
    "mailgunDomain": "mail.yourdomain.com",
    "fromName": "Jane at Acme Corp",
    "fromEmail": "jane@acmecorp.com",
    "unsubscribeUrl": "https://yourapp.com/unsubscribe?sid={{sequenceId}}&e={{email}}",
    "rateLimitPerRun": 100,
    "emailTemplate": """Hi {{firstName}},

I came across {{companyName}} and was impressed by your work in {{topService}}.

{{summarySnippet}}

I'd love to explore how we might work together. Would you be open to a quick 15-minute call this week?

Best,
[Your name]

To opt out: {{unsubscribeLink}}""",
    "subjectLine": "Quick question for {{companyName}}",
    "kvStoreName": "outreach-sequences",
}

# Run the Actor and wait for it to finish
run = client.actor("ryanclinton/outreach-sequencer").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print("💾 Check your data here: https://console.apify.com/storage/datasets/" + run["defaultDatasetId"])
for item in client.dataset(run["defaultDatasetId"]).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "mode": "start",
  "leads": [
    {
      "email": "jane@acmecorp.com",
      "firstName": "Jane",
      "companyName": "Acme Corp",
      "servicesOffered": [
        "SEO",
        "PPC"
      ],
      "summarySnippet": "Full-service digital marketing agency specializing in performance campaigns for e-commerce brands."
    }
  ],
  "emailProvider": "smtp",
  "smtpHost": "smtp-relay.brevo.com",
  "smtpUser": "yourlogin@example.com",
  "smtpPass": "your-smtp-password",
  "apiKey": "SG.xxxxxxxxxxxxxxxxxxxx",
  "mailgunDomain": "mail.yourdomain.com",
  "fromName": "Jane at Acme Corp",
  "fromEmail": "jane@acmecorp.com",
  "unsubscribeUrl": "https://yourapp.com/unsubscribe?sid={{sequenceId}}&e={{email}}",
  "rateLimitPerRun": 100,
  "emailTemplate": "Hi {{firstName}},\\n\\nI came across {{companyName}} and was impressed by your work in {{topService}}.\\n\\n{{summarySnippet}}\\n\\nI'\''d love to explore how we might work together. Would you be open to a quick 15-minute call this week?\\n\\nBest,\\n[Your name]\\n\\nTo opt out: {{unsubscribeLink}}",
  "subjectLine": "Quick question for {{companyName}}",
  "kvStoreName": "outreach-sequences"
}' |
apify call ryanclinton/outreach-sequencer --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "command": "npx",
            "args": [
                "mcp-remote",
                "https://mcp.apify.com/?tools=ryanclinton/outreach-sequencer",
                "--header",
                "Authorization: Bearer <YOUR_API_TOKEN>"
            ]
        }
    }
}

```

## OpenAPI specification

```json
{
    "openapi": "3.0.1",
    "info": {
        "title": "Cold Email Sequencer — SMTP, SendGrid & Mailgun",
        "description": "Send personalized cold email sequences via SMTP, SendGrid, or Mailgun. Automated day-3 and day-7 follow-ups. Sequence state persisted in KV Store across runs. CAN-SPAM compliant. $0.05/email.",
        "version": "1.0",
        "x-build-id": "VB6x1bdvST3J8sBtX"
    },
    "servers": [
        {
            "url": "https://api.apify.com/v2"
        }
    ],
    "paths": {
        "/acts/ryanclinton~outreach-sequencer/run-sync-get-dataset-items": {
            "post": {
                "operationId": "run-sync-get-dataset-items-ryanclinton-outreach-sequencer",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for its completion, and returns Actor's dataset items in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        },
        "/acts/ryanclinton~outreach-sequencer/runs": {
            "post": {
                "operationId": "runs-sync-ryanclinton-outreach-sequencer",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor and returns information about the initiated run in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK",
                        "content": {
                            "application/json": {
                                "schema": {
                                    "$ref": "#/components/schemas/runsResponseSchema"
                                }
                            }
                        }
                    }
                }
            }
        },
        "/acts/ryanclinton~outreach-sequencer/run-sync": {
            "post": {
                "operationId": "run-sync-ryanclinton-outreach-sequencer",
                "x-openai-isConsequential": false,
                "summary": "Executes an Actor, waits for completion, and returns the OUTPUT from Key-value store in response.",
                "tags": [
                    "Run Actor"
                ],
                "requestBody": {
                    "required": true,
                    "content": {
                        "application/json": {
                            "schema": {
                                "$ref": "#/components/schemas/inputSchema"
                            }
                        }
                    }
                },
                "parameters": [
                    {
                        "name": "token",
                        "in": "query",
                        "required": true,
                        "schema": {
                            "type": "string"
                        },
                        "description": "Enter your Apify token here"
                    }
                ],
                "responses": {
                    "200": {
                        "description": "OK"
                    }
                }
            }
        }
    },
    "components": {
        "schemas": {
            "inputSchema": {
                "type": "object",
                "required": [
                    "mode",
                    "emailProvider",
                    "fromName",
                    "fromEmail",
                    "unsubscribeUrl",
                    "emailTemplate",
                    "subjectLine",
                    "rateLimitPerRun"
                ],
                "properties": {
                    "mode": {
                        "title": "Mode",
                        "enum": [
                            "start",
                            "advance"
                        ],
                        "type": "string",
                        "description": "start — sends the first email to every lead in the leads array. advance — scans the KV store for sequences where nextFollowUpAt <= now() and sends the next follow-up.",
                        "default": "start"
                    },
                    "leads": {
                        "title": "Enriched lead list",
                        "type": "array",
                        "description": "Array of lead objects. Required in start mode. Each lead: { email, firstName, companyName, servicesOffered[], summarySnippet }.",
                        "default": []
                    },
                    "emailProvider": {
                        "title": "Email provider",
                        "enum": [
                            "smtp",
                            "sendgrid",
                            "mailgun"
                        ],
                        "type": "string",
                        "description": "smtp — works with any provider that supports SMTP. sendgrid — uses the SendGrid HTTP API v3 (requires apiKey). mailgun — uses the Mailgun Messages API (requires apiKey and mailgunDomain).",
                        "default": "smtp"
                    },
                    "smtpHost": {
                        "title": "SMTP host",
                        "type": "string",
                        "description": "Required when emailProvider is smtp. Your provider's SMTP server hostname. Examples: smtp-relay.brevo.com, smtp.sendgrid.net, smtp.mailgun.org, email-smtp.us-east-1.amazonaws.com, smtp.gmail.com."
                    },
                    "smtpPort": {
                        "title": "SMTP port",
                        "minimum": 1,
                        "maximum": 65535,
                        "type": "integer",
                        "description": "Required when emailProvider is smtp. 587 for STARTTLS (recommended for most providers), 465 for SSL.",
                        "default": 587
                    },
                    "smtpUser": {
                        "title": "SMTP username",
                        "type": "string",
                        "description": "Required when emailProvider is smtp. Usually the login email address or username for your provider account."
                    },
                    "smtpPass": {
                        "title": "SMTP password or API key",
                        "type": "string",
                        "description": "Required when emailProvider is smtp. Your SMTP password or SMTP-specific API key. Stored encrypted."
                    },
                    "apiKey": {
                        "title": "REST API key",
                        "type": "string",
                        "description": "Required when emailProvider is sendgrid or mailgun. SendGrid: API key with 'Mail Send' permission. Mailgun: private API key starting with 'key-'. Stored encrypted."
                    },
                    "mailgunDomain": {
                        "title": "Mailgun sending domain",
                        "type": "string",
                        "description": "Required when emailProvider is mailgun. Your verified Mailgun sending domain (e.g. mail.yourdomain.com). Must be verified in your Mailgun account."
                    },
                    "fromName": {
                        "title": "Sender display name",
                        "type": "string",
                        "description": "The name that appears in the From field of every email. Use a real, recognizable name — e.g. 'Jane at Acme Corp'. Required."
                    },
                    "fromEmail": {
                        "title": "Sender email address",
                        "type": "string",
                        "description": "The email address all emails are sent from. Must be verified and authorized on your email provider. SPF, DKIM, and DMARC records must be configured for good deliverability. Required."
                    },
                    "unsubscribeUrl": {
                        "title": "Unsubscribe URL",
                        "type": "string",
                        "description": "Your unsubscribe endpoint URL. The actor substitutes {{sequenceId}} and {{email}} before inserting as {{unsubscribeLink}} in every template. Example: https://yourapp.com/unsubscribe?sid={{sequenceId}}&e={{email}}. Required — CAN-SPAM mandates a working opt-out in every commercial email. You own this endpoint and your compliance."
                    },
                    "rateLimitPerRun": {
                        "title": "Max emails per run",
                        "minimum": 1,
                        "maximum": 10000,
                        "type": "integer",
                        "description": "Hard cap on emails sent per actor run. Set conservatively based on your provider's daily limit: Brevo free = 300/day, SendGrid free = 100/day, AWS SES sandbox = 200/day, Mailgun = account-dependent. Leads that hit the cap get status: deferred. Required.",
                        "default": 100
                    },
                    "emailTemplate": {
                        "title": "Email body template (step 1)",
                        "type": "string",
                        "description": "Body template for the initial email. Available variables: {{firstName}}, {{companyName}}, {{topService}}, {{summarySnippet}}, {{unsubscribeLink}}. Plain text only. Required.",
                        "default": "Hi {{firstName}},\n\nI came across {{companyName}} and was impressed by your work in {{topService}}.\n\n{{summarySnippet}}\n\nI'd love to explore how we might work together. Would you be open to a quick 15-minute call this week?\n\nBest,\n[Your name]\n\nTo opt out: {{unsubscribeLink}}"
                    },
                    "subjectLine": {
                        "title": "Email subject (step 1)",
                        "type": "string",
                        "description": "Subject line for the initial email. Supports {{companyName}} and {{firstName}}. Required.",
                        "default": "Quick question for {{companyName}}"
                    },
                    "followUpTemplate3": {
                        "title": "Follow-up template (day 3)",
                        "type": "string",
                        "description": "Body template for the day-3 follow-up email. Same variables available. Leave blank to disable day-3 follow-up.",
                        "default": ""
                    },
                    "followUpTemplate7": {
                        "title": "Follow-up template (day 7)",
                        "type": "string",
                        "description": "Body template for the day-7 follow-up email. Same variables available. Leave blank to disable day-7 follow-up.",
                        "default": ""
                    },
                    "followUpSubject3": {
                        "title": "Follow-up subject (day 3)",
                        "type": "string",
                        "description": "Subject line for the day-3 follow-up. Leave blank to re-use step 1 subject prefixed with 'Re: '.",
                        "default": ""
                    },
                    "followUpSubject7": {
                        "title": "Follow-up subject (day 7)",
                        "type": "string",
                        "description": "Subject line for the day-7 follow-up. Leave blank to re-use step 1 subject prefixed with 'Re: '.",
                        "default": ""
                    },
                    "kvStoreName": {
                        "title": "KV store name for sequence state",
                        "type": "string",
                        "description": "Name of the Apify Key-Value Store used to persist sequence state across runs. Each distinct campaign should use a unique name so sequences don't collide.",
                        "default": "outreach-sequences"
                    },
                    "dryRun": {
                        "title": "Dry run (no emails sent)",
                        "type": "boolean",
                        "description": "When true, templates are rendered and logged but no emails are dispatched and no charges are made. Use to verify template output before going live.",
                        "default": false
                    }
                }
            },
            "runsResponseSchema": {
                "type": "object",
                "properties": {
                    "data": {
                        "type": "object",
                        "properties": {
                            "id": {
                                "type": "string"
                            },
                            "actId": {
                                "type": "string"
                            },
                            "userId": {
                                "type": "string"
                            },
                            "startedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "finishedAt": {
                                "type": "string",
                                "format": "date-time",
                                "example": "2025-01-08T00:00:00.000Z"
                            },
                            "status": {
                                "type": "string",
                                "example": "READY"
                            },
                            "meta": {
                                "type": "object",
                                "properties": {
                                    "origin": {
                                        "type": "string",
                                        "example": "API"
                                    },
                                    "userAgent": {
                                        "type": "string"
                                    }
                                }
                            },
                            "stats": {
                                "type": "object",
                                "properties": {
                                    "inputBodyLen": {
                                        "type": "integer",
                                        "example": 2000
                                    },
                                    "rebootCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "restartCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "resurrectCount": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "computeUnits": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "options": {
                                "type": "object",
                                "properties": {
                                    "build": {
                                        "type": "string",
                                        "example": "latest"
                                    },
                                    "timeoutSecs": {
                                        "type": "integer",
                                        "example": 300
                                    },
                                    "memoryMbytes": {
                                        "type": "integer",
                                        "example": 1024
                                    },
                                    "diskMbytes": {
                                        "type": "integer",
                                        "example": 2048
                                    }
                                }
                            },
                            "buildId": {
                                "type": "string"
                            },
                            "defaultKeyValueStoreId": {
                                "type": "string"
                            },
                            "defaultDatasetId": {
                                "type": "string"
                            },
                            "defaultRequestQueueId": {
                                "type": "string"
                            },
                            "buildNumber": {
                                "type": "string",
                                "example": "1.0.0"
                            },
                            "containerUrl": {
                                "type": "string"
                            },
                            "usage": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "integer",
                                        "example": 1
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            },
                            "usageTotalUsd": {
                                "type": "number",
                                "example": 0.00005
                            },
                            "usageUsd": {
                                "type": "object",
                                "properties": {
                                    "ACTOR_COMPUTE_UNITS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATASET_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "KEY_VALUE_STORE_WRITES": {
                                        "type": "number",
                                        "example": 0.00005
                                    },
                                    "KEY_VALUE_STORE_LISTS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_READS": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "REQUEST_QUEUE_WRITES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_INTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "DATA_TRANSFER_EXTERNAL_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_RESIDENTIAL_TRANSFER_GBYTES": {
                                        "type": "integer",
                                        "example": 0
                                    },
                                    "PROXY_SERPS": {
                                        "type": "integer",
                                        "example": 0
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}
```
