USE_CASE_ID: stakeholder_map_full NAME: Stakeholder mapping — full account map (sourced + CRM, synthesized) CATEGORY: Account research and scoring The user wants an actual MAP per strategic account: who the buying committee is, who we already know, who we don't, which functions are uncovered, and who to engage first. Not a contact list — a synthesized picture of the account, with the contact list underneath it. Three things make this different from stakeholder_map_sourcing.txt, which it otherwise contains wholesale: 1. It merges two populations — people SOURCED from the web, and contacts the account already has in the CRM — on one Contacts sheet. 2. Qualification is deliberately loose, because the narrowing happens in synthesis, not per person. 3. The account row reads the finished Contacts sheet back and an LLM turns it into the map, which lands on a Final sheet. STATUS: SCAFFOLD. The shape below is taken from a live production build and the mechanics in §5 (the sub-sheet join, the record-key traps, the CRM extraction) are load-bearing and correct. The synthesis prompt and the Final-sheet schema in stage 6 are intentionally sketched — they are the part that gets tuned per customer, and they should be fleshed out here once a second build has proven a shape worth generalizing. Do not treat stage 6 as prescriptive. 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: - Named / enterprise accounts, where the deliverable is an account map a rep or AE will actually read before a meeting. - The user has a CRM with existing contacts on those accounts and wants sourced people reconciled against them ("who do we already know here?"). - They want a per-account verdict — coverage, gaps, entry points — not just rows. DO NOT USE WHEN: - The deliverable is a contact list → stakeholder_map_sourcing.txt. That flow is this one's stages 1-3; if the user doesn't need the CRM merge or the synthesized map, do not build the tail. It is the expensive half. - There is no CRM, or CRM contacts aren't wanted in the picture → stakeholder_map_sourcing.txt, plus the synthesis tail if they still want a map. - The user wants CRM contacts refreshed/verified, not a map → crm_contact_change_detection.txt or crm_pull_enrich_update.txt. ================================================================================ 2. INPUTS ================================================================================ - Account list, one row per account: account_id (CRM), account_name, domain. The account_id is what the CRM pull keys on and what the writeback targets — without it, use stakeholder_map_sourcing.txt. - Function buckets + seniorities (see stakeholder_map_sourcing.txt §2 — same input, same rules). - The CRM object + relationship to pull (e.g. Salesforce Contact where AccountId = the row's account_id). - What the map is FOR: the seller's product and the decision it needs. The synthesis LLM is only as good as this context. ================================================================================ 3. OUTPUTS ================================================================================ - Contacts sheet: every person at the account, from BOTH populations (sourced + CRM), deduped and loosely qualified, each tagged with its source and — for CRM rows — its CRM record id. - Final sheet: the map. One row per selected stakeholder (LinkedIn URL, account, engagement priority, and whatever activation fields the customer needs), plus the account-level synthesis. - Optional CRM writeback: contacts updated / created, and a "stakeholders mapped" flag on the account. ================================================================================ 4. WORKFLOW DESIGN ================================================================================ Three sheets. The Accounts sheet sources and (later) synthesizes; the Contacts sheet dedupes and qualifies both populations; the Final sheet holds the map and drives activation. [Accounts sheet] account_id + account_name + domain -> [ stakeholder_map_sourcing.txt stages 0-4 ] ──push──┐ finder × N buckets -> merge -> title triage │ web agent(s) │ -> CRM pull: contacts on this account ──JS──push─────────┤ v [Contacts sheet] one row per person (source = sourced | crm) -> normalize LinkedIn -> dedupe -> scrape -> title EN -> profile summary -> LOOSE qualify -> surface verdict as a column │ <── (the account row waits for this sheet to finish) ────┘ -> delay (or a second pass — see stage 5) -> lookup Contacts WHERE account = this row (find_all: true) -> JS: extract the qualified people from the records -> LLM: SYNTHESIZE THE MAP ← the scaffolded part -> raw_to_structured_array -> push v [Final sheet] one row per mapped stakeholder -> work email -> verify -> CRM / sequencer The awkward part of this design is the join: the Contacts sheet is a child fan-out that runs asynchronously, while the parent account row carries straight on. Stage 5 is about not getting that wrong. ================================================================================ 5. IMPLEMENTATION ================================================================================ STAGES 1-3 — Source, triage, push Exactly stakeholder_map_sourcing.txt stages 0-4. Read that file; it is not repeated here. One addition: tag every pushed row with its source ("sourced") on a source column, so the map can tell a person we found from a person we already knew. STAGE 4 — Pull the CRM contacts for this account Actions: salesforce_lookup_record (or hubspot_lookup_object) → format_data_using_js_expression → raw_to_structured_array → push_data_to_sheet. Config: - Look up the CRM's contact object filtered by the account relationship — e.g. Salesforce Contact, add_fields_and_their_values [{name: "AccountId", value: "{{input.account_id}}"}], exact_match true. - The lookup returns records as arrays of {responseId, responseStructureId, value} field triples, NOT plain objects. A JS formatter normalizes them into flat objects before raw_to_structured_array can fan them out. This is the responseId pattern; it is documented end-to-end, with the JS, in extract_data_from_crm.txt — use that, don't re-derive it. - Push into the SAME Contacts sheet as the sourced people, carrying the CRM record id (e.g. sf_contact_id), the existing email, and source = "crm". Why the same sheet: dedupe, qualification and the map all want ONE population. A CRM contact who is also found by the finder should be one row, marked as already-known — that is the single most valuable cell in the whole map. Note: the live build this file is drawn from keeps CRM contacts on their own sheet, because those rows need CRM-specific handling (record matching, field-level writeback that must not degrade existing data). That is a legitimate variation (§7) — but if the map is the deliverable, one sheet is the simpler build. STAGE 5 — Read the finished Contacts sheet back onto the account row Actions: delay_step → lookup_another_floqer_workflow_row → format_data_using_js_expression. THE RACE. push_data_to_sheet hands rows to the child sheet, which runs them asynchronously. The parent row does not wait. Look the Contacts sheet up immediately and you will read rows whose qualification cells are still empty — the lookup succeeds, the map is built from nothing, and no cell anywhere goes red. Two ways to handle it: (a) delay_step before the lookup. Set `time` to a string of seconds ("900" = 15 min) comfortably longer than the child chain takes for the widest account. Simple, in-flow, and what the live build does. It is a guess, and it is wrong on the tail: too short and you map a half-finished sheet, too long and every account row idles. (delay_step.txt §6 currently documents only CRM index lag; the child-sheet-settle wait is this second, legitimate use.) (b) Split the pass. Stop the account chain after the pushes, let the Contacts sheet drain, then Run Action on the lookup for the account rows. Deterministic, and the right choice for a large batch or a scheduled run. THE LOOKUP. - table_to_search: the Contacts sheet id. target_column: an INPUT column on that sheet (account_name or account_id). Action-output cells cannot be match keys — which is the reason the account fields have to be pushed onto every contact row in the first place. - find_all: true → `record` comes back as an ARRAY of full rows. Branch on `total` before reading it. READING THE RECORDS — two traps, both silent: - Every cell comes back keyed by its action's DISPLAY NAME, verbatim, spaces and caps included ("Job Title", "Profile Summary"). Not snake_case, not the action id. - A filter cell holds the filter's STATUS (`""` passed / `conditionNotMet` blocked), never the value it filtered on. So `record["Title relevance"] === "high"` is always false. Both are why stakeholder_map_sourcing.txt stage 6 surfaces the relevance verdict as its own format_data_using_js_expression column: the lookup can then read it by that formatter's display name. Do the same for every field the map needs (function, buying power, reason, summary). A JS formatter then filters the record array down to the qualified people and projects the fields the synthesis needs. STAGE 6 — Synthesize the map ← SCAFFOLD, tune per customer Actions: llm_models → raw_to_structured_array → push_data_to_sheet (Final sheet). Shape: - The LLM receives the qualified people for ONE account (name, title, function, buying power, reason, summary, already-in-CRM flag) and the seller's context. - It decides HOLISTICALLY, across the account, not per person — that is the whole reason this step exists rather than a per-person score. Prompt for content, not structure: what the map should SAY (who owns the problem, who signs, who blocks, who we already know, which function has nobody, where to enter), and put the shape in output_format. - Fields worth having (sketch, not gospel): coverage_summary, per-person engagement_priority (1/2/3) with a one-line why, gaps (functions with no credible contact), recommended_entry_point. - Ask for a bounded set (e.g. 25-40 people) — an unbounded map is not a map. Then raw_to_structured_array over the per-person part and push_data_to_sheet fans it into the Final sheet: one row per stakeholder, carrying account_id / account_name / domain / LinkedIn URL / engagement priority. Keep the account-level narrative (coverage, gaps, entry point) on the ACCOUNT row — it is one per account, not one per person. TO FLESH OUT LATER: the prompt itself, the field list, and whether the narrative belongs on the account row, on a per-account Final row, or both. Do not copy a customer's rubric into this file — copy the shape that survived two customers. STAGE 7 — Activate (optional) On the Final sheet: person_work_email_waterfall → verifier → CRM create/update or sequencer. Standard tail; nothing map-specific. Guard any CRM write while you are still building (best practice 4). ================================================================================ 6. BEST PRACTICES ================================================================================ 1. Everything in stakeholder_map_sourcing.txt §6 still applies — bucketed finders, short concept terms, triage before scrape, normalize-then-dedupe, always-on second source. This flow does not replace those rules; it inherits them. 2. One population, tagged by source. Sourced people and CRM contacts belong on the same sheet with a source column, so dedupe collapses the overlap and the map can say "we already know 4 of the 11 people who matter". Two separate sheets means the map cannot see that, which is the single most useful thing it could tell a rep. 3. Qualify loose, narrow in synthesis. The per-person gate should admit anyone plausibly on the committee. The LLM at stage 6 does the prioritization, with the whole account in front of it — which is exactly the judgment a per-person score cannot make. 4. Park a BLOCKER before every CRM write until the flow is proven. A format_data_using_js_expression that throws — `(function(){ throw "SF BLOCK: intentionally halted"; })()` — immediately upstream of a salesforce_update_record / create action stops the write from firing while you are still testing, and the halt is loud and obvious in the cell. Blank it (or delete the node) to arm the write. A run_if that can never be met does the same job but reads like a bug six weeks later; the throwing blocker says what it is. 5. Test the join, not just the actions. The failure mode unique to this flow is a lookup that runs before the child sheet has finished: every cell is green and the map is built on empty qualification fields. Cell-test the lookup on one account AFTER the Contacts sheet has visibly settled, and read what the record array actually contains before you wire the synthesis prompt to it. 6. Prove on one account, end to end, before the batch. This flow has the highest per-account cost in the library. ================================================================================ 7. COMMON VARIATIONS ================================================================================ - CRM contacts on their own sheet (what the live build does): when the CRM rows need their own treatment — matching a sourced person back to an existing record, verifying the person is still there, filling CRM fields without degrading what's already in them — give them a dedicated sheet and have the synthesis lookup read BOTH. More faithful to CRM hygiene, worse at answering "who do we already know". Pair with crm_contact_change_detection.txt. - Narrative map instead of rows: skip the per-person fan-out and keep the map as a single synthesized cell on the account row (a briefing a rep reads). Cheapest possible version of the tail. - Writeback: flag the account as mapped (salesforce_update_record on the account object, a boolean field) so the next run — or a scheduled re-map — can skip it. - Scheduled re-map: re-run quarterly and diff against the last map. The interesting output is the delta (new stakeholder, someone left, a gap that closed), not the map. - Still-at-company gate on the CRM population: CRM contacts rot faster than sourced ones. See crm_contact_change_detection.txt. ================================================================================ 8. FAILURE MODES AND MITIGATIONS ================================================================================ - The map is generic / built on nothing, but no cell failed. The lookup ran before the Contacts sheet finished (stage 5's race). Mitigation: lengthen the delay, or split the pass. Verify by reading the record array, not by reading cell colors. - The extracted people array is empty even though the sheet is full. You read a record key that doesn't exist — a snake_case name, an action id, or a filter cell (which holds status, not value). Mitigation: surface each needed field as its own formatter column on the Contacts sheet and read it by that display name (stage 5). - CRM contacts never arrive on the sheet. The lookup's records come back as responseId field triples, not objects; the JS didn't normalize them. Mitigation: use the responseId pattern from extract_data_from_crm.txt verbatim. - CRM records get overwritten with worse data. A writeback fired during a test run. Mitigation: best practice 4 (blocker node), and never enable a write on a chain you have not cell-tested end to end. - Duplicate stakeholders on the Final sheet. The dedupe key is the LinkedIn URL, and a CRM contact may arrive without one. Mitigation: resolve a LinkedIn URL for CRM rows before the dedupe (person_linkedin_url_by_email, or a web agent from name + company), or dedupe on a composite key built in JS. ================================================================================ 9. RELATED USE CASES ================================================================================ - Stakeholder mapping — multi-source contact coverage (stakeholder_map_sourcing.txt): the sourcing half of this flow, standalone. Build it first; most users want only that. - Extract data from CRM (extract_data_from_crm.txt): the responseId pattern stage 4 depends on. - CRM contact change detection (crm_contact_change_detection.txt): for keeping the CRM population honest. - Cross-table lookup via HTTP API Call (cross_floqer_table_lookup.txt): when the stage-5 lookup outgrows lookup_another_floqer_workflow_row (filtering on an output column, aggregating across rows). - Account research and scoring (account_research_and_scoring.txt): decides WHICH accounts earn a map. ================================================================================ 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/lookup_another_floqer_workflow_row.txt https://floqer.com/docs/action-detail/delay_step.txt https://floqer.com/docs/action-detail/salesforce_lookup_record.txt https://floqer.com/docs/action-detail/raw_to_structured_array.txt https://floqer.com/docs/action-detail/push_data_to_sheet.txt https://floqer.com/docs/action-detail/llm_models.txt https://floqer.com/docs/action-detail/format_data_using_js_expression.txt