Your stack

Run a live Apollo-to-Instantly pipeline on your own keys, climbing simulate, plan, and dry-run before one armed run that sends

What you'll learn

You have Apollo, Harvest, Anthropic, and Instantly, and you want a live campaign with nothing sent until you've read the dry run.

  • climb simulate, plan, dry-run, armed, with a spend sentence before each
  • what preflight checks on the Instantly campaign
  • why the armed run can differ from the dry run, and how to pin it

This needs Apollo, Harvest, Anthropic, and Instantly keys, plus an Instantly campaign that exists and is active.

Nothing spends until the dry run, and only the armed run sends. The dry run spends Apollo reveal credits, Harvest calls, and model tokens. Sending means adding people to your live Instantly campaign, which emails them on its own schedule.

This is See it run on live vendors. You'll climb the gate ladder in order: simulate, plan, dry-run, armed.

The file you'll run

Fetch the pipeline, and install its vendor adapters from the registry:

curl -fsSLO https://raw.githubusercontent.com/gtme-run/gtme/main/examples/apollo-to-instantly.yaml
gtme adapters add apollo/search apollo/enrich \
  harvest/profile harvest/recent-posts instantly/add-to-campaign

Here it is without comments:

The lines you'll change:

  • Who it searches for is query:, titles:, and employee_ranges:. Apollo search is free (ADR-043).
  • How many people it pulls is limit:.
  • Who passes the filter is the template: under icp-filter. Only they cost a reveal.
  • What the lines say is the template: under personalize.

We recommend limit: 5 for the first dry run, raised after the receipt reads right:

    limit: 5

Without Harvest, delete the linkedin and posts steps and point personalize at Apollo's fields, or plan fails:

    uses: [first_name, title, company_name]
    with:
      template: >
        Write first_line and ps_line using title and company_name.

With Harvest, someone it finds nothing for gets lines written without posts.

Simulate it

Simulate spends nothing and sends nothing:

gtme run apollo-to-instantly.yaml --simulate

The output is similar to the following:

simulate: ignoring missing credentials (5 plan problems:
...
simulate: recorded responses only — no network, no spend, nothing sends, nothing persists
run 01M3K1VE3JW9NW8HAGD7XY1NMA (apollo-to-instantly)
...
send: 2 in, 0 out, 0 cached, 0 filtered, 0 failed, 2 held (dry run)

run 01M3K1VE3JW9NW8HAGD7XY1NMA — done (SIMULATED — recorded responses only; nothing sent, nothing persisted)
step         adapter                    in  out  empty  cached  filtered  failed  cost     avoided
source       apollo/search              0   2    -      0       -         -       $0       -
icp-filter   ai/filter                  2   2    -      0       -         -       $0       -
reveal       apollo/enrich              2   2    -      0       -         -       $0.0200  -
linkedin     harvest/profile            2   2    -      0       -         -       $0.0240  -
posts        harvest/recent-posts       2   2    -      0       -         -       $0.0240  -
personalize  ai/compose                 2   2    -      0       -         -       $0       -
send         instantly/add-to-campaign  2   0    -      0       -         -       $0       -
send: preflight skipped — the target is not read under --simulate; --dry-run checks it
send: resolved variables for 2 record(s) — review, then run again without --dry-run to arm:
  nh:ed48d7bfcbdb5e0ce36005a8cec0be0812e91c1f4a4f542bc86eed418173f417
    first_line: "Fixture first line for nh:ed48d7bfcbdb5e0ce36005a8cec0be0812e91c1f4a4f542bc86eed418173f417"
    ps_line: "Fixture ps line for nh:ed48d7bfcbdb5e0ce36005a8cec0be0812e91c1f4a4f542bc86eed418173f417"
...
total: $0.0680 (estimated) spent

Apollo and Harvest answered from fixtures (recorded responses), the model from canned text; $0.0680 is what the reveals and the Harvest calls would cost. "held (dry run)" and "without --dry-run to arm" are the CLI's words under simulate too, not a third mode.

The send: resolved variables block, each person's values for the email, is the part to read. The dry run prints the same block with real people.

Plan it

Plan spends nothing and sends nothing:

gtme plan apollo-to-instantly.yaml

Without keys, the output is the following:

gtme: 5 plan problems:
  - step "source": missing credential APOLLO_API_KEY (set it in the environment or run `gtme secret set APOLLO_API_KEY`)
  - step "reveal": missing credential APOLLO_API_KEY (set it in the environment or run `gtme secret set APOLLO_API_KEY`)
  - step "linkedin": missing credential HARVEST_API_KEY (set it in the environment or run `gtme secret set HARVEST_API_KEY`)
  - step "posts": missing credential HARVEST_API_KEY (set it in the environment or run `gtme secret set HARVEST_API_KEY`)
  - step "send": missing credential INSTANTLY_API_KEY (set it in the environment or run `gtme secret set INSTANTLY_API_KEY`)

Plan exits with code 3, which means every problem is a missing credential, and each line names its fix. Plan only warns about the Anthropic key; without it, every record fails at icp-filter and the receipt names the key.

Each command prompts for a key and saves it to ~/.gtme/secrets, a home-directory file only you can read:

gtme secret set APOLLO_API_KEY
gtme secret set HARVEST_API_KEY
gtme secret set ANTHROPIC_API_KEY
gtme secret set INSTANTLY_API_KEY

Run plan again to see each step's estimated cost.

Dry-run it

Set campaign: to an active Instantly campaign's id, from its URL. Before any record moves, the runner reads the campaign and stops if its sequence emails, A/B variants included, lack {{first_line}} and {{ps_line}} (ADR-040). Pausing the campaign in Instantly stops its emails.

The dry run spends Apollo reveal credits, Harvest calls, and model tokens. It sends nothing:

gtme run apollo-to-instantly.yaml --dry-run
dry run: deliver steps will resolve and receipt their variables, but nothing sends
run 01M3D1ZTKBQBS57C9WPK8NQM60 (apollo-to-instantly)
source [info]: apollo/search: 5 records
...
send: preflight ok — campaign "Q3 VP Marketing" (0198a0b1-2c3d-4e5f-8a9b-0c1d2e3f4a5b) — 3 check(s)
send: 5 in, 0 out, 0 cached, 0 filtered, 0 failed, 5 held (dry run)

run 01M3D1ZTKBQBS57C9WPK8NQM60 — done (dry run — nothing sent)
step         adapter                    in  out  empty  cached  filtered  failed  cost     avoided
source       apollo/search              0   5    -      0       -         -       $0       -
icp-filter   ai/filter                  5   5    -      0       -         -       $0.0105  -
reveal       apollo/enrich              5   5    -      0       -         -       $0.0500  -
linkedin     harvest/profile            5   5    -      0       -         -       $0.0600  -
personalize  ai/compose                 5   5    -      0       -         -       $0.0232  -
send         instantly/add-to-campaign  5   0    -      0       -         -       $0       -
send: preflight ok — campaign "Q3 VP Marketing" (0198a0b1-2c3d-4e5f-8a9b-0c1d2e3f4a5b) — 3 check(s) (✓ campaign active, ✓ variable first_line referenced, ✓ variable ps_line referenced)
send: resolved variables for 5 record(s) — review, then run again without --dry-run to arm:
  nh:e9de8aa74c349b813529a458a46fee98774debfb75e44f3c4dc2fd1f50ef3f92
    first_line: "Your note about using pen and paper to reprioritize when juggling multiple projects resonated — it's a simple habit that doesn't get mentioned enough."
    ps_line: "Happy to connect if you're open to it."
  ...
total: $0.1437 (estimated) spent

Check that the campaign checks passed, the count matches limit, and every first_line reads like something you'd send.

Arm it

The armed run searches Apollo again, so it can include people the dry run didn't. People already judged reuse their verdicts, reveals, and lines from the ledger; anyone new is judged, revealed, and sent unread. Arm right after reading the dry run. To pin the list exactly, qualify into a group and send from it.

The armed run spends only on people the ledger lacks, and it sends:

gtme run apollo-to-instantly.yaml
run 01M3D217B7R83KF1HCM4W3ENKJ (apollo-to-instantly)
...
send [info]: instantly: added 2 leads
send: 5 in, 5 out, 0 cached, 0 filtered, 0 failed

run 01M3D217B7R83KF1HCM4W3ENKJ — done
step         adapter                    in  out  empty  cached  filtered  failed  cost  avoided
source       apollo/search              0   5    -      0       -         -       $0    -
icp-filter   ai/filter                  5   0    -      5       -         -       $0    ?
reveal       apollo/enrich              5   0    -      5       -         -       $0    $0.0500
linkedin     harvest/profile            5   0    -      5       -         -       $0    $0.0600
personalize  ai/compose                 5   0    -      5       -         -       $0    ?
send         instantly/add-to-campaign  5   5    -      0       -         -       $0    -
send: preflight ok — campaign "Q3 VP Marketing" (0198a0b1-2c3d-4e5f-8a9b-0c1d2e3f4a5b) — 3 check(s) (✓ campaign active, ✓ variable first_line referenced, ✓ variable ps_line referenced)
send: attested 5 confirmed, 0 contradicted, 0 inconclusive (deliveries are accepted, never sent, until a provider attests)
total: $0 (estimated) spent, $0.1100+? avoided via cache (20 records skipped)

Everything the dry run paid for came back from the ledger; the only new work was five adds. Delivery is keyed on email, so a second run adds nobody twice:

gtme run apollo-to-instantly.yaml
run 01M3D219H50J12GF22T56NES50 — done
step         adapter                    in  out  empty  cached  filtered  failed  cost  avoided
source       apollo/search              0   5    -      0       -         -       $0    -
icp-filter   ai/filter                  5   0    -      5       -         -       $0    ?
reveal       apollo/enrich              5   0    -      5       -         -       $0    $0.0500
linkedin     harvest/profile            5   0    -      5       -         -       $0    $0.0600
personalize  ai/compose                 5   0    -      5       -         -       $0    ?
send         instantly/add-to-campaign  5   0    -      0       -         -       $0    -
send: 5 already delivered
total: $0 (estimated) spent, $0.1100+? avoided via cache (20 records skipped)

cached on every paid step, savings under avoided, and send: 5 already delivered. The ? is the model's metered price, which the receipt can't restate here.

Next

Connect your stack covers the same climb for any pipeline. Add a vendor covers a vendor gtme doesn't ship. For Claude Code, paste this line; the agent stops at the dry-run receipt and never arms:

Follow gtme.run/start.md, "Your stack", with apollo-to-instantly.yaml at limit: 5