USE_CASE_ID: stakeholder_map_sourcing NAME: Stakeholder mapping — multi-source contact coverage at an account CATEGORY: Account research and scoring The user has a small list of strategic accounts and wants to know EVERYONE who touches the deal inside each one — not the three best people to email. Finance, payments, product, risk, ops, engineering, commercial, the C-suite: the whole buying committee, as a deduped and qualified contact list per account. The defining constraint is RECALL, not efficiency. A single employee- finder call tuned to one function returns a usable outbound list and misses most of the committee. So this flow deliberately runs many sources — one finder call per FUNCTION BUCKET, plus an always-on web agent — and pays for that breadth with a triage step, a dedupe step, and a qualification step on a Contacts sheet. That trade only makes sense on accounts that justify it. Per account, this costs an order of magnitude more than find_people_at_target_companies.txt. Run it on 20 named accounts, not 2,000 — and if the user's real ask is "get me the right people at these companies", build THAT flow instead. Section 6 (Best practices) is the point of this file: the cost controls and the dedupe are what separate a working stakeholder map from a sheet of 250 half-relevant duplicates. 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 names a handful of strategic / enterprise accounts and wants the buying committee mapped at each one. - The ask is coverage-shaped: "who are all the people we should know at X", "map the stakeholders", "we need to multi-thread", "who owns payments / treasury / compliance there". - The persona is genuinely MULTI-FUNCTION — a deal that needs finance AND product AND risk AND engineering to say yes. DO NOT USE WHEN: - The user has a long company list and wants the right few people at each → find_people_at_target_companies.txt. Cheaper, simpler, and the correct answer to the more common request. Do not upgrade them to this flow because it sounds more thorough: that flow is one finder call per company, this one is N finder calls plus web agents per account, then a scrape and three model calls for every person who survives triage. - There is no account list yet → icp_outbound_prospecting.txt. - The people already exist in the CRM and the user just wants them pulled → extract_data_from_crm.txt. - The user wants the ACCOUNT qualified/scored, not the people → account_research_and_scoring.txt. - The user also wants CRM contacts merged in and a synthesized per-account map as the deliverable → stakeholder_map_full.txt, which is this flow plus those two stages. ================================================================================ 2. INPUTS ================================================================================ - An account list, one account per row: account_name + domain (required), plus a CRM account_id if anything will be written back. - FUNCTION BUCKETS — the load-bearing input, and the one to nail down before building. Each bucket is: * a name (the function): e.g. "Payments", "Risk & Compliance" * short CONCEPT terms, comma-separated: e.g. "Payments, Payment Operations, Settlement, Clearing, FX, Remittances" * the seniorities you'll accept: e.g. ["c-suite", "vice president", "director", "senior manager", "manager"] Ask the user which functions touch their deal. Do not invent them, and do not collapse them into one list (best practice 1). - A per-bucket cap (how many people per function, per account). - A relevance rubric for the qualification step: what makes a person RELEVANT to this seller, expressed as function + buying power, not as a list of exact titles. - Optional: country filter, and whether work emails are needed (the email tail is off by default — the deliverable is the map). ================================================================================ 3. OUTPUTS ================================================================================ - A Contacts sheet: one row per person, deduped across every source, carrying account_name / domain / account_id, LinkedIn URL, first + last name, current title (translated to English), business_function, buying_power, a relevance verdict + reason, and a short profile summary. - Per-bucket coverage on the account row: each finder's no_of_employees output tells you which functions the account actually has (an empty "Risk & Compliance" bucket is itself a finding). - Optionally a verified work email per contact (see §7). ================================================================================ 4. WORKFLOW DESIGN ================================================================================ Two sheets. The Accounts sheet is a fan-out engine (many sources, one triage, two pushes). The Contacts sheet is where dedupe and qualification happen — one row per person, so per-person paid actions can run. The sketch is topology only; action IDs and config live in §5, which is the single source of truth. [Accounts sheet] input.account_name + input.domain -> company LinkedIn from domain -> firmographics (company LinkedIn URL + country) -> finder × N function buckets (short concept terms, capped) -> merge employee lists -> JS: [{title, linkedin_url}] -> LLM triage: keep the ~40 that give holistic coverage -> csv -> list -> push ------------------------------+ -> web agent(s): broad people search (+ C-suite) | -> csv -> list -> push ------------------------------+ v [Contacts sheet] input.person_linkedin_url + account fields -> JS: normalize LinkedIn -> auto_dedupe (delete) <- before any paid step -> scrape LinkedIn profile -> LLM: translate title to EN -> web agent: profile summary -> LLM: qualify (function / buying power / relevance) -> JS: surface relevance as its own column -> [filter] Two pushes into ONE sheet is deliberate, not sloppy: the finder path produces an employee structured_array and the web-agent path produces a csv/raw list, and those two shapes cannot be merged upstream (best practice 7). The Contacts sheet is where they reconcile — which is why the dedupe lives there and not on the account row. ================================================================================ 5. IMPLEMENTATION ================================================================================ STAGE 0 — Resolve a company identifier Actions: company_linkedin_url_using_website, then floqer_company_firmographics. Why: the native finder matches on a company LinkedIn URL or domain, never a plain name. Firmographics also gives you the account's country + HQ, which you carry onto every contact row for the qualification prompt. STAGE 1 — One finder call per function bucket Action: get_employees_by_company_using_floqer_native (one instance per bucket; NAME each instance after its function — "Payments", "Risk & Compliance" — or the sheet becomes unreadable). Config that matters (read action-detail/get_employees_by_company_using_floqer_native.txt §4 before building): - company_identifier: the firmographics company_linkedin_url. - job_title: the bucket's SHORT concept terms, comma-separated. This is a contains-ANY-term filter, so terms compound as OR. "Treasury, Payments, Settlement" — never "VP of Treasury Operations" (best practice 2). - job_level: the bucket's seniorities as a JSON array. Function goes in job_title, seniority goes in job_level; they compound. - number_of_employees: the per-bucket cap (best practice 3). STAGE 2 — Merge the finder arrays Action: merge_employee_finder_structured_array, with every finder's `employees` output as an entry in lists_of_employees. Note: it merges, it does NOT dedupe — and it silently ignores any input that isn't a finder structured_array (best practice 7). STAGE 3 — Triage on title BEFORE anyone gets scraped ← the cost gate Actions: format_data_using_js_expression → llm_models → csv_to_structured_array_format → push_data_to_sheet. How: a. JS reduces the merged array to the two fields the triage needs: Object.values({{.list_of_employees}}).map(record => ({ title: record.find(d => d.responseStructureId === 'title')?.value, linkedin_url: record.find(d => d.responseStructureId === 'linkedin_url')?.value })) b. llm_models receives that array and returns ONLY a comma- separated list of the LinkedIn URLs worth carrying forward. Prompt it for CONTENT — "select the subset that gives the most holistic view of decision makers, influencers and specialist ICs; prefer coverage of every function over depth in one" — and give it a target count (~40) it is allowed to miss in either direction. Do not prompt it to score people; that happens later on real profile data. c. csv_to_structured_array_format turns the comma-separated string into a list. d. push_data_to_sheet fans that list into the Contacts sheet. Why this stage exists: N buckets × cap can be 200+ people per account. Scraping and qualifying all of them costs orders of magnitude more than one LLM call over their titles (best practice 4). STAGE 4 — Always-on web-agent source Action: llm_web_agents (floqer-nova by default; a second instance on parallel-core buys genuinely different coverage on hard accounts), then csv_to_structured_array_format → push_data_to_sheet into the SAME Contacts sheet. Config: - mission: name the account (name + domain + company LinkedIn URL) and describe the FUNCTIONS you want, stating the lists are representative, not exhaustive. Ask for LinkedIn profile URLs. - Guard the downstream csv action with a run_if: `{{.linkedin_profile_urls}} contains "linkedin.com"` — web agents return prose, an apology, or nothing on thin accounts, and you do not want that pushed onto the Contacts sheet as a row. - Watch the single-URL case. csv_to_structured_array_format returns an EMPTY list when handed a single value with no comma in it (csv_to_structured_array_format.txt §4) — so an account where the agent finds exactly one person silently yields zero rows. Feeding the csv action a comma-joined pair of agent outputs (e.g. the broad search's URLs plus a dedicated C-suite agent's) sidesteps it in practice; raw_to_structured_array is the alternative if you need a single-source list to survive. - Map the same account columns in the push as stage 3 does, so both sources produce identically-shaped rows. Why: the native finder is structurally blind on smaller, regional and non-tech orgs. This is coverage insurance, and on a stakeholder map it is not optional (best practice 5). STAGE 5 — Normalize, then dedupe, then pay (Contacts sheet) Actions: format_data_using_js_expression → auto_dedupe_rows. Config: - The formatter strips protocol / www / country subdomain / query / trailing slash to a canonical LinkedIn URL (recipe: format_data_using_js_expression.txt §8.4). - auto_dedupe_rows on THAT column, select_dedupe_action "delete_duplicate", ignore_case true. Placement is the whole point: after the normalizer (the dedupe key must be populated, or the cell errors) and BEFORE the scrape (or you pay to enrich the same person twice — three sources means heavy overlap by design). STAGE 6 — Enrich and qualify each person Actions, in order: - enrich_person_linkedin_profile — the profile is what everything downstream reads. - llm_models "translate title to EN" — international accounts return native-language titles, and the relevance gate cannot classify what it cannot read (best practice 8). - llm_web_agents — a 2-3 sentence profile summary a rep can read. - llm_models "qualify" — returns business_function, buying_power, title_relevance (high / medium / low) and a one-line relevance_reason. Prompt on FUNCTION, not exact wording, and qualify LOOSELY (best practice 9). - format_data_using_js_expression to surface title_relevance as its OWN column, then a filter on it. Surfacing it matters: a downstream lookup reads sheet cells by display name, and a filter cell holds the filter's status, not the value it filtered on (lookup_another_floqer_workflow_row.txt §2). DATA PASSING: Both pushes must map account_name, domain, account_id and company location onto every contact row (choose_columns_to_send_to). The Contacts sheet cannot see its parent row — anything the qualification prompt or a later writeback needs has to be carried across at push time. ================================================================================ 6. BEST PRACTICES ================================================================================ 1. Buckets, not one mega-finder. One finder instance per FUNCTION, each with its own concept terms and seniority set. Cramming every function into a single job_title with a big cap does not give you coverage — it gives you a sample skewed to whichever function happens to be biggest at that company (usually engineering), and the small functions you actually needed (treasury, compliance) fall off the bottom. Bucketing is what makes the recall even. 2. Short concept terms in job_title, seniority in job_level. job_title is a contains-ANY-term filter. A phrase title ("Head of Payment Operations") matches almost nobody and recall collapses to near-zero. Feed bare concepts ("Payments, Settlement, Clearing") and let job_level do the seniority narrowing. 3. Cost is buckets × cap × accounts. Ten buckets at a cap of 25 is up to 250 people per account BEFORE any per-person step. Know that number before you run the list, and set the cap per bucket according to how deep that function goes (the C-suite bucket needs a cap of 8, not 25). 4. Triage on title before you scrape. The finder is cheap per person; scrape + summarize + qualify is not. One LLM call over {title, linkedin_url} is the highest-leverage action in this flow — it is what makes a wide net affordable. Never wire the merged finder array straight into the Contacts sheet. 5. Run a second source on EVERY account, not just the empty ones. The native finder's blind spots are structural (sub-50-headcount, regional, non-tech, family-owned), and you cannot tell from the account row whether a thin result is a thin company or a thin index. An always-on web agent removes the question. Coverage is the deliverable here — a single source cannot produce it. 6. Normalize the LinkedIn URL, then dedupe, then spend. The same person comes back from the finder and the web agent with different surface forms of the same URL (linkedin.com/in/x, https://www.linkedin.com/in/x/, https://uk.linkedin.com/in/x?utm=). auto_dedupe_rows does a literal compare — without the normalizer it sees three people, and you pay to scrape all three. 7. merge_employee_finder_structured_array only merges FINDER arrays. A csv_to_structured_array_format or raw_to_structured_array output is a valid structured_array, but the merge action cannot reconcile its discovered columns with the finder schemas and drops those rows — silently, with no error. This is exactly the natural-looking build (web agent → raw_to_structured_array → merge with the finder output) and it quietly loses the entire web-agent source. Push the two sources into the same Contacts sheet separately and reconcile there. 8. Translate titles to English before you classify. On any international account a meaningful share of titles come back in the local language; the relevance LLM will grade them "low" simply because it cannot read them, and the gate silently drops the exact regional stakeholders the map exists to find. 9. Qualify LOOSELY. This is a map, not a shortlist — the point is to see the whole committee, including the influencers and specialist ICs who never appear in an outbound list. A tight per-person gate turns the map back into an outbound list and wastes the money you just spent on coverage. Let the relevance verdict be generous, and let a human (or the synthesis step in stakeholder_map_full.txt) do the narrowing. 10. Prove on ONE account first. Run a single account row, then open the Contacts sheet and read it: are all the buckets represented? Did dedupe collapse the cross-source overlap? Is anyone obviously missing? A wrong bucket definition, propagated across the list, is both expensive and invisible — the sheet still looks full. ================================================================================ 7. COMMON VARIATIONS ================================================================================ - Guaranteed C-suite coverage: a dedicated llm_web_agents instance whose only job is to return the CEO/CFO/COO/CPO LinkedIn URLs, in addition to a C-suite finder bucket. Executives are the people the map is most often judged on and the ones a title filter most often misses (their titles are the least standardized). - Coverage report per function: read each finder's no_of_employees on the account row and format them into a per-bucket count. Tells the rep which functions the account is thin in — and an unexpectedly empty bucket usually means the concept terms are wrong, not that the function doesn't exist. - Work email tail: person_work_email_waterfall + a verifier on the Contacts sheet, after the relevance filter. Only enrich the people who passed — email is the most expensive step per person here. - Still-at-company gate: insert the LinkedIn-experiences gate (still_at_company_outbound_gate.txt) after the scrape. Worth it when the map feeds outreach, since a web agent can surface a stale name. - Extra sources: get_employees_by_company_using_apollo or _sales_navigator as a third finder. Both merge cleanly with the native finder (they are finder arrays). Sales Nav is materially more expensive; add it only when the account is worth it. - Push to CRM: create/upsert the qualified contacts against the CRM account. This is where stakeholder_map_full.txt begins. ================================================================================ 8. FAILURE MODES AND MITIGATIONS ================================================================================ - Contacts sheet full of near-duplicates. The normalizer is missing, or the dedupe sits after the scrape. Mitigation: normalize → auto_dedupe_rows → scrape, in that order (best practice 6). Check ignore_case is true. - Merged list is smaller than the sum of the finders / the web-agent people never appear. You fed a non-finder array into merge_employee_finder_structured_array; it dropped them without erroring (best practice 7). Mitigation: push the web-agent source into the Contacts sheet on its own push_data_to_sheet. - A web-agent account produces no contact rows, quietly. Either the agent returned prose instead of URLs, or it found exactly ONE person — and csv_to_structured_array_format returns an empty list for a single comma-less value (its §4). Neither shows up as a failure. Mitigation: run_if guard (`contains "linkedin.com"`) on the csv action with continue_workflow_if_run_condition_not_met so a silent account doesn't halt the row, and comma-join two agent outputs (or use raw_to_structured_array) so a lone stakeholder survives. Same class of trap for a JS formatter that can return "" — it fails with "Missing input data"; return a sentinel instead (format_data_using_js_expression.txt). - One bucket returns "No employees found" and the row flips to has_failures. That is a zero-match, not an error, and it is expected on some buckets at some accounts. The chain does not halt. Mitigation: read CELL status, not row status. Only re-tune the bucket if a function you KNOW exists came back empty (that's a terms problem — best practice 2). - Everything grades "high" (or almost nothing does). The rubric is unanchored. Mitigation: give the qualify prompt the seller's actual buying context and 2-3 worked examples of high vs low. Re-cell-test on 5 people before re-running the sheet. - Cost came in far above expectation. buckets × cap × accounts, and/or the triage stage was skipped. Mitigation: best practices 3 and 4. The triage step is not optional at this width. ================================================================================ 9. RELATED USE CASES ================================================================================ - Stakeholder mapping — full account map (stakeholder_map_full.txt): this flow plus CRM contact extraction and an LLM-synthesized map per account. Build THIS one first; that one consumes it. - Find people at target companies (find_people_at_target_companies.txt): the cheap, broad-list sibling. If the user's list is long and the persona is single-function, that is the right flow, not this one. - Extract data from CRM (extract_data_from_crm.txt): pulling the contacts the account already has. - Still-at-company gate (still_at_company_outbound_gate.txt): the freshness check before the map feeds outreach. - Account research and scoring (account_research_and_scoring.txt): qualifies the ACCOUNT; often runs on the same account row before this flow decides whether the account is worth mapping. ================================================================================ This file is maintained manually. Last updated: 2026-07-13. Use case catalog: https://floqer.com/docs/use-case-catalog.txt Related action details: https://floqer.com/docs/action-detail/get_employees_by_company_using_floqer_native.txt https://floqer.com/docs/action-detail/merge_employee_finder_structured_array.txt https://floqer.com/docs/action-detail/auto_dedupe_rows.txt https://floqer.com/docs/action-detail/csv_to_structured_array_format.txt https://floqer.com/docs/action-detail/push_data_to_sheet.txt https://floqer.com/docs/action-detail/llm_web_agents.txt https://floqer.com/docs/action-detail/format_data_using_js_expression.txt