Agent #002 Β· Research & Market Intelligence

Buying Committee Mapper

🟑 Growth

Maps stakeholders in target accounts to buying roles (Economic Buyer, Champion, Technical, User, Influencer, Blocker)

Build time
0.5-1 day
Primary user
ABM Lead / AE
Primary outcome
Reduced single-threaded account risk
Deployable pack
Yes: full pack

Adam's take

Single-threading kills more deals than pricing does. This one pays for itself the first time a champion goes quiet and you already know who else to call.
Adam Woozeer, founder

Required tools

HubSpotClayn8n

Optional tools

LinkedIn Sales NavigatorApollo

Expected value

Reduces single-threaded deal risk; earlier multi-threading

Deployable workflow pack

The full pack includes the agent spec, deployment guide, architecture diagrams, n8n workflow skeleton, Clay template, HubSpot properties, JSON schemas, OpenAPI spec, cost and ROI calculators, demo data, QA checklist, and governance notes.

View pack on GitHub
Full deployment guide β†’Governance & failure modes β†’QA checklist β†’

How to deploy this agent

Single-threaded deals are one of the most common, and most preventable, causes of lost or stalled opportunities. This agent classifies every known contact at a target account into a buying role, scores committee coverage, and flags gaps before a deal stalls.

Core signals

Economic Buyer: Rule: seniority (C-level, Owner, VP) combined with a budget-owning department (Revenue Operations, Sales, Finance, Executive).
Technical Buyer: Rule: title contains an engineering/architecture/IT/security/data pattern.
User Buyer: Rule: department (Sales, Sales Development, Marketing) combined with manager/director/IC seniority.
Influencer: Rule: adjacent department (Operations, Procurement, PMO, Finance).
Champion / Blocker: Never assigned directly from title. Engagement signals (reply count, meeting count, rep-flagged objections) produce a candidate_champion or candidate_blocker label, held pending human confirmation in HubSpot.
Unknown: No rule matched and no behavioral signal fired β€” written honestly as unknown rather than guessed.

Input schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://deploytheagent.com/schemas/agent-002/input.schema.json",
  "title": "Buying Committee Mapper β€” Input",
  "description": "One contact record on a target account entering classification. Identity fields required; enrichment and engagement may be null.",
  "type": "object",
  "required": ["account_domain", "contact_id", "full_name", "is_target_account"],
  "additionalProperties": false,
  "properties": {
    "account_domain": { "type": "string" },
    "contact_id": { "type": "string", "description": "HubSpot contact ID, or 'sourced:<provider>:<ref>' for candidates not yet in CRM" },
    "full_name": { "type": "string", "minLength": 1 },
    "email": { "type": ["string", "null"] },
    "job_title": { "type": ["string", "null"] },
    "department": { "type": ["string", "null"] },
    "seniority": { "type": ["string", "null"], "enum": ["c_level", "vp", "director", "manager", "ic", "owner", null] },
    "is_target_account": { "type": "boolean", "description": "Gate: only true records are processed" },
    "is_sourced": { "type": "boolean", "default": false, "description": "True when the contact came from Apollo/Sales Nav and does not exist in CRM yet" },
    "unsubscribed": { "type": "boolean", "default": false },
    "engagement": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "replies_90d": { "type": ["integer", "null"], "minimum": 0 },
        "meetings_90d": { "type": ["integer", "null"], "minimum": 0 },
        "last_activity_date": { "type": ["string", "null"], "format": "date" },
        "rep_flagged_objection": { "type": "boolean", "default": false }
      }
    }
  }
}

Output schema

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://deploytheagent.com/schemas/agent-002/output.schema.json",
  "title": "Buying Committee Mapper β€” Output",
  "description": "Contact-level classification plus account-level coverage. Champion/Blocker appear only as candidate_* pending human confirmation; the rules engine may not emit them as confirmed roles.",
  "type": "object",
  "required": ["contacts", "account"],
  "additionalProperties": false,
  "properties": {
    "contacts": {
      "type": "array",
      "items": {
        "type": "object",
        "required": ["contact_id", "buying_committee_role", "buying_role_confidence", "buying_role_model_version", "review_required"],
        "additionalProperties": false,
        "properties": {
          "contact_id": { "type": "string" },
          "buying_committee_role": {
            "type": "string",
            "enum": [
              "economic_buyer",
              "technical_buyer",
              "user_buyer",
              "influencer",
              "unknown",
              "candidate_champion",
              "candidate_blocker"
            ],
            "description": "Confirmed 'champion' and 'blocker' values are written only by the human review workflow, never by this agent"
          },
          "buying_role_confidence": { "type": "number", "minimum": 0, "maximum": 1 },
          "buying_role_model_version": { "type": "string" },
          "classification_basis": { "type": "string", "description": "Which rule or signal produced the role, for audit" },
          "review_required": { "type": "boolean", "description": "True for candidates and sourced contacts" },
          "unreachable": { "type": "boolean", "default": false, "description": "Unsubscribed; mapped for completeness, excluded from outreach suggestions" }
        }
      }
    },
    "account": {
      "type": "object",
      "required": ["account_domain", "committee_coverage_score", "committee_gaps", "single_threaded_risk", "committee_scan_date"],
      "additionalProperties": false,
      "properties": {
        "account_domain": { "type": "string" },
        "committee_coverage_score": { "type": "integer", "minimum": 0, "maximum": 100 },
        "committee_gaps": {
          "type": "array",
          "items": { "type": "string", "enum": ["economic_buyer", "champion", "technical_buyer", "user_buyer"] }
        },
        "single_threaded_risk": { "type": "boolean" },
        "committee_scan_date": { "type": "string", "format": "date" },
        "engaged_contacts_90d": { "type": "integer", "minimum": 0 }
      }
    }
  }
}

Scoring / classification logic

Confirmed roles weigh 1.0 toward committee coverage; candidates and sourced-but-unconfirmed contacts weigh 0.5. An account is flagged single-threaded risk when two or fewer contacts have engaged in the last 90 days, independent of total contact count.

HubSpot CRM properties

buying_committee_role contact
Role written by Agent #002. Champion/Blocker set only via human confirmation workflow.
buying_role_confidence contact
0-1 classification confidence written by Agent #002.
buying_role_model_version contact
Role-mapping config version (+ prompt version if LLM fallback used).
committee_coverage_score company
0-100: filled required roles / required roles. Candidates count half.
committee_gaps company
Required roles with no mapped contact. Feeds Agent #005 sourcing.
single_threaded_risk company
True when <=1 contact engaged in last 90 days.
committee_scan_date company
Date of most recent committee mapping run.

n8n workflow skeleton

Weekly committee mapping: pull contacts on target accounts, classify buying roles by rule, detect champion/blocker candidates from engagement, score coverage, write agent-owned HubSpot fields, queue reviews. Configure every node marked CONFIGURE.

  1. Weekly mapping trigger
    CONFIGURE: default Tuesday 06:00 (after Agent #001's Monday scan). Add a webhook trigger for on-approval mapping if #001 is live.
  2. Load role-mapping config
    CONFIGURE: replace with your versioned role config built from real closed-won deals. Version string flows to buying_role_model_version.
  3. Fetch contacts on target accounts
    CONFIGURE: {{HUBSPOT_CREDENTIAL}}. Pull contacts on companies where icp_match_band = Target (or your manual target list). Include title, department, seniority, engagement, marketable status.
  4. Fetch sourced candidates from Clay
    CONFIGURE: Clay table export URL + {{CLAY_API_KEY}}. Sourced rows carry contact_id 'sourced:<provider>:<ref>' and is_sourced=true. Only sourced for accounts with committee_gaps.
  5. Validate + gate target accounts
    Drops non-target-account rows and structurally invalid rows; logs drops.
  6. Classify buying roles
    Rules-first classification. Champion/blocker emitted only as candidate_* from engagement signals; this node cannot output confirmed champion/blocker. LLM fallback for unmatched titles goes here if enabled (store prompt version in model_version).
  7. Score committee coverage
    Confirmed roles weigh 1.0, candidates and sourced weigh 0.5. Gaps = required roles with no confirmed contact. Single-threaded when <=1 engaged contact in 90d.
  8. Write contact fields (CRM contacts only)
    CONFIGURE: writes buying_committee_role, buying_role_confidence, buying_role_model_version for contacts whose contact_id is a HubSpot ID. Sourced contacts are NEVER written here; they go to the review queue.
  9. Write company coverage fields
    CONFIGURE: writes committee_coverage_score, committee_gaps, single_threaded_risk, committee_scan_date.
  10. Queue reviews (sourced + candidates)
    CONFIGURE: {{SLACK_CREDENTIAL}}, channel #committee-review. Lists sourced contacts pending CRM approval and champion/blocker candidates pending confirmation. Confirmation happens in HubSpot by a human; this workflow never creates contacts or confirms roles.
  11. Alert single-threaded Targets
    CONFIGURE: channel. Posts Target accounts with single_threaded_risk=true and their gaps.
  12. Write run log
    Audit: run date, config version, role distribution, review queue size. Alert if influencer share of classified contacts >40% (loose config symptom).

Agent-driven Clay integration (optional)

The default deployment path pulls Clay data through a static export URL, shown above. Teams already running Clay through its official CLI/MCP (clay-run/agent-plugins) can use this agent-driven path instead.

Clay MCP/CLI Integration β€” Agent #002

Pack Version: 1.0.0

Optional path. Use this instead of a manual `clay-template.csv` import when the mapper's contact-sourcing step should call Clay through the official CLI/MCP rather than a static export.

Source: `clay-run/agent-plugins` (github.com/clay-run/agent-plugins), the official Clay plugin for Claude Code, Cursor, and Codex. Bundles a `clay` CLI and an MCP server, authenticated with a `CLAY_API_KEY`.

When to use this

The mapper sources candidate stakeholder contacts per account and checks job-change status on already-mapped contacts (see the "Stale committee treated as current" failure mode in `agent-spec.md`). If that job-change check runs on a schedule against a live Clay table rather than a periodic export, the MCP/CLI path lets the workflow query Clay directly instead of waiting on the next CSV refresh.

If contact sourcing only happens at account-approval time with no ongoing job-change monitoring, the CSV template is enough.

Setup

  1. Create a Clay API key: Clay β†’ Settings β†’ Account.
  2. Store it as `CLAY_API_KEY` in the n8n credential store, scoped to this workflow. Never place it in the workflow JSON or the repo.
  3. Install the plugin in the build environment: `/plugin marketplace add clay-run/agent-plugins` then `/plugin install clay@clay-plugins` (Claude Code), or the Codex/Cursor equivalents in the plugin README.
  4. Verify before first production run: `clay whoami`. Exit 0 confirms the key resolves to the correct workspace. Exit 3 means the key is missing or invalid.

Scope for this agent

  • Read stakeholder candidate rows and job-change status: unattended, matches this agent's existing scoring logic.
  • Write role classification back to Clay for downstream reference: unattended, agent-owned columns only.
  • Create or update HubSpot contacts from Clay data: not allowed through this path. Contact creation stays gated behind the human-review step this agent's Governance section already requires (`no_auto_crm_writes_without_review`).

Governance addition

Log every Clay CLI/MCP call this workflow makes (table, action, row count, timestamp) alongside the existing `audit_trail_required` flag. Rotate `CLAY_API_KEY` if the n8n instance or its execution logs are ever exposed.

Fallback

The default deployment path stays the CSV import described in `clay-setup-guide.md`. This file documents the agent-driven alternative for teams already running Clay through its official CLI/MCP elsewhere in their stack.

Cost calculator

Cost Calculator β€” Agent #002

Pack Version: 1.0.0

Cost drivers

DriverUnitTypical priceNotes
Person enrichment (title/dept/seniority)credit/contact$0.05–$0.15Only for contacts with missing or junk titles
Contact sourcingcredit/candidate$0.10–$0.30Apollo/Sales Nav via Clay, gaps only, capped 3/gap
Job-change monitoringcredit/contact/mo$0.02–$0.05Mapped contacts only
n8ninstance$0–$50/moShared with other agents
LLM fallback (optional)tokens/title~$0.001Only unmatched titles

Assumptions: blended $0.07/credit; ~40% of CRM contacts need title enrichment; average 1.5 gaps per account at first run, falling to ~0.5 steady state; 3 sourced candidates per gap at $0.15.

Worked scenarios (steady state, monthly)

50 target accounts250 target accounts1,000 target accounts
Contacts on accounts (~6/account)3001,5006,000
Title enrichment (40% Γ— $0.07, 60-day cache β†’ half monthly)$4$21$84
Sourcing (0.5 gaps Γ— 3 candidates Γ— $0.15)$11$56$225
Job-change checks ($0.03/mapped contact)$9$45$180
n8n share$0–$25$25$25
Total~$24–$49~$147~$514

First run costs more: full enrichment pass plus ~1.5 gaps/account sourcing. At 250 accounts expect ~$180–$250 for the initial map.

What this replaces

Manual committee mapping at ~1 hour per account. Mapping 250 accounts by hand is 250 operator hours nobody actually spends, which is the real point: the work simply doesn't happen, and deals stay single-threaded.

ROI calculator

ROI Calculator β€” Agent #002

Pack Version: 1.0.0

Fill the blanks with your numbers. This agent's value splits into time saved and deal-risk reduction; model them separately and only claim the second if you can measure it.

Time savings

InputDefaultYours
Accounts needing committee maps/month40___
Hours per manual map1___
Hours per map with agent (review only)0.1___
Loaded hourly cost$75___
Monthly time saving40 Γ— 0.9 Γ— $75 = $2,700___

Honest caveat: most teams don't do 40 manual maps a month, they do 5 and skip the rest. If that's you, count the 5 real maps ($338) as saved time and treat the other 35 as new capability, priced by the risk model below.

Running cost

InputDefaultYours
Monthly Clay/enrichment (cost-calculator.md)$147 (250 accounts)___
Review time: ABM lead, 1 hr/week at $90$360___
Build amortized (1 day Γ— $600 / 12)$50___
Monthly cost$557___

Deal-risk reduction (measure before claiming)

Pull your last 4 quarters: win rate and slip rate on deals with 3+ engaged contacts versus 1. If your data shows the multi-threaded lift most teams find, the model is:

InputYours
Open pipeline on single-threaded Target accounts___
Historical win-rate delta (multi vs single-threaded)___ %
Share of flagged accounts you expect to multi-thread___ %
Risk-adjusted pipeline recovered= product of the three

No default numbers here. If you can't pull the cohort comparison, run the agent one quarter, then build the case from your own before/after.

Governance notes

Failure modes

QA checklist

QA Checklist β€” Agent #002

Pack Version: 1.0.0

Complete on a test portal or sandboxed property group. The reviewer must not be the builder.

Data validation

  • ☐ All five demo-data records pass `schemas/input.schema.json`
  • ☐ Workflow output on demo data matches `demo-data/sample-output.json` field for field
  • ☐ Output validates against `schemas/output.schema.json`
  • ☐ A non-target-account contact is gated out and logged
  • ☐ A record missing `job_title` classifies Unknown, does not fail the run

Classification

  • ☐ Dana Reyes (VP Revenue Operations) β†’ economic_buyer, 0.9
  • ☐ Marcus Webb (3 replies + 1 meeting) β†’ candidate_champion 0.7, review_required true, title-role noted in basis
  • ☐ Priya Nair (Senior Data Engineer) β†’ technical_buyer, 0.8
  • ☐ Jordan Blake ("Growth Ninja", unsubscribed) β†’ unknown 0.3, unreachable true
  • ☐ Elena Marsh (sourced) β†’ user_buyer, review_required true
  • ☐ The rules engine cannot emit confirmed `champion` or `blocker`: attempt to force it via config; the output schema rejects the record
  • ☐ Every record carries `classification_basis` and `buying_role_model_version`

Coverage

  • ☐ Acme scores 75 with gaps [champion, user_buyer] (candidates weigh 0.5, confirmed 1.0)
  • ☐ Gaps list only roles with no *confirmed* contact
  • ☐ Single-threaded flag: account with 1 engaged contact flags true; Acme (2 engaged) flags false
  • ☐ Blocker candidates do not add coverage weight

CRM safety

  • ☐ Sourced contacts (`sourced:*` IDs) are never written to HubSpot; they appear only in the review queue
  • ☐ Contact writes touch only the three `buying_role*` properties; company writes only the four committee properties (diff before/after)
  • ☐ Unsubscribed contacts are classified but excluded from sourcing/outreach suggestions
  • ☐ Private app scopes match the setup guide

Operations

  • ☐ Review queue posts to the agreed channel with basis text per candidate
  • ☐ Single-threaded alert posts with gaps listed
  • ☐ Run log records role distribution; Influencer >40% of classified contacts raises the loose-config alert
  • ☐ Job-change path: departed contact resets to Unknown and alerts
  • ☐ Rollback tested: workflow disabled, test property values cleared

Sign-off

RoleNameDate
Builder
Reviewer (not the builder)
ABM lead (review queue owner)

FAQ

How does the mapper determine buying roles without asking the contact directly?
It combines title and department data against a versioned role config with engagement signals (replies, meetings, flagged objections) already present in the CRM. Champion and Blocker only ever appear as candidates pending human confirmation.
What counts as a single-threaded account?
An account with two or fewer contacts who have engaged in the last 90 days, regardless of total contact count.
Does this agent reach out to unmapped stakeholders automatically?
No. It flags coverage gaps and queues sourced candidates for review. Sourcing additional contacts is Agent #005: Prospect List Builder.
Can a contact’s role change over time?
Yes β€” classification re-runs on a schedule for active target accounts rather than assigning a role once and leaving it static.

Related agents