Connect your stack: secrets, plan, dry-run, arm
Take a pipeline file that names your vendors, install what it needs, store each key, and climb from plan to an armed run without a surprise
What you'll learn
You have a pipeline file that names your vendors, and you want it running on your own keys with nothing spent or sent until you've read what it will do.
- how plan's errors become the checklist of adapters to install and keys to store
- where a stored key lives and which copy wins when there are two
- what to read at simulate and dry-run before you arm
Goal: your pipeline runs armed on your own vendor keys, after every cheaper rung of the gate ladder comes back clean. The rungs are plan, simulate, dry-run, and the armed run, the one with no flag that spends and sends.
Before you start
You need gtme (Install) and one key per vendor the file names. Plan names each key except the model's. For the file in the next section, that's a HubSpot private-app token that can read contacts, an Instantly API key, and an Anthropic API key. gtme secret set keeps them in ~/.gtme/secrets, whatever GTME_LEDGER points at.
You need an Instantly campaign, active, whose emails use the two lines this file writes. Put {{first_line}} and {{ps_line}} in every email of its sequence, A/B variants included. An active campaign emails anyone added to it on its schedule, and pausing it in Instantly stops its emails.
Steps 1 through 7 spend nothing and send nothing. Step 8, the dry-run, spends model tokens on five records and sends nothing. Step 9, the armed run, spends only on what the ledger doesn't already have, and it sends. This page shows real output for steps 1 through 7. Steps 8 and 9 call HubSpot and Instantly for real, so for their output it points to Your stack, which ran the same send step.
The file
This pipeline sources contacts from HubSpot, writes two lines for each with a model, and adds them to an Instantly campaign. Save it as hubspot-to-instantly.yaml:
hubspot/contact-search takes contacts that have known_property set, oldest HubSpot record first, and limit: 5 stops at five. Those can be customers. To send to prospects only, add a filter step on hubspot.lifecycle_stage, which the source provides, before you arm.
Steps
-
Plan the file before you install anything. Plan reads the file and the adapters you have installed, with no network:
gtme plan hubspot-to-instantly.yamlThe output is similar to the following:
gtme: 2 plan problems: - step "source": adapters: unknown adapter "hubspot/contact-search" — if it is a registry entry, install it: gtme adapters add hubspot/contact-search built-in: agent/compose, agent/filter, agent/review, ai/compose, ai/filter, ai/review, csv/deliver, csv/source, demo/enrich, http/deliver, http/enrich, human/compose, human/filter, human/review, text/compose ... - step "send": adapters: unknown adapter "instantly/add-to-campaign" — if it is a registry entry, install it: gtme adapters add instantly/add-to-campaign ...The problem is in the file itself, not a missing key. The
built-inline is every adapter this binary ships, and no vendor is on it. Neitherhubspot/contact-searchnorinstantly/add-to-campaignis one, so plan prints the command that installs each from the registry. Plan can't check which fields a missing adapter provides, so it reports nothing about the steps after one until you install it. -
Find the missing adapter in the registry. Searching reads GitHub and spends nothing:
gtme adapters search hubspotThe output is similar to the following:
ID ROLE TIER INSTALL DESCRIPTION hubspot/contact-search source verified gtme adapters add hubspot/contact-search Source HubSpot contacts via the CRM v3 Search API, filtered…Install both by ID, the commands plan printed:
gtme adapters add hubspot/contact-search instantly/add-to-campaignThe output is similar to the following:
fetched github.com/gtme-run/gtme-bindings/hubspot-contact-search@bcf671b9e1df177ed24e98b58e0c132176261c78 at bcf671b9e1df hubspot/contact-search v1 — source (person) calls: api.hubapi.com demands: HUBSPOT_ACCESS_TOKEN needs: none provides: company_name, email, first_name, hubspot.contact_id, hubspot.lifecycle_stage, last_name, title fixtures: ok — 1 response(s) on file, 2 record(s) extracted ...hubspot/contact-searchis a binding, a vendor adapter written as YAML.instantly/add-to-campaignprints the same surface as a process adapter, built for your platform and pinned to a gtme release (the decision record, ADR-063).calls:lists every host it reaches, anddemands:is the key it needs. The install is pinned to commitbcf671b9e1df, the one the registry's index names (the decision record, ADR-042). TheINSTALLcolumn's longer reference also works, and pins whatever commitmainpoints to. If search finds nothing, Add a vendor shows how to write the binding. -
Plan again until only keys are left:
gtme plan hubspot-to-instantly.yamlIt prints:
gtme: 2 plan problems: - step "source": missing credential HUBSPOT_ACCESS_TOKEN (set it in the environment or run `gtme secret set HUBSPOT_ACCESS_TOKEN`) - step "send": missing credential INSTANTLY_API_KEY (set it in the environment or run `gtme secret set INSTANTLY_API_KEY`)Exit code
3means every remaining problem is a missing credential. This list is your key checklist. -
Store each key. Each command prompts for the value, doesn't echo it, and saves it to
~/.gtme/secrets, a file only your user can read:gtme secret set HUBSPOT_ACCESS_TOKEN gtme secret set INSTANTLY_API_KEY gtme secret set ANTHROPIC_API_KEYPlan didn't list
ANTHROPIC_API_KEY, because the AI adapters mark it optional and plan only warns. Without it,linesfails at run time, so store it now.The runner looks in your environment first and the secrets file second (SPEC §6). A key exported in your shell wins over the stored one. To rotate a key, run
gtme secret setagain, which replaces the line, and unset any exported copy. -
Check what's stored. This prints names, never values:
gtme secret listIt prints:
ANTHROPIC_API_KEY HUBSPOT_ACCESS_TOKEN INSTANTLY_API_KEY -
Plan until it's clean:
gtme plan hubspot-to-instantly.yamlThe output is similar to the following:
pipeline hubspot-to-instantly (version 1) 1. source [source] — hubspot/contact-search@1 ... creds: HUBSPOT_ACCESS_TOKEN (resolved) est/record: $0.0000 2. lines [compose] — ai/compose@1 ... warning: optional credential ANTHROPIC_WORKSPACE_ID is not set; this step will fail at run time if it needs it est/record: ? 3. send [deliver] — instantly/add-to-campaign@2 ... creds: INSTANTLY_API_KEY (resolved) est/record: $0.0000 send surface: 1 deliver step(s) send → instantly/add-to-campaign (touch scope: hubspot-to-instantly) ... plan ok — nothing has been spentEvery
creds:line saysresolved. Ignore theANTHROPIC_WORKSPACE_IDwarning unless a run fails on it.est/record: ?means the model is metered, so the dry-run receipt is where you see its cost.send surfacelists every step that sends when you arm. -
Simulate it. Simulate runs offline from each adapter's recorded responses:
gtme run hubspot-to-instantly.yaml --simulateThe output is similar to the following:
simulate: recorded responses only — no network, no spend, nothing sends, nothing persists ... run 01M3K16V5G360T18EQBN9FDYPW — done (SIMULATED — recorded responses only; nothing sent, nothing persisted) step adapter in out empty cached filtered failed cost avoided source hubspot/contact-search 0 2 - 0 - - $0 - lines 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: ada.quill@example.test first_line: "Fixture first line for ada.quill@example.test" ps_line: "Fixture ps line for ada.quill@example.test" ... total: $0 (estimated) spentRead two things. There's no
simulation gap:line, so every adapter had recorded responses to serve; a gap line names an adapter simulate couldn't vouch for. Andsend: resolved variableslists the fields thesendstep hands the campaign, herefirst_lineandps_line. The last line's "run again without --dry-run to arm" is dry-run's wording. After simulate, the dry-run is next, not the armed run. -
Dry-run it. In
hubspot-to-instantly.yaml, keeplimit: 5and setcampaign:to your campaign's id, from its URL in Instantly. The dry-run spends model tokens and sends nothing:gtme run hubspot-to-instantly.yaml --dry-runBefore any record moves, preflight reads the campaign, prints its name, and reports whether it's active and every email uses both variables (the decision record, ADR-040). For a campaign named Q4 CRM follow-up, the receipt's line reads:
send: preflight ok — campaign "Q4 CRM follow-up" (0198a0b1-7e6d-4c5b-9a8f-1e2d3c4b5a69) — 3 check(s) (✓ campaign active, ✓ variable first_line referenced, ✓ variable ps_line referenced)A failed check names the fix: activate the campaign, or add the missing variable to every email. An email that uses one of Instantly's lead tags, such as
{{firstName}}, needs a field mapped to it too, asfirstName: first_name, or preflight blocks. Then read everyfirst_lineandps_linein the resolved variables, the exact text the armed run sends. -
Arm it. With
limit: 5still set, this adds up to five people, and Instantly emails them on the campaign's schedule. A blocked preflight stops the step here, before anyone is added:gtme run hubspot-to-instantly.yamlOn the receipt,
linesshows the dry-run's lines ascached, so they aren't paid for twice, andsendshows how many people it added. The source searches HubSpot again, so a contact added after the dry-run is composed and sent without your read. Groups pin the exact list you dry-ran. Running the file again adds no one twice, becauseidempotency: emailskips anyone already added.
What you have now
A pipeline on your own keys that has sent once, with its adapter pinned. gtme runs lists the armed run as done, not done (dry), and the people appear in the campaign's leads in Instantly. Check the install and its pin:
gtme adapters
The output is the following:
ID VERSION ROLE KIND SOURCE
hubspot/contact-search 1 source binding github.com/gtme-run/gtme-bindings/hubspot-contact-search@bcf671b9e1df177ed24e98b58e0c132176261c78 (bcf671b9e1df)
The hash in parentheses is the pin, the short form of the commit in SOURCE. gtme secret list and gtme plan recheck the keys. To have Claude Code do steps 1 through 7 and stop there, paste this line. The agent leaves each gtme secret set for you to run (For agents):
Connect hubspot-to-instantly.yaml: plan it, install what plan names, list the keys for me to set, then simulate and stop before the dry-run
Next
- Runs and receipts reads every column of the dry-run receipt.
gtme adaptersverifies an installed binding and moves its pin withupdate.- A campaign is a folder packages the connected pipeline with its pins to share or rerun.