# B2B Buying Committee: How to Map One on LinkedIn with an AI Agent

What a B2B buying committee is, who sits on it, and how to map one at a target account from LinkedIn – with an AI agent or in code.

Deals rarely hang on one person. The question is who the others are at your target account, and how to find them before the deal goes quiet.

> **The short version.** A B2B buying committee is the group of people at one account who shape a purchase: typically a champion who advocates for the change internally, an economic buyer who controls budget, technical and operational evaluators, the day-to-day users, a security, legal or procurement gate, and often an executive sponsor. There is no universal size – it grows with price, risk and company size. To map it, name the roles first, then find candidates for each: search the company's employees on LinkedIn by title and by how close they are to your network, read their profiles and recent posts as evidence, and mark each role as a guess with a confidence level until conversations confirm it. An AI agent or a script can run that research on your own LinkedIn account; reporting lines and budget authority are never on LinkedIn, so the map stays an informed hypothesis.

## What is a B2B buying committee?

A buying committee is everyone at the customer who can move, slow or stop a purchase – the people who evaluate, approve and live with the decision. Mapping it for one target account is what sales teams call account mapping (not to be confused with partner account mapping, which compares customer lists between companies).

| Role | What they care about | Clues on LinkedIn | Title keywords to search |
|---|---|---|---|
| Champion (advocate) | Getting the change through; mobilizes colleagues | Posts about the problem; a role in the affected team. A problem owner is only a *potential* champion until they advocate | Head of, Manager + the function |
| Economic buyer | Return, cost, priority | Budget-owning seniority in the function | VP, Director, C-level of the function |
| Technical or operational evaluator | Integration, security, implementation | Engineering, IT, operations, data roles | Engineer, Architect, IT, Operations |
| User or workflow representative | Day-to-day fit | Hands-on titles in the team | Specialist, Analyst, Representative |
| Security, legal or procurement gate | Risk, terms, vendor process | Security, legal, procurement departments | Security, Legal, Procurement, Compliance |
| Executive sponsor | Strategic fit, breaking deadlocks | Executive titles above the function | CEO, COO, CRO, CFO |

A few rules keep the map honest:

- **There is no universal size.** Committees grow with price, risk and company size.
- **One person can fill several roles** – at a smaller company the economic buyer may also be the champion.
- **A title match gives a candidate, not a role.** LinkedIn's own [sales terms](https://business.linkedin.com/sell/resources/sales-terms/buying-committee) define the champion as an advocate, and that shows in behavior, not in a job title.

## Why single-threaded deals stall

A deal with one contact rests on one person's calendar, priorities and job. If they go quiet, change roles or turn out not to hold the budget, the deal stalls with nobody else to call. **Multithreading in sales** means building relationships with several members of the committee, each on the questions they own, so the deal does not depend on any one of them.

## Where the map comes from

| Route | Best for | Watch out for |
|---|---|---|
| Sales Navigator, by hand | Reps who already have a seat and map a few accounts | Creating CRM contacts – with an optional stakeholder role on an opportunity – requires Advanced Plus and CRM Sync ([LinkedIn Help](https://www.linkedin.com/help/sales-navigator/answer/a170004)); the evidence-and-confidence map is still yours to maintain |
| Sales-engagement platforms with org-chart features | Teams that live in that platform, its database and their CRM – Amplemarket, for example, describes an Org Chart Skill and a deal contact gap analysis built on its own data and HubSpot | The map comes from the vendor's data; LinkedIn is an outreach channel there, not the source |
| LinkedIn research on your own account (Linked API) | Agents and scripts that query LinkedIn directly and write the map in the shape you need | Bounded by what your account can see; roles stay inferred |

Role-first mapping is not new – it is how good account executives already work. What changes with the last route is who does the legwork: an agent or a script runs the LinkedIn research, and you review the result.

## How to map a buying committee from LinkedIn

1. **Name the roles and title keywords** for this deal, using the table above. A RevOps tool and a security product have different committees.
2. **Pull candidates per role.**
   - Search the company's employees by `position` keyword, one search per keyword, optionally narrowed by location and by connection degree (`1st`, `2nd`, `3rd+`). One search returns up to 1,000 people.
   - Add the decision makers LinkedIn lists for the company – up to 20.

   The reference for both is in the docs: [company employees](/docs/action-st-retrieve-company-employees) and [decision makers](/docs/action-st-retrieve-company-dms).
3. **Read the evidence.** For the strongest candidates, open the profile (current position, experience) and recent posts.
4. **Assign roles with a confidence level** and the evidence behind each:
   - **high** – the role is stated or clearly shown: a matching title plus posts or experience that fit;
   - **medium** – the title fits and nothing contradicts it;
   - **low** – a title-keyword match only.

   A champion is confirmed only by what they do in conversations, never by title.
5. **Mark warm paths.** 1st-degree candidates you can message directly. For 2nd-degree candidates, look up your mutual connections – the label shows proximity, not a named introducer.
6. **Find the gaps** – roles with no candidate – and decide whom to research next.
7. **Write the map** to your CRM or a sheet. A person reviews it before any outreach.

## Run it with an AI agent

The research runs from a plain-language instruction. Connect an assistant such as Claude to your account through the [MCP server](/mcp/available-tools) – `fetch_company` with employees and decision makers, `fetch_person` for profiles and posts – the agent-friendly [CLI](/cli), or a ready-made [skill](/skills). Setup is covered in [giving an AI agent access to LinkedIn](/guides/linkedin-ai-agent).

A runnable instruction. It is a starting point, not a record of an observed run:

```text
Map the buying committee at Example Target (https://www.linkedin.com/company/example-target)
for a RevOps analytics product.

Roles and title keywords:
- Champion (candidate): "Head of Revenue Operations", "RevOps Manager"
- Economic buyer: "VP Sales", "Chief Revenue Officer"
- Technical evaluator: "Salesforce Administrator", "Data Engineer"
- User: "Sales Operations Analyst"
- Security, legal or procurement: "Procurement", "Security"

For each keyword, search the company's employees by position and note each person's
connection degree. Also list the decision makers LinkedIn shows for the company.
For the top 3 candidates per role, read their profile and posts from the last 90 days.

Return a table: name, profile URL, inferred role, confidence (high / medium / low)
with the evidence and links behind it, connection degree. Then list the roles with
no candidate. Do not send any invitations or messages – stop for my review.
```

The pause before outreach is part of your instruction, not a built-in approval queue – keep it explicit.

## Or build it in code

The same research as a script. It runs one employee search per title keyword, adds LinkedIn's decision makers, reads recent posts for the closest candidates, and writes `committee-map.json` with candidates, confidence, gaps and any searches that failed. The role rules are deterministic – change them for your deal.

```typescript
import LinkedApi from '@linkedapi/node';
import { writeFileSync } from 'node:fs';

const linkedapi = new LinkedApi({
  linkedApiToken: 'your-linked-api-token',
  identificationToken: 'your-identification-token',
});

const COMPANY_URL = 'https://www.linkedin.com/company/example-target';
const PROBLEM_TERMS = /pipeline|forecast|crm/i;
const NINETY_DAYS_MS = 90 * 24 * 60 * 60 * 1000;
const ROLES = [
  { role: 'champion (candidate)', keywords: ['Head of Revenue Operations', 'RevOps Manager'] },
  { role: 'economic buyer', keywords: ['VP Sales', 'Chief Revenue Officer'] },
  { role: 'technical evaluator', keywords: ['Salesforce Administrator', 'Data Engineer'] },
  { role: 'user', keywords: ['Sales Operations Analyst'] },
  { role: 'security, legal or procurement', keywords: ['Procurement', 'Security'] },
];

interface TCandidate {
  role: string;
  name: string;
  profileUrl: string;
  headline: string;
  connectionDegree: string | null;
  confidence: 'high' | 'medium' | 'low';
  evidence: Array<string>;
}

async function employeesFor(keyword: string) {
  const workflow = await linkedapi.fetchCompany.execute({
    companyUrl: COMPANY_URL,
    retrieveEmployees: true,
    employeesRetrievalConfig: { limit: 25, filter: { position: keyword } },
  });
  const { data, errors } = await linkedapi.fetchCompany.result(workflow.workflowId);
  return { employees: data?.employees ?? [], errorTypes: errors.map((error) => error.type) };
}

async function decisionMakers() {
  const workflow = await linkedapi.fetchCompany.execute({ companyUrl: COMPANY_URL, retrieveDMs: true });
  const { data, errors } = await linkedapi.fetchCompany.result(workflow.workflowId);
  if (errors.length > 0) console.warn('decision makers', errors.map((error) => error.type));
  return (data?.dms ?? []).map((dm) => ({
    name: dm.name,
    profileUrl: dm.publicUrl,
    headline: dm.headline,
    connectionDegree: dm.viewerState.connectionDegree,
  }));
}

async function recentProblemPost(profileUrl: string) {
  const workflow = await linkedapi.fetchPerson.execute({
    personUrl: profileUrl,
    retrievePosts: true,
    postsRetrievalConfig: { limit: 5, since: new Date(Date.now() - NINETY_DAYS_MS).toISOString() },
  });
  const { data, errors } = await linkedapi.fetchPerson.result(workflow.workflowId);
  if (errors.length > 0) console.warn(profileUrl, errors.map((error) => error.type));
  return data?.posts?.find((post) => PROBLEM_TERMS.test(post.text ?? ''));
}

async function mapCommittee() {
  const candidates: Array<TCandidate> = [];
  const failedSearches: Array<{ role: string; keyword: string; errorTypes: Array<string> }> = [];
  for (const { role, keywords } of ROLES) {
    for (const keyword of keywords) {
      const { employees, errorTypes } = await employeesFor(keyword);
      if (errorTypes.length > 0) failedSearches.push({ role, keyword, errorTypes });
      for (const employee of employees) {
        const isTitleMatch = employee.headline.toLowerCase().includes(keyword.toLowerCase());
        candidates.push({
          role,
          name: employee.name,
          profileUrl: employee.publicUrl,
          headline: employee.headline,
          connectionDegree: employee.viewerState.connectionDegree,
          confidence: isTitleMatch ? 'medium' : 'low',
          evidence: [`Matched "${keyword}" in employee search; headline: ${employee.headline}`],
        });
      }
    }
  }

  const closest = candidates.filter(
    (candidate) => candidate.confidence === 'medium' && (candidate.connectionDegree === '1st' || candidate.connectionDegree === '2nd'),
  );
  for (const candidate of closest) {
    const post = await recentProblemPost(candidate.profileUrl);
    if (post) {
      candidate.confidence = 'high';
      candidate.evidence.push(`Recent post on the problem: ${post.url}`);
    }
  }

  const emptyRoles = ROLES.map(({ role }) => role).filter((role) => !candidates.some((candidate) => candidate.role === role));
  const isFullySearched = (role: string) => !failedSearches.some((search) => search.role === role);
  const map = {
    company: COMPANY_URL,
    candidates,
    decisionMakers: await decisionMakers(),
    gaps: emptyRoles.filter(isFullySearched),
    incompleteRoles: emptyRoles.filter((role) => !isFullySearched(role)),
    failedSearches,
  };
  writeFileSync('committee-map.json', JSON.stringify(map, null, 2));
}

mapCommittee();
```

```python
import json
import re
from datetime import datetime, timedelta, timezone

from linkedapi import FetchCompanyParams, FetchPersonParams, LinkedApi, LinkedApiConfig

linkedapi = LinkedApi(
    LinkedApiConfig(
        linked_api_token="your-linked-api-token",
        identification_token="your-identification-token",
    )
)

COMPANY_URL = "https://www.linkedin.com/company/example-target"
PROBLEM_TERMS = re.compile(r"pipeline|forecast|crm", re.I)
ROLES = [
    {"role": "champion (candidate)", "keywords": ["Head of Revenue Operations", "RevOps Manager"]},
    {"role": "economic buyer", "keywords": ["VP Sales", "Chief Revenue Officer"]},
    {"role": "technical evaluator", "keywords": ["Salesforce Administrator", "Data Engineer"]},
    {"role": "user", "keywords": ["Sales Operations Analyst"]},
    {"role": "security, legal or procurement", "keywords": ["Procurement", "Security"]},
]


def degree(person):
    return person.viewer_state.connection_degree if person.viewer_state else None


def employees_for(keyword):
    workflow = linkedapi.fetch_company.execute(
        FetchCompanyParams(
            company_url=COMPANY_URL,
            retrieve_employees=True,
            employees_retrieval_config={"limit": 25, "filter": {"position": keyword}},
        )
    )
    result = linkedapi.fetch_company.result(workflow.workflow_id)
    employees = (result.data.employees if result.data else None) or []
    return employees, [error.type for error in result.errors or []]


def decision_makers():
    workflow = linkedapi.fetch_company.execute(FetchCompanyParams(company_url=COMPANY_URL, retrieve_dms=True))
    result = linkedapi.fetch_company.result(workflow.workflow_id)
    if result.errors:
        print("decision makers", [error.type for error in result.errors])
    dms = (result.data.dms if result.data else None) or []
    return [
        {"name": dm.name, "profileUrl": dm.public_url, "headline": dm.headline, "connectionDegree": degree(dm)}
        for dm in dms
    ]


def recent_problem_post(profile_url):
    since = (datetime.now(timezone.utc) - timedelta(days=90)).strftime("%Y-%m-%dT%H:%M:%SZ")
    workflow = linkedapi.fetch_person.execute(
        FetchPersonParams(
            person_url=profile_url,
            retrieve_posts=True,
            posts_retrieval_config={"limit": 5, "since": since},
        )
    )
    result = linkedapi.fetch_person.result(workflow.workflow_id)
    if result.errors:
        print(profile_url, [error.type for error in result.errors])
    posts = (result.data.posts if result.data else None) or []
    return next((post for post in posts if PROBLEM_TERMS.search(post.text or "")), None)


def map_committee():
    candidates = []
    failed_searches = []
    for entry in ROLES:
        for keyword in entry["keywords"]:
            employees, error_types = employees_for(keyword)
            if error_types:
                failed_searches.append({"role": entry["role"], "keyword": keyword, "errorTypes": error_types})
            for employee in employees:
                headline = employee.headline or ""
                candidates.append({
                    "role": entry["role"],
                    "name": employee.name,
                    "profileUrl": employee.public_url,
                    "headline": headline,
                    "connectionDegree": degree(employee),
                    "confidence": "medium" if keyword.lower() in headline.lower() else "low",
                    "evidence": [f'Matched "{keyword}" in employee search; headline: {headline}'],
                })

    for candidate in candidates:
        if candidate["connectionDegree"] not in ("1st", "2nd") or candidate["confidence"] != "medium":
            continue
        post = recent_problem_post(candidate["profileUrl"])
        if post:
            candidate["confidence"] = "high"
            candidate["evidence"].append(f"Recent post on the problem: {post.url}")

    empty_roles = [entry["role"] for entry in ROLES if not any(c["role"] == entry["role"] for c in candidates)]
    failed_roles = {search["role"] for search in failed_searches}
    committee_map = {
        "company": COMPANY_URL,
        "candidates": candidates,
        "decisionMakers": decision_makers(),
        "gaps": [role for role in empty_roles if role not in failed_roles],
        "incompleteRoles": [role for role in empty_roles if role in failed_roles],
        "failedSearches": failed_searches,
    }
    with open("committee-map.json", "w") as f:
        json.dump(committee_map, f, indent=2)


map_committee()
```

Every call is asynchronous – `execute` starts the work and returns a workflow ID, `result` waits for it. Action failures, such as LinkedIn refusing a search, come back in `errors`; critical request or account failures raise a `LinkedApiError` instead ([handling results](/sdks/handling-results#handling-critical-errors)). Roles with no candidate and at least one failed search go under `incompleteRoles`, never under gaps, and `failedSearches` records every failed search – an unfinished search is not evidence that nobody holds the role. The confidence rules are deliberately conservative: a title match alone is low or medium, and only a matching title plus a recent post on the problem becomes high. Whether someone is a champion is left to your conversations.

## Multithreading without noise

- **Start with your strongest relationship** and ask how the decision actually gets made – that conversation corrects the map faster than any search.
- **Write to each role about what it owns.** The economic buyer cares about return and priority, the evaluator about integration, the user about daily work. One pitch to everyone reads as spam.
- **Never imply a colleague shared anything** they did not.
- **Choose the right first touch.** Check [the connection degree](/guides/linkedin-connection-degrees) to decide between an invitation and a message, and keep first touches reviewed – see [AI for sales prospecting](/guides/ai-sales-prospecting-linkedin).
- **Watch who is already engaging.** People reacting to your posts or viewing your profile may already be on the committee – see [LinkedIn buying signals](/guides/linkedin-buying-signals).

## Limits

- **Reporting lines and budget authority are not on LinkedIn.** Every role is inferred until the buyer confirms it.
- **Titles are self-reported** and vary between companies.
- **Employee lists are bounded** by what LinkedIn shows your account.
- **A 2nd-degree label is proximity**, not an introduction.
- **No emails or phone numbers** – Linked API works with what LinkedIn shows.
- **Pace and limits.** Profile views and searches count toward per-action limits by category and period ([limits API](/docs/admin-limits)). The account runs in a dedicated cloud browser at a human pace.

Pulling a company's employee list as data, rather than mapping a committee, is covered in the [company scraper guide](/guides/linkedin-company-scraper).

## Frequently Asked Questions (FAQ)

#### Who is on a B2B buying committee?

Typically a champion, an economic buyer, technical and operational evaluators, users, a security, legal or procurement gate, and often an executive sponsor. One person can hold several of these roles.

#### How big is a typical buying committee?

There is no universal number. It grows with the price of the purchase, the risk it carries and the size of the company.

#### How do you find decision makers in a company on LinkedIn?

Search the company's employees by title and seniority keywords, check the decision makers LinkedIn lists for the company, then confirm each candidate on their profile and recent posts.

#### What is multithreading in sales?

Building relationships with several members of the buying committee, so a deal does not depend on one contact's availability or authority.

#### Can an AI agent map a buying committee?

Yes, for the research: an agent can search employees, read profiles and posts and draft the map on your own account. The roles stay inferred, and a person reviews the map before any outreach.

## Build it in, or hand it to an agent

Build the research into your own tools with the REST API, the [Node and Python SDKs](/sdks) or the [CLI](/cli). Or have an AI agent run it out of the box through the [MCP server](/mcp/available-tools), the agent-friendly CLI or a ready-made [skill](/skills). Plans start at $49 a seat a month billed annually – see [pricing](/pricing).

*Facts verified October 12, 2026 – Sales Navigator CRM contact creation against LinkedIn Help; Amplemarket's org-chart and gap-analysis features against its live guide on that date.*
