USE_CASE_ID: find_people_at_target_companies NAME: Find people at a list of target companies CATEGORY: Outbound prospecting The user already has a list of target companies — sourced themselves (a CSV, a saved search, a CRM export, a list someone handed them) — and just wants the right people inside each one, as a usable contact list. No ICP discovery, no firmographic filtering, no scoring or tiering. The company list is the input; an enriched people list is the output. This is the most common "find people" request, and the one most often built badly. The mechanics of the employee finder are unintuitive, so agentic builds reach for the obvious-looking configuration and quietly get near-zero recall, blown budgets, or a sheet that can't be enriched. Section 6 (Best practices) is the point of this file — read it before you build, not after. 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 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: - The user already has the companies. They sourced the list and don't need Floqer to discover or qualify accounts. - They want people at those companies — names, LinkedIn, title, and (usually) a verified work email — as a list they can use. - The selection logic is simple: "the senior people in [function]" or "the decision makers", expressed as a function + seniority, not a scored rubric. DO NOT USE WHEN: - The user has no company list yet and needs accounts discovered from an ICP → ICP-based outbound prospecting (icp_outbound_prospecting.txt). That doc's later stages are this flow; this file is those stages standalone. - The trigger is an external intent signal (hiring, social activity, web visit) → Intent-driven outreach category. - The user wants accounts qualified, scored, or tiered before picking people → Account research and scoring category. (No scoring here — that's the whole point.) - The user already has the people and only needs emails/phones → skip the finder; go straight to the enrichment actions in stage 3. - The user only needs ONE specific named person per company → use a person-enrich action (person_enrich_using_apollo) with name + company, not an employee finder. ================================================================================ 2. INPUTS ================================================================================ - A company list, one company per row. Each row should carry a company_domain or a company LinkedIn URL (see best practice 1 — plain company names match the wrong company for generic names). - A people selector: the function(s) you want (e.g. "marketing", "sales", "engineering", "finance") and the seniority you'll accept (e.g. C-suite, VP, director). Expressed loosely — this flow turns it into the right finder filters, it is not a scored ICP. - Optional: a per-company cap (how many people per company), a country filter, and an outreach/CRM destination if the list isn't the final deliverable. ================================================================================ 3. OUTPUTS ================================================================================ - A people list — one row per person — on a Floqer sheet: full name, LinkedIn URL, current title, company, and (when stage 3 runs) a verified work email. - Optionally pushed onward to a sequencer or CRM (see variations) — but the default deliverable is the enriched sheet itself. ================================================================================ 4. WORKFLOW DESIGN ================================================================================ The shortest chain that works. This is a deliberately small flow — do not bolt on scoring, multi-stage qualification, or research steps the user didn't ask for. 1. Cap the company list (if large) before any per-company paid call. 2. Find the right people at each company (one finder call per row). 3. Fan out the returned employee array into one row per person on a new sheet. 4. (Usual) Enrich + verify a work email per person. 5. (Optional) Normalize, dedupe, and push to a sequencer/CRM. Stages 4 and 5 are optional — if the user only wants "who works there", stop after stage 3. Chain shape (two sheets; the always-on dual source fans into one People sheet — see §7 variations). The sketch is topology only; action IDs and config live in §5, which is the single source of truth: [Companies sheet] input.company_name -> web agent: resolve domain -> native finder ----------------------> push ----+ -> web agent: people -> raw->struct -> push ------+ (both sources) v [People sheet] standardize-LinkedIn -> dedupe -> scrape -> current-roles (JS) -> still-at-company (LLM) -> [filter] -> work-email -> verify ================================================================================ 5. IMPLEMENTATION ================================================================================ STAGE 1 — Cap the company list Action: csv_to_structured_array_format / filter with a hard cap. Why: The finder is paid per employee found. Cap the company count before it runs, and prove on 1-3 companies first (see best practice 7). Skip if the list is already small. STAGE 2 — Find people at each company Action: get_employees_by_company_using_floqer_native (default — cheapest per employee at 0.005 each). Apollo (get_employees_by_company_using_apollo) or Sales Nav (get_employees_by_company_using_sales_navigator) only when the data source itself matters; Sales Nav is far more expensive. Config that matters (read action-detail/get_employees_by_company_using_floqer_native.txt §4 before building): - company_identifier: the row's domain or LinkedIn URL, NOT name. - job_title: the bare FUNCTION term(s), comma-separated — e.g. "marketing" or "sales, revenue". This is a contains-ANY-term filter, not an exact title match (best practice 2). - job_level: the seniorities you'll accept, as a JSON array — e.g. ["c-suite", "vice president", "director"]. This does the seniority narrowing; job_title does the function narrowing. They compound. - number_of_employees: the smallest count that meets the need (best practice 3). - country_code: optional ISO-alpha-2 JSON array if geo-scoping. STAGE 3 — Fan out to one row per person Action: push_data_to_sheet, mapping the `employees` structured_array to a new sheet (full_name, linkedin_url, job_title, company_name, company_website per row). Why: The finder returns ALL matched people as a single structured array on the company row. Per-person enrichment (email, phone) runs per row, so you must expand the array into rows first. This is the step agentic builds skip most often (best practice 4). STAGE 4 — Enrich + verify work email (usual) Action: person_work_email_waterfall, then a verifier (person_work_email_verification_by_neverbounce / _by_million_verifier / _by_zerobounce / _by_allegrow). Why: Always verify after enrichment — unverified sends harm domain reputation more than they help. Skip this whole stage if the deliverable is just the people list, not outreach. STAGE 5 — Normalize, dedupe, push (optional) Action: format_data_using_js_expression to fix title/company casing (the finder returns mangled casing — see best practice 6), auto_dedupe_rows to drop repeats, then the user's sequencer (smartleads/instantly/lemlist/heyreach/...) or CRM create action. Why: Only when the list feeds an external system. A raw Floqer sheet doesn't need the casing fix; a CRM/CSV/sequencer does. DATA PASSING: Carry company_name and company_domain onto each fanned-out person row so downstream personalization and dedupe work. Reference structured arrays as {{action_instance_id.list_name.column_name}}. See concepts.txt section 5. ================================================================================ 6. BEST PRACTICES ================================================================================ The employee finder is unintuitive. These are the misconfigurations agentic builds repeatedly ship — each one looks correct and fails quietly. 1. Identify companies by domain or LinkedIn URL, never plain name — and resolve the domain with a WEB AGENT by default. Generic names ("Apex", "Summit", "Core") resolve to the wrong company, and email coverage downstream is only as good as the domain you carry, so domain resolution is not a place to economize. floqer_company_ firmographics works but misses or returns a corporate sub-domain for regional / smaller fintechs — exactly the rows where email find-rate then craters. An llm_web_agents step ("return the registrable ROOT domain for {{input.company_name}}") reads the live web and gets the bare root domain at ~100% coverage. Resolve the domain ONCE on the company row, then carry it onto every fanned-out person row (map it in the push) so the email step has it. Validated live 2026-06-10: web-agent domain resolution returned a clean root domain for 5/5 payments/fintech companies where firmographics had returned a sub-domain on one. 2. Put the FUNCTION in job_title, the SENIORITY in job_level. This is the single biggest cause of empty results. job_title is a contains-ANY-term filter, so a full persona title — "VP of Treasury Operations", "Head of Demand Generation", "Director, Revenue Marketing" — matches almost nobody and recall collapses to near-zero. Feed the bare function ("treasury", "marketing", "sales") in job_title and set job_level (["c-suite","vice president","director"]) for seniority. The two filters compound and catch every title variant ("VP, Marketing", "Head of Marketing", "Marketing Director") a phrase filter would drop. Use a specific title string ONLY when you genuinely want one narrow role and are willing to miss variants. 3. Set number_of_employees to the smallest count that meets the need. Cost scales per employee returned. "Top 3 marketers per company" should request 3, not 50. Don't leave it at a high default. 4. Fan out the array to rows before per-person enrichment. The finder returns everyone in one structured_array on the company row; email and phone actions run per row. Push the array to a new sheet (one row per person) FIRST. Trying to email-enrich the array in place is the most common build error and silently produces nothing usable. 5. Treat "No employees found" as a zero-match signal, not an error. When filters match nobody at a company, the cell fails with "No employees found for this company" and the row flips to has_failures — and that flag is sticky even after every other cell succeeds. The chain does NOT halt. Read individual CELL statuses, not row status, when checking whether people came back. Don't retry the same finder on a true zero-match — loosen the filters or use the web-agent fallback (failure mode below). 6. Normalize casing before anything strict. Titles come back sentence-cased ("Gtm engineer") and company/country can carry non-ASCII or lowercased text — an upstream-provider artifact, not a Floqer transform. A downstream LLM tolerates it; Salesforce, Instantly, and CSV exports surface the garbage verbatim. Add a format_data_using_js_expression normalizer before any CRM/sequencer/ export write (the title-case + acronym-preserve snippet is in get_employees_by_company_using_floqer_native.txt §4). 7. Prove on 1-3 companies, then scale. Cell-test one finder call, read the returned people, confirm the function/seniority filters are returning who you expect, THEN run the whole list. A wrong job_title across 500 companies is real money and a useless sheet. 8. Dedupe people on a STANDARDIZED LinkedIn URL, not just companies. The same person can match for more than one company on the list (advisors, multi-company founders), and the moment you run two sources for coverage (native finder + web-agent finder, see best practice below) the SAME person comes back from both — with differently-formatted URLs (linkedin.com/in/x vs https://www.linkedin.com/in/x/ vs https://in.linkedin.com/in/x?…). A literal dedupe treats those as three people. So: normalize the URL first in a format_data_using_js_expression (bare canonical — strip protocol/www/country-subdomain/query/slash; recipe in format_data_using_js_expression.txt §8.4), then auto_dedupe_rows (delete_duplicate, ignore_case) on that normalized column, placed BEFORE the scrape so you never pay to enrich a duplicate. If you merge finder structured_arrays on the company row instead, use merge_employee_finder_structured_array — but the web-agent finder returns a raw_array, not a finder structured_array, so the standardize-then-dedupe-on-the-people-sheet path is the one that covers both sources. Validated live 2026-06-10: native + web-agent on PayU produced 14 rows with 3 cross-source LinkedIn overlaps; standardize + dedupe collapsed them to 11 before any paid step. ================================================================================ 7. COMMON VARIATIONS ================================================================================ - People list only (no email): stop after stage 3. The deliverable is names + LinkedIn + title per company. Common for LinkedIn-first outreach or manual review. - Maximum coverage per function: widen job_level rather than adding title strings — e.g. ["c-suite","vice president","director","senior manager"]. Keep job_title to the bare function. - Always-on dual source (coverage to ~100%): run an llm_web_agents people finder on EVERY company in parallel with the native finder (not just gated to finder-zero rows), then dedupe. Mission returns a JSON array of {full_name, job_title, linkedin_url} via the single- field raw_array pattern (llm_web_agents.txt §8) -> raw_to_structured_ array -> push to the People sheet alongside the native push. You pay the web agent on every row, but no company is left to a single source's coverage gap. Pair with best practice 8's standardize + dedupe — mandatory here, since the two sources overlap heavily. Validated live 2026-06-10: on a company the native finder returned zero for (Mswipe), the always-on web agent still found 4 in-scope product/payments people. - Verify still-at-company before email: insert the still-at-company gate (LinkedIn scrape -> current-roles-from-experiences JS -> "still at company?" LLM -> hard filter) between the scrape and the email step, so you don't spend on someone who has moved on. Match GENEROUSLY — on the distinctive brand root + domain, NOT the exact legal entity — or you will drop people who merely moved between sister entities under one brand (e.g. sourced for "Mswipe Technologies", now at "Mswipe Capital" — same brand, keep). Full pattern + prompt: still_at_company_outbound_gate.txt. This also vets web-agent-sourced people, which is worth doing since a web agent can surface a slightly stale name. Validated live 2026-06-10. - Push to sequencer / CRM: add stage 5's destination action. Field mapping at minimum: email, first_name, company. - Account-level headcount signal (no fan-out): if the user wants "how many people in [function] does each company have" rather than the people themselves, set the finder's filters and read the no_of_employees output on the company row directly — skip stages 3-5 entirely. - Multi-function: run the finder once per function set and union with merge_employee_finder_structured_array before fan-out (best practice 8), rather than cramming unrelated functions into one job_title. ================================================================================ 8. FAILURE MODES AND MITIGATIONS ================================================================================ - Near-zero people returned across the list. Almost always best practice 2: a full title phrase in job_title. Mitigation: move seniority to job_level, leave only the bare function in job_title, re-test one cell. - "No employees found" on some companies → row marked has_failures. Mitigation: expected for a true zero-match; read cell status not row status (best practice 5). If it's a coverage gap rather than a real zero (see next), switch those rows to the web-agent fallback. - Under-indexed companies return nothing (sub-50-headcount, non-tech, regional service firms, family businesses, government contractors). The coverage gap is structural — retrying the finder won't help. Mitigation: for those segments use llm_web_agents with a raw_array fan-out (read company sites, Companies House, news), then raw_to_structured_array → push_data_to_sheet. See icp_outbound_prospecting.txt stage 3 (alternative) and llm_web_agents.txt §5/§8 for the pattern. - Cost spike on a large list. Mitigation: cap the company list (stage 1) AND keep number_of_employees tight (best practice 3). Both multiply. - Sheet can't be email-enriched / downstream refs resolve empty. Mitigation: you skipped the fan-out (best practice 4). Email/phone actions need one row per person; push the structured array to a sheet first. - Garbled titles/company names in the CRM or CSV. Mitigation: add the casing normalizer before write (best practice 6). ================================================================================ 9. RELATED USE CASES ================================================================================ - ICP-based outbound prospecting — when there's no company list yet; its later stages ARE this flow. - Hiring-signal-triggered outbound — same find-people-at-a-company fan-out, but triggered by a job-posting feed instead of a static list. - Extract data from CRM — when the "people" already live in a CRM and you want to pull rather than discover them. - Account research and scoring — bolt on AFTER this if the user later decides they do want the accounts qualified. ================================================================================