ACTION_ID: instantly_get_lead NAME: Instantly: Get Lead Data CATEGORY: Outreach CREDITS: 0 Look up one email address in the connected Instantly workspace and return that lead's record — most importantly WHEN THEY WERE LAST EMAILED. The read half of Instantly: pair it with a `formula` to suppress people a campaign already touched recently, before `instantly_add_to_campaign` enrolls them again. PREREQUISITE: Instantly account connection — the user must create it in the Floqer UI (Connections page); an agent cannot. Without it the action fails the row with a connection / authentication error. Confirm it exists before configuring. 1. INPUTS email (email, required) Email. The address to look up. Matched EXACTLY — this is not a fuzzy name search, so a typo or a differently-cased alias returns `found_in_instantly = false` rather than a near match. campaign (string, optional) Campaign. Restrict the lookup to one campaign. Leave it empty to search every campaign in the workspace — which is what a suppression check wants, since "have we emailed them on ANY variant" is the question. Resolve the id via the options call in §3. list_id (string, optional) List ID. Restrict the lookup to one Instantly lead list. There is no options resolver for lists — paste the id from Instantly. in_campaign (dropdown: yes | no, optional) In Campaign. OMIT IT to ignore the filter — that is the default and what a suppression check wants. `yes` returns only records attached to a campaign, `no` only records that are NOT. Labels ("Yes") are accepted as well as ids ("yes"). in_list (dropdown: yes | no, optional) In List. Same against lead-list membership; omit to ignore. 2. OUTPUTS found_in_instantly (boolean) — Found In Instantly. `true` when the email exists as a lead anywhere the filters allow. last_contacted (date) — Last Contacted. The most recent send to this person across every campaign they appear in. EMPTY when they exist but have never been emailed, and empty when they aren't in Instantly at all. This is the field a cooldown gate reads. last_replied (date) — Last Replied. last_opened (date) — Last Opened. last_clicked (date) — Last Clicked. last_touch (date) — Last Touch. Most recent interaction of any kind. last_interest_change (date) — Last Interest Change. campaign_id (string) — Campaign ID. The campaign the returned record belongs to (the most recently contacted one — see §4). list_id (string) — List ID. The lead list the returned record belongs to. lead_status (number) — Lead Status. Instantly's own numeric status code, passed through unchanged. Read the current meanings from Instantly's lead schema; they are not redefined here. interest_status (number) — Interest Status. Instantly's numeric interest code, passed through unchanged. verification_status (number) — Verification Status. Instantly's numeric email-verification code, passed through unchanged. opens (number) — Opens. replies (number) — Replies. clicks (number) — Clicks. lead_id (string) — Lead ID. Instantly's UUID for the returned record. Instantly's own Get Lead endpoint keys on this UUID, which is why this action looks leads up by email instead. email (email) — Email, as stored in Instantly. first_name (string), last_name (string), company_name (string), job_title (string), phone (string) — the lead's profile fields. website (url) — Website. personalization (string) — Personalization. The personalization line stored on the lead. custom_variables (json) — Custom Variables stored on the lead. created_at (date) — Created At. updated_at (date) — Updated At. all_campaign_records (jsonArray) — All Campaign Records. EVERY record for this email, one per campaign, sorted newest-contact first. Use it when you need per-campaign detail; the flat fields above already carry the newest one. 3. HOW TO CONFIGURE Resolve the campaign list via Get Action Field Options (only needed if you are scoping to one campaign): POST /api/v1/workflows/{workflow_id}/sheets/{sheet_id}/actions/{action_instance_id}/options/campaign (body: {}) → user's Instantly campaigns. `value` is the campaign ID. Configure Action body — the whole-workspace lookup, which is the one you almost always want: { "inputs": { "email": "{{input.email}}" } } Scoped to a single campaign: { "inputs": { "email": "{{input.email}}", "campaign": "", "in_campaign": "yes" } } Field-by-field: - email Required. Usually `{{input.email}}` or the output of an email waterfall upstream. - campaign Omit for a suppression check. Setting it narrows the answer to one campaign, so someone emailed last week by a DIFFERENT campaign comes back as never contacted. - list_id Same narrowing caveat as `campaign`. - in_campaign Omit unless you specifically want campaign-attached or / in_list list-only records. `no` is a real filter, not an "off" switch — it asks for records that are NOT in a campaign / list. To ignore the filter, leave the field out. 4. KEY NOTES - ONE EMAIL IS SEVERAL RECORDS. In Instantly a lead record belongs to a campaign, so an address enrolled in three campaigns exists three times. This action returns the MOST RECENTLY CONTACTED record in the flat output fields, which is what makes `last_contacted` the latest send across every variant rather than one campaign's stale timestamp. `all_campaign_records` holds the rest. - EMPTY `last_contacted` HAS TWO MEANINGS, and both mean "never emailed them": not in Instantly at all, or in Instantly but not yet contacted. Check `found_in_instantly` when you need to tell them apart. - GUARD FOR EMPTY IN EVERY DATE COMPARISON. `"" > 30` is false, so a naive `days since > 30` test silently classifies never-contacted people as "do not send" — the exact opposite of what a cooldown gate wants, and it drops your freshest leads. Always lead with the empty check: `if (!last) return "send";` See §5. - A miss does not fail the row. `found_in_instantly` comes back `false` with blank fields, and the chain continues. Gate downstream actions on the value with a `filter`, not on `continue_workflow_if_action_fails`. - One API call per row against the user's Instantly key. Instantly's rate limit is per workspace, so a very large sheet paces itself rather than erroring. - Status codes are Instantly's, passed through untranslated. Do not hard-code a meaning for `lead_status` / `interest_status` / `verification_status` without checking Instantly's current schema. 5. WHERE IT FITS IN A WORKFLOW The dominant pattern is a CONTACT COOLDOWN GATE — do not re-email anyone a campaign touched in the last N days: ... -> instantly_get_lead "Instantly: last contacted" -> formula "Cooldown gate (30 days)" -> filter "Drop recently contacted" -> instantly_add_to_campaign formula: (() => { const last = "{{.last_contacted}}"; if (!last) return "send"; return (Date.now() - new Date(last).getTime()) / 86400000 > 30 ? "send" : "skip"; })() filter: {{.result}} is "send" The formula emits the words "send" / "skip" rather than a boolean so the filter condition reads unambiguously; `filter` uses the `is` / `is not` dialect, and an empty or malformed cell can never accidentally satisfy `is "send"`. Placement: put the three steps immediately before the first step you do not want to spend on. Everything upstream still runs for suppressed rows, so gate before enrichment if the enrichment is what costs, not just before the campaign push. Changing the window is the number in the formula — `> 30` becomes `> 45`. Nothing else moves. To drive it per row from a column, read that column instead of the literal: const windowDays = Number("{{input.cooldown_days}}") || 30; Variants on the same three steps: - Also skip anyone who ever replied — a reply is a stronger signal than the calendar: (() => { const last = "{{.last_contacted}}"; const replies = Number("{{.replies}}") || 0; if (replies > 0) return "skip"; if (!last) return "send"; return (Date.now() - new Date(last).getTime()) / 86400000 > 30 ? "send" : "skip"; })() - Only suppress people who are still actively enrolled, and allow re-contact of anyone who finished a campaign: set `in_campaign` to `Yes` on the lookup so records detached from any campaign do not count against the cooldown. - Build a re-engagement list instead of suppressing: flip the filter to keep the "skip" side inverted — keep rows where `last_contacted` is OLDER than the window and `replies` is 0. Rollout note: when you insert these steps into a chain that already has rows, run the FIRST new action (the lookup) with `run_next_action: true` to cascade the new cells down. Re-running the whole row can mark it complete before the newly added cells execute. Other placements: - Enrichment fill: read `first_name` / `company_name` / `custom_variables` back out of Instantly for rows your sheet is missing them for. 6. WHEN TO USE - Suppress people already emailed recently, before pushing a list into an Instantly campaign. The main reason this action exists. - Check whether a prospect is already in outreach before a rep works them manually. - Pull engagement counters (`opens`, `replies`, `clicks`) onto a sheet for scoring or reporting. 7. WHEN NOT TO USE Pushing a lead INTO a campaign -> action-detail/instantly_add_to_campaign.txt The prospect lives in a CRM rather than the sequencer, and the suppression rule is a CRM field (an unsubscribe flag, a do-not-contact owner) -> action-detail/attio_lookup_record.txt -> action-detail/salesforce_lookup_record.txt Looking a prospect up in a different sequencer -> action-detail/outreach_lookup_prospect.txt Last updated: 2026-08-21.