USE_CASE_ID: hiring_signal_outbound NAME: Hiring-signal-triggered outbound CATEGORY: Intent-driven outreach The user has a list builder (or any other source) feeding job postings into Floqer as they're discovered. The list builder enforces the user's job criteria (titles, seniority, company size, geo, salary) before delivery. Each hiring post is treated as an intent signal at the *company* level — same company hiring multiple roles is the same signal, not N — and the default path is to find ICP contacts at that company and enroll them in outreach. This is the canonical Floqer pattern for any intent signal that arrives at a per-event grain (job post, funding announcement, leadership change, tech-stack change) but where the GTM action is per-company. A secondary "recruiting" path treats the post at the role level and targets the hiring manager instead; see Common Variations. For routing, file structure, and cross-cutting principles, see https://floqer.com/docs/use-case-catalog.txt. This file does not repeat them. INDEX: 1. When to use / when not to use 2. Inputs and pre-flight clarifications 3. Outputs 4. Workflow design 5. Implementation 6. Best practices 7. Common variations 8. Failure modes and mitigations 9. Related use cases ================================================================================ 1. WHEN TO USE / WHEN NOT TO USE ================================================================================ USE WHEN: - A list builder (or any other source) is pushing job postings into Floqer on discovery. New rows arrive over time and the sheet's `auto_run: true` setting drives the chain — no cadence or scheduler on your side. - You want each company-with-hiring-signal to surface the right decision-maker (CRO, Head of Sales, VP Eng — whatever ICP role the post implies) and put them into outreach. - The job criteria (titles, company size, geo, salary band) are already enforced at the list-builder level. Floqer's job is to convert posts to contacts to outreach, not to score the post itself. DO NOT USE WHEN: - You're scoring accounts against a static signal mix on a schedule and want a Slack digest of tier movers — see account_scoring_with_cooldown_digest.txt. That use case is pull-on-schedule; this one is push-on-event. - You only need to know who the *hiring manager* is and reach them about the role itself (a recruiting flow). See section 7 Common Variations: "Recruiting variant" for that path — similar plumbing, different finder and different output. - You're starting cold and need to discover companies first — see icp_outbound_prospecting.txt. The hiring-signal use case assumes the list builder hands you a company per row. - The hiring post is the only signal you'll ever use and you'd rather a single rep cold-call the hiring company manually. Floqer's value is in batched at-scale enrichment + sequencer handoff; one-touch low-volume manual flows don't need this. ================================================================================ 2. INPUTS AND PRE-FLIGHT CLARIFICATIONS ================================================================================ INPUTS (per row, as delivered by the list builder) Job-level fields: job_title, job_url, job_posted_date, seniority, salary band, employment_status, description, location, remote / hybrid flags. Company-level fields: company_name, company_domain, company_industry, no_of_employees, total_open_jobs, company_linkedin_url, funding_stage, country, etc. ~70 fields total — most are pre-enriched by the list builder. Data-hygiene observations worth flagging when you start the use case (these came up on the reference workflow): - `Company domain` and `Company Domain` may both appear as input names but resolve to the SAME reference token (`{{input.company_domain}}`) — Floqer snake-cases input names and case-difference collapses. No action needed in the chain; just don't be confused by seeing both in the UI. - `Company` and `Company name` are SEPARATE inputs with their own references (`{{input.company}}` vs `{{input.company_name}}`) — typically the same value, redundantly delivered. Pick one and stick with it; the rest of this file uses `company_name`. - `Company domain` is occasionally empty when the list builder couldn't confidently resolve it. A `Possible Domains` field (JSON-encoded array of candidate domains) sometimes sits alongside as a hint; sometimes it's empty too. The chain's Stage 1 web-agent + JS consolidator covers all three cases (direct → web-agent fallback → Possible Domains fallback). PRE-FLIGHT CLARIFICATIONS Ask these before configuring the chain: - **Additional Floqer-side filtering?** The list builder enforces job-level criteria upstream; check whether the user also wants Floqer-side filters on top (e.g. "skip rows where company has >5000 employees", "only senior+ seniority posts", "only US/CA/UK locations", "only postings from the last 14 days"). Filter early so dedupe and enrichment don't spend credits on rows you'd toss. - **Dedupe grain.** Default is company-level (one chain run per unique company, regardless of how many roles they're hiring). Confirm. Role-level (one run per unique company+role) is a Common Variation. - **ICP persona.** Which role(s) the user wants found at each company — drives the `job_title` / `job_level` filters on the employee finder. Examples: "VP Sales + Head of Sales + Director of Sales", "Head of RevOps", "VP / Director of Eng". - **Output destination.** Default sends contacts to a connected sequencer (Instantly / Smartlead / Lemlist / Reply / HeyReach / LaGrowthMachine). Confirm which is connected, OR whether the user prefers a review sheet for rep triage before push. - **CRM dedupe.** Should already-in-CRM accounts be skipped? Common when the user is sales-led and wants only net-new prospects surfaced. Adds a HubSpot/Salesforce lookup step upstream of the employee finder. ================================================================================ 3. OUTPUTS ================================================================================ - Per company-hiring-signal: a deduped record on the Jobs Feed sheet with all source job context preserved. Duplicates remain on the sheet (mark_as_duplicate) as an audit trail of which roles a company is hiring for. - Per ICP contact at qualifying companies: enriched contact rows on a fan-out sub-sheet with verified work email, ready for outreach. - Contacts pushed to the user's connected sequencer (default), OR appended to a review sheet for rep triage (variation). - Optional: a Floqer-table snapshot of all enrolled contacts for cross-flow audit and re-engagement workflows. ================================================================================ 4. WORKFLOW DESIGN ================================================================================ Two sheets: Jobs Feed (main — receives list builder rows) `auto_run: true` so rows execute as they arrive. The list-builder webhook lands a row, the chain fires once per row, dedupe handles the same-company-multiple-roles case downstream. Chain stages: 1a. llm_web_agents — domain resolver, GATED via run_if (`{{input.company_domain}} is empty`) so it only fires on the ~10% of rows the list builder couldn't resolve. 1b. format_data_using_js_expression — consolidate domain in priority order: direct list-builder value → web-agent result → first entry from `Possible Domains` → empty. 2. (Optional) Floqer-side filters — additional gates the list builder didn't apply. 3. auto_dedupe_rows on `company_name` (or the consolidated domain) with `mark_as_duplicate`, so duplicates remain for audit but skip paid downstream steps. 4. filter on `value_type is "unique"` — only first occurrence of each company continues. 5. get_employees_by_company_using_floqer_native — find ICP contacts at the company. 6. push_data_to_sheet — fan out the contacts structured array into the Contacts sub-sheet. Contacts (sub-sheet — one row per ICP contact) Receives rows from the Jobs Feed push. `auto_run: true` so contacts process as they arrive. Chain stages: 1. (Optional) format_data_using_js_expression — normalize LinkedIn URL for contact-level dedupe. Recipe in https://floqer.com/docs/action-detail/format_data_using_js_expression.txt §8.4 (LinkedIn URL normalization). 2. (Optional) auto_dedupe_rows on the normalized URL — same person can surface via multiple companies in your feed history. 3. person_work_email_waterfall — verified work email. 4. (Optional) one of the four verifier actions — neverbounce / zerobounce / millionverifier / allegrow. 5. (Optional) hubspot_lookup_object or salesforce_lookup_record — skip existing CRM contacts. 6. filter — `email is not empty` (and `CRM record is empty` if the lookup step ran). 7. Sequencer enrollment — one of `instantly_add_to_campaign`, `smartleads_add_to_campaign`, `lemlist_add_lead_to_campaign`, `reply_add_and_push_to_campaign`, `heyreach_add_leads_to_campaign`, `la_growth_machine_add_to_audience`. Why two sheets: hiring signals arrive at the company grain, but outreach happens at the contact grain. `push_data_to_sheet` is the clean fan-out for this 1-to-many relationship. ================================================================================ 5. IMPLEMENTATION ================================================================================ QUICK REFERENCE — action chain per sheet: JOBS FEED (main — receives list builder rows, auto_run: true) inputs: ~70 fields from the list builder. Key ones used in this chain: company_name, company_domain, possible_domains, job_title, seniority, no_of_employees, job_posted_date. chain: llm_web_agents "Resolve domain (fallback)" // run_if company_domain is empty; output_format declares { domain: string } format_data_using_js_expression "Final Domain (consolidate)" // priority: direct → web agent → Possible Domains [filter "Optional ICP filter" ] // optional — additional Floqer-side gates auto_dedupe_rows "Dedupe by company name" // mark_as_duplicate, ignore_case: true filter "Drop duplicate companies" // value_type is "unique" [hubspot_lookup_object | salesforce_lookup_record "Skip if account in CRM" ] [filter "Net-new accounts only" ] // optional — gates on the CRM lookup get_employees_by_company_using_floqer_native "Find ICP contacts" push_data_to_sheet "Send to Contacts sheet" CONTACTS (sub-sheet, auto_run: true) inputs: first_name, last_name, full_name, linkedin_url, job_title, company_name, company_website (from the employee finder's structured array). chain: [format_data_using_js_expression "Normalize LinkedIn URL" ] [auto_dedupe_rows "Dedupe contacts by LinkedIn" ] person_work_email_waterfall "Find work email" [person_work_email_verification_by_* "Verify email" ] [hubspot_lookup_object | salesforce_lookup_record "Skip if contact in CRM" ] filter "Email present (and not in CRM)" "Enroll in campaign" // instantly / smartlead / lemlist / etc. Detailed stages below. STAGE 1 — Domain resolution (Jobs Feed sheet) The list builder delivers `company_domain` for most rows, but ~10% arrive empty when it couldn't confidently resolve. A `possible_domains` field (JSON-encoded array of candidates) sometimes sits alongside as a hint; sometimes it's empty too. Two-step pattern — a gated web-agent fallback, then a JS consolidator that picks the winner in priority order: STAGE 1a — Web-agent fallback (llm_web_agents) Gate with `run_if` so it only fires when `company_domain` is empty. Don't burn agent credits on the ~90% of rows the list builder already resolved. { "inputs": { "select_a_model": "sonar-agent-fast", "mission": "Find the primary website domain for the company named \"{{input.company_name}}\". Return only the bare domain (lowercase, no protocol, no www., no path), e.g. \"acme.com\". If you cannot determine it confidently, return an empty string.", "output_format": { "domain": { "type": "string" } } }, "run_if": { "variable": "{{input.company_domain}}", "operator": "is empty" } } The output `domain` field is referenceable as `{{.domain}}` once the agent runs. `output_format`'s schema is parsed by Floqer at Configure Action time, so you don't need a discovery dance for the output to show up downstream. STAGE 1b — Final-domain consolidator (format_data_using_js_expression) Priority: direct list-builder value > web-agent result > first entry from Possible Domains > empty. Clean every input (lowercase, strip protocol / www / path / query / fragment) so downstream sees a canonical domain regardless of source. (() => { const clean = (d) => { if (!d) return ""; return String(d).toLowerCase().trim() .replace(/^https?:\/\//, "") .replace(/^www\./, "") .replace(/\/.*$/, "") .replace(/\?.*$/, "") .replace(/#.*$/, ""); }; // Priority 1: direct from the list builder const direct = clean("{{input.company_domain}}"); if (direct) return direct; // Priority 2: web-agent fallback (only fires when the list builder didn't resolve) const fromAgent = clean("{{.domain}}"); if (fromAgent) return fromAgent; // Priority 3: first candidate from Possible Domains try { const candidates = JSON.parse("{{input.possible_domains}}" || "[]"); if (Array.isArray(candidates) && candidates.length) { return clean(candidates[0]); } } catch (e) {} return ""; })() Note on the variable-reference syntax: string-typed fields (`company_domain`, the agent's `domain` output) are wrapped in DOUBLE QUOTES inside the JS — the substitution "eats" the quotes and leaves a clean JS string. Backticks behave differently and inject the JSON-stringified form (wrapping quotes survive as literal chars in the resulting string, which makes empty values look truthy). See https://floqer.com/docs/action-detail/format_data_using_js_expression.txt §1 INPUTS (Variable substitution behavior) for the side-by-side example. The JSON.parse on `possible_domains` does use double-quotes too — its stored value is a JSON-encoded string (e.g. `["acme.com"]`), so quotes-get-eaten + JSON.parse gives the array directly. Treat the consolidator's output as the canonical domain for the rest of the chain. Reference downstream as `{{.formatted_data}}` instead of `{{input.company_domain}}` directly. Edge cases: - Both list builder AND web agent fail → consolidator returns "". The employee finder rejects empty `company_identifier` at run time and the chain halts there. Insert a `filter` on `{{.formatted_data}} is not empty` upstream of the finder if you'd rather skip these rows explicitly than hit the halt. - Web agent returns a domain that doesn't match the actual company (the LLM hallucinated or the company isn't online). The dedupe step won't catch this; the employee finder will return "No employees found" or, worse, return the wrong company's employees. Reach for an additional verification step (e.g. `enrich_company_linkedin_profile` against the proposed domain, with a filter on whether `company_name` in the scrape matches the row's input) when this risk matters. STAGE 2 — Optional ICP filtering (Jobs Feed sheet) Ask the user whether they want Floqer-side filters beyond what the list builder already enforced. Common adds: - Company size band `{{input.no_of_employees}} is between [50, 1000]` - Seniority filter `{{input.seniority}} is "senior" OR "executive"` - Geo allowlist `{{input.country_code}} is one of ["US", "CA", "GB"]` - Recency `{{input.job_posted_date}} is after <14 days ago>` - Domain present `{{.formatted_data}} is not empty` Stack with combinators per https://floqer.com/docs/action-detail/filter.txt — "AND" within a group, separate groups joined by "OR". STAGE 3 — Dedupe at the company level (Jobs Feed sheet) Single `auto_dedupe_rows` on `company_name` (or the consolidated domain — pick one canonical key per workflow). Use `mark_as_duplicate` rather than `delete_duplicate` so duplicates remain on the sheet as an audit trail of which roles a company is hiring for. { "inputs": { "auto_dedupe_columns": "{{input.company_name}}", "select_dedupe_action": "mark_as_duplicate", "ignore_case": true } } For role-level grain (one row per company+role), see Common Variations. STAGE 4 — Gate downstream on uniqueness filter on `{{.value_type}} is "unique"`. Duplicate rows complete the dedupe step but the chain halts here for them. STAGE 5 — (Optional) Skip accounts already in CRM When the user is sales-led and only wants net-new prospects, insert a CRM lookup BEFORE the employee finder — both saves enrichment credits and avoids duplicate work for existing accounts. hubspot_lookup_object on company domain (preferred when domain is reliable) salesforce_lookup_record on account name Gate the downstream finder with `{{.total}} is 0` — only net-new companies proceed. STAGE 6 — Find ICP contacts `get_employees_by_company_using_floqer_native` is the cheapest default (0.005 credits per employee returned). Configure with the user's ICP role + seniority — example for "find senior sales decision-makers": { "inputs": { "company_identifier": "{{.formatted_data}}", "number_of_employees": "10", "job_title": "VP Sales, Head of Sales, Director of Sales", "job_level": ["c-suite", "vice president", "director"] } } Use Apollo (`get_employees_by_company_using_apollo`) instead when richer per-person attributes are needed (employment history, departments, phone numbers in the finder output) — see https://floqer.com/docs/action-detail/get_employees_by_company_using_apollo.txt. STAGE 7 — Fan out to Contacts sub-sheet `push_data_to_sheet` with `select_a_list: {{.employees}}`, mapping structured-array columns into the Contacts sheet's inputs. Carry company context (name, domain) onto each contact row so personalization works downstream. STAGE 8 — Contact enrichment (Contacts sub-sheet) Standard contact-enrichment chain, already documented in detail elsewhere: - person_work_email_waterfall: configure with linkedin_url + full_name + company_domain for the highest-signal mix. - One verifier action (MillionVerifier @ 0.1 credit is the cheap default). - Optional hubspot_lookup_object / salesforce_lookup_record by email to skip contacts already in CRM. STAGE 9 — Sequencer enrollment Pick the sequencer action that matches the user's connected tool. Field mapping at minimum: email, first_name, company_name. Most sequencer actions accept `custom_variables` for personalization tokens — pass the upstream job_title or job_description so the rep's template can reference the triggering hire. ================================================================================ 6. BEST PRACTICES ================================================================================ - **Resolve domain before dedupe and enrichment.** Without Stage 1's gated web-agent fallback + JS consolidator, rows with empty `company_domain` either fail silently downstream or get the wrong company at the employee finder. The web agent is cheap (~0.2–1 credit per row that needs it) and only fires when the list builder didn't already resolve, so the spend is bounded. - **Company-level dedupe is the right default.** Same company hiring multiple roles is the same intent signal, not N independent signals. The contacts you'd reach are the same regardless of which role triggered the chain. Role-level dedupe is a variation for users with persona-by-role ICPs. - **Filter → dedupe → enrich, in that order.** Filtering on row inputs (job seniority, geo, size band) is free; dedupe is free; the employee finder is the first paid step. Keep them in that order so paid steps only see qualified, deduped rows. - **Cap number_of_employees on the finder.** Even at 0.005 credits per employee, set a sane limit (5–20) so a 5000-headcount company doesn't return 100+ contacts when the user only wants the senior decision-makers. - **Use Floqer Native unless you have a reason to use Apollo.** Cheaper, and the structured-array output is sufficient for most outreach chains. Reach for Apollo when you need its richer per-person fields (department, employment history, phone numbers inline) in the finder output. - **Always verify emails before enrolling.** Sequencer reputation matters; a 10% bounce rate burns deliverability fast. MillionVerifier / Allegrow at 0.1 credit is cheap insurance. - **Snapshot enrolled contacts.** Add a `push_data_to_another_floqer_workflow` at the end of the Contacts chain to write a per-contact audit row to a master "Enrolled" sheet. Powers cross-flow dedupe and re-engagement workflows down the road. ================================================================================ 7. COMMON VARIATIONS ================================================================================ - **Recruiting variant** (alternative output path). Treat the post as a *role-level* signal and target the hiring manager instead of generic ICP contacts. The list builder includes a `Hiring Team` field that often names the hiring manager; parse it with a JS formatter, then enrich via `person_enrich_using_apollo` or `person_enrich_using_people_data_labs` to get the manager's LinkedIn / email. Output is usually a CRM contact create + Slack ping (one-touch personal outreach) rather than sequencer enrollment. - **Role-level dedupe grain** (default is company-level). Some users want to act per-role rather than per-company — common when ICP varies by hire type (a "VP Sales" hire calls for a different contact than a "VP Eng" hire at the same company). Combine company + role into a derived column via a JS formatter upstream of dedupe, then dedupe on the derived column: // Yields "Acme|VP Sales" for the derived dedupe key `{{input.company_name}}|{{input.job_title}}` - **Persona varies by post.** Different posts call for different contacts ("they're hiring a CISO → talk to the CFO about your security tool", "they're hiring a VP Sales → talk to the CEO"). Stage 6 becomes branch-conditional: a JS formatter or `llm_models` step classifies the post's persona signal, then a series of `run_if`-gated `get_employees_by_company_using_floqer_native` actions each target a different role. The Partnerships flow uses a similar pattern in its `ICP Qualifier/Vertical` step. - **CRM-aware exclusion.** Skip companies already in active pursuit (covered as the optional Stage 5 above). Saves enrichment credits and avoids duplicate work. - **Brief writing on hot rows.** Insert `llm_models` after the dedupe gate but before `push_data_to_sheet`, generating a short outreach brief (signals summary + suggested angle) per company. Store the brief on the Jobs Feed row; reference it from the Contacts sub-sheet via a cross-sheet lookup when composing the sequencer payload. Use a cheap model (GPT 4.1 Nano / Gemini Flash Lite) given the volume. - **Multi-source intent.** The same workflow shape works for any per-event intent signal — funding announcements, leadership changes, tech-stack changes, web-visit signals. Swap the list builder for the trigger source; rename the Jobs Feed sheet; the rest of the chain (resolve domain → dedupe → find contacts → enrich → outreach) is identical. ================================================================================ 8. FAILURE MODES AND MITIGATIONS ================================================================================ - Domain missing from the list builder AND web agent fails to resolve it AND `Possible Domains` empty → consolidator emits "", downstream enrichment fails. Mitigation: the chain already includes the web-agent fallback as Stage 1a. When even that fails, gate the employee finder with `filter` on `{{.formatted_data}} is not empty` so the row halts cleanly at the gate rather than producing an opaque "Company identifier is empty" error mid-chain. - Web agent hallucinates the domain (resolves to an unrelated company's site). Mitigation: dedupe doesn't catch this — the wrong-company domain will look unique and proceed. Add a verification step (e.g. `enrich_company_linkedin_profile` against the proposed domain, then a `filter` on whether the scraped `company_name` matches the row's input) when this risk matters. Lower-risk than it sounds for most lists — the sonar-agent-fast model is grounded against web search, so confabulation is rare for companies with any web presence. - Same company arrives many times within a short window (the list builder publishes multiple roles before the chain completes the first row). Mitigation: `auto_dedupe_rows` handles this — each row is compared against earlier rows on the sheet, so later arrivals are marked `duplicate` on arrival. Cache stays on (default); same inputs return the same outputs anyway. - Employee finder returns "No data found" for under-indexed companies and halts the row. Mitigation: see icp_outbound_prospecting.txt Stage 3 alternative — web-agent fan-out for sub-50-headcount / non-tech / family-business segments where Floqer Native and Apollo are sparse. Reach for this when the list builder is publishing companies outside the dense-LinkedIn coverage zone. - High bounce rate on sequencer enrollment. Mitigation: always verify before push (Stage 8 step). Cheap (0.1 credit) and protects the user's sender domain. - Sequencer rate limits. Mitigation: most sequencers cap new-leads-per-day. Add a `delay_step` upstream of the sequencer action if you're seeing bursty arrivals, or batch via `push_data_to_another_floqer_workflow` into a "Daily enrollment" sheet that runs on a cadence. - Same person surfaces via multiple companies over the lifetime of the feed (they're at company A in March, at company B in May — the feed pushes contacts from both). Mitigation: contact-level `auto_dedupe_rows` on normalized LinkedIn URL (Contacts sub-sheet, Stage 8.1 + 8.2). Pair with the Job Change Detection use case if the user cares about catching these movers and re-routing them. - Snake_case collision on `Company domain` and `Company Domain` inputs. Mitigation: not a bug — both names resolve to the same `{{input.company_domain}}` reference. Pick one in the UI and don't be confused by seeing both. - Recruiting variant: `Hiring Team` field is empty or non-parseable. Mitigation: fall back to `llm_web_agents` ("find the hiring manager for {{input.job_title}} at {{input.company_name}}"), or skip the row with a filter and notify the user via Slack so they can manually research. ================================================================================ 9. RELATED USE CASES ================================================================================ - icp_outbound_prospecting — when the user has no list and needs to discover companies first via firmographic filters. Stage 1 (discovery) is different; Stages 2–7 (find contacts → enrich → outreach) are nearly identical. - account_scoring_with_cooldown_digest — when the output is a Slack digest of new high-tier accounts rather than direct sequencer outreach. Different output shape; same intent-signal-upstream pattern. - account_research_and_scoring — when accounts need scoring against a multi-signal rubric before outreach. Plug as an upstream block on the Jobs Feed sheet between dedupe and the employee finder. - (planned) job_change_detection_on_crm_contacts — the same-person-changed-company sibling. The Contacts sub-sheet's optional URL-normalize + dedupe step is the seed; the Job Change use case adds the LinkedIn re-scrape + before/after comparison. ================================================================================ This file is maintained manually. Last updated: 2026-05-13. Use case catalog: https://floqer.com/docs/use-case-catalog.txt Related action details: https://floqer.com/docs/action-detail/auto_dedupe_rows.txt https://floqer.com/docs/action-detail/get_employees_by_company_using_floqer_native.txt https://floqer.com/docs/action-detail/person_work_email_waterfall.txt https://floqer.com/docs/action-detail/push_data_to_sheet.txt https://floqer.com/docs/action-detail/format_data_using_js_expression.txt