Documentation

Kairon MCP setup

Connect Kairon to your AI assistant and it can search LinkedIn, build lists, run campaigns and answer replies for you. Setup takes about a minute.

Server URLhttps://app.heykairon.com/mcp

Getting started

  1. Step 1

    Create a Kairon account

    Sign up and finish onboarding. It takes about 10 minutes and you need an active plan.

  2. Step 2

    Connect a LinkedIn seat

    In Kairon, go to Settings and then Connections. Every tool acts as that seat, so nothing works until there is one.

  3. Step 3

    Get your MCP server URL

    It is the same for every account, and it is at the top of this page. You will paste it in the next step.

  4. Step 4a

    Connect with OAuth (Claude web and desktop)

    Open Settings, then Connectors, then Add custom connector. Paste the server URL and connect. Claude sends you to the Kairon sign-in once to approve it.

  5. Step 4b

    Connect from a terminal (Claude Code, Cursor, scripts)

    Run either of these, then the same sign-in as above. The Kairon CLI runs the same tools from a script or a cron job; its commands are documented at /cli.

    claude mcp add --transport http kairon https://app.heykairon.com/mcpnpm i -g @kairon/cli && kairon login
  6. Step 5

    Start asking

    You never name a tool yourself. Describe what you want and the assistant picks the tools it needs.

    • Find agencies in Spain hiring an SDR this month and save them to a list called Q3.
    • How is the Q3 campaign doing?
    • Who answered me this week, and what did they say?

Tools

Everything an assistant can do once it is connected. If something is not on this list, it cannot do it.

This list is generated from the server on every release, so it cannot fall behind. The text stays in English because that is what your assistant reads.

List building

Who should we talk to, and which list are they in?

filter_resolve

Resolve a Sales Navigator filter

  • read-only
  • idempotent
  • open-world

Resolve a human-readable Sales Navigator filter value (location, industry, function) to a LinkedIn id, echoing the canonical label so a wrong match is visible. Cached globally; pass the returned urn to search_sales_navigator, or let it resolve names for you.

Parameters: filter_resolve
NameTypeWhat it is for
typerequired"location" | "industry" | "function" | "current_company" | "past_company" | "company_location" | "school" | "past_role" | "groups" | "persona" | "technologies" | "account_lists" | "lead_lists" | "postal_code"
queryrequiredstring

icp_get

Get an ICP

  • read-only
  • idempotent

Read one ICP and its latest (active) version by icpId.

Parameters: icp_get
NameTypeWhat it is for
icpIdrequiredstring

icp_list

List ICPs

  • read-only
  • idempotent

List this org's ICPs, each with its latest (active) version's criteria and conditions.

Takes no parameters.

linkedin_fetch

Fetch a LinkedIn profile or company

  • read-only
  • idempotent
  • open-world

Fetch a LinkedIn person OR company through the caller's connected account — whichever the target addresses. Address it with target{ type, value } where type is url, public_identifier, or provider_id (pass whichever you have). The kind is detected from the target: a /in/ URL or a bare slug is a person, a /company/ URL is a company. Pass kind: 'company' for a company addressed by universal name or numeric id. Returns { kind, profile | company }; complete data, metered + cached (refresh: true forces a live read).

Parameters: linkedin_fetch
NameTypeWhat it is for
targetrequiredobject
kind"profile" | "company"Override auto-detection. Required for a company addressed by universal name or numeric id.
refreshboolean

list_add

Add members to a list

  • write

Add members to a list ({ asset, id, items }, ≤500 items). Each item is { memberId } (a lead or company already in the org), { url } (a pasted LinkedIn /in/ or /company/ URL — INSTANT and FREE, zero provider calls: creates or converges on a bare member that can be resolved later), OR { snapshot } (a search hit — pass the hit as-is, providerId required — which creates/dedups a born-resolved member so the saved list shows the person or company, not a bare id). Per-item results: added | already_member | invalid | not_found. Re-adding a current member converges (already_member). This is how you assemble an audience after a search: pass the result hits straight in.

Parameters: list_add
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
itemsrequiredobject[]

list_create

Create a list

  • write

Create a named list — the campaign targeting unit. asset: 'leads' for a list of people, asset: 'companies' for a list of companies (company-first targeting: build the company set, then find people inside it — pass the finished list's id to search_sales_navigator as companyLists to do that second step). Names are unique per org and per asset, case-insensitive; a taken name fails with the existing list id (no silent merge). Returns the new list with a member count of 0. Build membership with list_add.

Parameters: list_create
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
namerequiredstring

list_delete

Delete a list

  • write
  • destructive
  • idempotent

Soft-delete a list by asset + id: it disappears from list_list with its memberships, the member leads/companies are untouched (they stay in the org pool, in their other lists, with their qualification state intact), and the name is immediately free for reuse.

Parameters: list_delete
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring

list_get

Get one list

  • read-only
  • idempotent

One list by asset + id: its name, live member count, and created date. Out-of-org or deleted ids are not found. To read who is IN it, use list_members.

Parameters: list_get
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring

list_list

List lists

  • read-only
  • idempotent

Read this org's lists of one asset, keyset-paginated, newest first, each with its live member count. q matches the list name (case-insensitive). Page with the returned cursor. Deleted lists never appear.

Parameters: list_list
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
qstring
cursorstring
limitinteger= 25

list_members

Read a list's members

  • read-only
  • idempotent

One keyset page of a list's members ({ asset, id, q?, cursor?, limit? }), newest-added first. Each row is { id, title, subtitle, location, linkedinUrl } — the id removes it with list_remove, the URL reads it with linkedin_fetch. q filters case-insensitively over the member's display fields (name, headline, company, location). Page with the returned cursor.

Parameters: list_members
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
qstring
cursorstring
limitinteger= 25

list_remove

Remove members from a list

  • write

Remove members from a list ({ asset, id, memberIds }). Deletes only the membership — the leads/companies stay in the org pool and in their other lists, and can be re-added later. Returns how many memberships were removed.

Parameters: list_remove
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
memberIdsrequiredstring[]

list_update

Rename a list

  • write

Rename a list ({ asset, id, name }). Same uniqueness rule as create: a name already taken by another live list of that asset fails with that list id.

Parameters: list_update
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
namerequiredstring

profile_posts

A profile's recent posts

  • read-only
  • idempotent
  • open-world

A page of a profile's recent posts (transient — not persisted). Address the person with target{ type, value } (url, public_identifier, or provider_id). Use cursor to page; one page is one metered action.

Parameters: profile_posts
NameTypeWhat it is for
targetrequiredobject
cursorstring

search_people

Search people (classic)

  • read-only
  • idempotent
  • open-world

Classic LinkedIn people search — works WITHOUT a Sales Navigator seat. Pass keywords and/or structured filters: id-resolved filters ({type,query} over location/industry/company/past_company/school/service — pass human-readable NAMES, resolved to ids server-side — a name LinkedIn does not recognize REFUSES the search with CHANNEL_ACTION_FILTER_UNRESOLVED naming it, rather than running without that filter and quietly returning a wider audience; re-issue with a name LinkedIn uses, or drop the filter if you meant the broader search), networkDistance (1|2|3), profileLanguage (ISO codes), openTo, connectionsOf/followersOf (member ids from people.suggest), advancedKeywords (first/last/title/company/school). Or paste a search url to run it verbatim. Transient hits + a cursor; one page is one metered action. To persist a hit, call profile_fetch on it.

Parameters: search_people
NameTypeWhat it is for
urlstring
keywordsstring
cursorstring
limitintegerPage size for paging (default 10). Classic search is clamped to 50 provider-side (Sales Nav allows 100). Set by "Load all", not by the agent.
filtersobject[]
networkDistance1 | 2 | 3[]Connection degree: 1 (1st), 2 (2nd), 3 (3rd+). LinkedIn native filter CONNECTIONS.
profileLanguagestring[]Profile languages as 2-letter ISO-639-1 codes (e.g. ["en","es"]).
openTo"proBono" | "boardMember"[]
connectionsOfstring[]Member ids whose connections to search within (from people.suggest).
followersOfstring[]Member ids whose followers to search within (from people.suggest).
advancedKeywordsobject

search_sales_navigator

Search (Sales Navigator)

  • read-only
  • idempotent
  • open-world

Search LinkedIn Sales Navigator over people OR companies. This is the expert path: prefer it over classic keyword search whenever a structured filter fits the intent — filters are precise where keywords are noisy.

Read the full description

HOW FILTERS WORK • filters are human-readable, id-resolved dimensions { type, query, mode? }. You pass a NAME (e.g. { type: "industry", query: "Financial Services" }); the server resolves it to a LinkedIn id for you — never pass ids. mode is "include" (default) or "exclude". • Phrase each query the way LinkedIn names things: canonical industry names ("Financial Services", not "fintech"), real place names ("Greater Madrid Metropolitan Area" or the country, not a neighborhood). Resolution takes the single best typeahead match. • Every other dimension is a typed field (seniority, role, companyHeadcount, …) — read each field's description for when to use it. COMBINING FILTERS — this determines your results: • Multiple values of the SAME dimension are OR'd (industry ["Financial Services","Banking"] = either). • DIFFERENT dimensions are AND'd (industry AND location AND seniority). Each dimension you add NARROWS the result set — add them one intent at a time and stop before you over-constrain to zero. EXCLUSIONS — your primary tool for removing who does NOT belong. Almost every dimension takes an exclude side, and you should reach for it whenever a broad filter sweeps in an obvious wrong segment (a positive filter alone often cannot express "leaders but NOT recruiters"): • role.exclude — drop unwanted current-title words: e.g. searching "growth leaders" but flooded with agencies/recruiters → role:{ include:[…], exclude:["recruiter","talent","agency","assistant","intern","student"] }. This is usually the highest-leverage exclusion. • seniority.exclude — drop levels you do not want (e.g. exclude ["entry_level","training","unpaid"] when you want decision-makers). • id-resolved filters with mode:"exclude" — drop a whole industry, company, or location: e.g. { type:"industry", query:"Staffing and Recruiting", mode:"exclude" }, or exclude a competitor via { type:"current_company", query:"Acme", mode:"exclude" }. • Use exclusions proactively in your FIRST search when the ask implies an obvious anti-segment, and reactively when the echoed hits show a recurring wrong type — name the exclusion in your read-back and apply it next turn. LOCATIONS — expand regions to countries: • A region/continent does NOT resolve as one id ("LATAM", "Europe", "APAC" will fail). Emit ONE location filter PER country (same dimension repeated = OR). LATAM → Argentina, Brazil, Mexico, Colombia, Chile, Peru, Ecuador, Uruguay, Costa Rica, Panama (+ more as relevant). A single named country → just that country. Never invent locations the user did not name. PEOPLE — TITLES (role) beat the coarse ladder. This is the highest-leverage filter; get it right: • role matches ONLY the CURRENT job-title field (title-scoped keywords, include/exclude) — NOT the whole profile. That is why it beats keywords for any title intent: trust it, do not fall back to keywords for a title. (keywords matches every text on the profile, so it is far noisier.) • For a PAST title ("used to be a CFO", "previously a founder"), use the id-resolved past_role filter — filters:[{ type:"past_role", query:"Chief Financial Officer" }] (JOB_TITLE typeahead; supports mode:"exclude"). role is current-only; past_role is the only way to filter on prior titles. • A job TITLE goes in role (title-keyword include/exclude). Use seniority (cxo, vice_president, director…) ONLY for a pure level with no title intent — for "leaders of X" the seniority block below recalls far better. • Exec / C-suite titles and founder are SINGLE-TITLE pills: one role.include entry per variant, standing alone — "CEO", "Chief Executive Officer", "Founder", "Fundador", "Fundadora", "Cofounder", "Co-founder". Never wrap these in a seniority block. • SENIORITY BLOCK — THE DEFAULT FOR ANY LEADERSHIP INTENT. The moment you read the ask as *the people who LEAD or DECIDE on some area* — "heads of growth", "payment managers", "gerentes de cobranzas", "marketing decision-makers", "whoever owns billing", "the person in charge of ops" — emit ONE seniority-block pill: a marker AND a parenthesized OR-list of ROLE synonyms, e.g. <<SENIORITY_BLOCK_EN>> AND ("payments" OR "billing" OR "collections"). The marker expands server-side to a broad leadership OR-block (head/VP/director/manager/chief…), so you recall the WHOLE pyramid — a hand-enumerated title list always misses the variants real people actually use. Put only role words in the OR-list (never seniority words), quote each term, AND/OR uppercase. At most ONE seniority-block pill per search. • Reach for the block by DEFAULT: whenever the audience is leaders/owners/decision-makers of a domain, the block belongs in the search. B2B outbound almost always targets a decision-maker, so a people search WITHOUT a seniority block should be the rare exception, and you should be able to name why. The only real exceptions: (a) the ask is exec/C-suite/founder ONLY → single-title pills instead (see above); (b) the ask names a SPECIFIC non-leadership title with no leadership intent ("data engineer", "recruiter", "designer") → plain title pills; (c) the ask is a PURE level with no domain ("all VPs, any function") → seniority alone. Anything that reads as "the leaders of X" gets the block — do not settle for a bare role keyword and do not hand-enumerate the ladder. • Marker by language: Spanish geo/terms (Mexico, Spain, LATAM, "gerente","jefe") → <<SENIORITY_BLOCK_ES_EN>> with Spanish+English role synonyms; Brazil / Portuguese terms → <<SENIORITY_BLOCK_PT_EN>> with Portuguese+English; otherwise → <<SENIORITY_BLOCK_EN>> (English only). • Top-of-pyramid: when asking for leaders of a department, ALSO add the matching C-title as its own single-title pill — HR→CHRO, Marketing→CMO, Finance→CFO, Tech/Engineering→CTO, Operations→COO, Product→CPO. CLOSE THE LOOP — every search echoes back the filters AS RESOLVED (query → label, or a failure) plus a hit count. ALWAYS read it: • Wrong resolution (you said "Apple", it resolved to the wrong entity) → re-issue with a more specific query. • A filter that did not resolve REFUSES the whole search (CHANNEL_ACTION_FILTER_UNRESOLVED, naming which one) — it is never dropped, because running without it would silently widen your audience. Re-issue with a name LinkedIn uses (canonical industry name, real place name), or drop the filter yourself if you meant the broader search. • Targeting people at companies you already hold as a Kairon company list? Pass companyLists: [{ id }] (mode:"exclude" to suppress a set) — NOT account_lists, which is Sales Navigator's own saved lists. Never enumerate the companies as current_company filters when you have the list id: that costs a call per name and caps at 40. • Zero or too-few hits → loosen: drop the narrowest dimension, widen a range, or broaden the role OR-list. • Too many hits, OR the hits include a recurring wrong segment (recruiters, students, the wrong industry) → add an EXCLUSION (see EXCLUSIONS) before adding more positive filters — it removes the noise without shrinking the real audience. COMMON TRAPS • "Startup" / "scale-up" is NOT a filter — approximate it by SIZE, and the size field DEPENDS ON CATEGORY. On a PEOPLE search: small companyHeadcount buckets (["1-10","11-50","51-200"]), optionally companyType: ["privately_held"]. On a COMPANIES search: small headcount buckets (["1-10","11-50","51-200"]) — a companies search has NO company-type facet, so do NOT send companyType (nor companyHeadcount) on it; both are people-search-only and the search will reject them. • Industry uses LinkedIn's canonical names in an industry filter ("Financial Services", "Banking" — NOT "fintech"); add related categories as OR values. A business model / product ("fintech", "SaaS", "marketplace") describes WHAT a company does → that is keywords, not industry. • keywords is a LAST resort — ONLY skills/tools/tech the user EXPLICITLY named (e.g. "Workday", "Kubernetes"). Never derive keywords from a role, title, location, or industry; each has its own filter. • keywords is a BOOLEAN query, not a phrase. Bare space-separated words are AND-ed — fintech payments neobank demands all three in one profile and returns ZERO. For alternatives, OR + quote each term AND wrap the whole group in parentheses: ("fintech" OR "payments" OR "neobank"). The parentheses are MANDATORY — an unparenthesized OR of 4+ terms is silently mis-parsed by LinkedIn and returns ~0 even though it looks right. Quote multi-word phrases; combine groups with AND/NOT; operators UPPERCASE. If a keyword search returns zero, suspect (a) missing parentheses around the OR group and (b) implicit-AND, and re-issue before dropping terms. WORKED EXAMPLES (natural language → filters) • "Payment managers / gerentes de pagos in LATAM" → category:people, role:{include:["<<SENIORITY_BLOCK_ES_EN>> AND (\"pagos\" OR \"payments\" OR \"medios de pago\" OR \"billing\" OR \"cobranzas\")"]}, filters:[{type:"location",query:"Mexico"},{type:"location",query:"Brazil"},{type:"location",query:"Colombia"},{type:"location",query:"Argentina"}]. • "Marketing leaders in the US" → category:people, role:{include:["CMO","Chief Marketing Officer","<<SENIORITY_BLOCK_EN>> AND (\"marketing\" OR \"branding\" OR \"growth\" OR \"demand generation\")"]}, filters:[{type:"location",query:"United States"}]. • "CEOs and founders in Argentina" → category:people, role:{include:["CEO","Chief Executive Officer","Founder","Fundador","Fundadora","Cofounder","Co-founder"]}, filters:[{type:"location",query:"Argentina"}]. • "VP-level engineers at Berlin startups" (pure level + a title word) → category:people, seniority:{include:["vice_president"]}, role:{include:["Engineering"]}, filters:[{type:"location",query:"Berlin, Germany"}], companyHeadcount:["1-10","11-50","51-200"]. • "Fintech companies in LATAM, 50–200 staff" → category:companies, filters:[{type:"industry",query:"Financial Services"},{type:"industry",query:"Banking"},{type:"location",query:"Mexico"},{type:"location",query:"Brazil"},{type:"location",query:"Colombia"}], headcount:["51-200"], keywords:"fintech". Requires the connected account to hold a Sales Navigator seat. Transient hits + a cursor; page with the returned cursor. One page is one metered action.

Parameters: search_sales_navigator
NameTypeWhat it is for
category"people" | "companies"= "people"Search people (default) or companies. Some fields are valid for only one category.
keywordsstringLAST resort — matches noisily across all text. Prefer a structured filter for any title/industry/geography/size intent; use keywords only for a concept no filter captures. This is a BOOLEAN query, not a phrase: bare space-separated words are AND-ed (ALL must appear), so "fintech payments neobank" requires all three together and usually returns ZERO. For ALTERNATIVES, OR them, quote each, and ALWAYS wrap the OR group in parentheses: ("fintech" OR "payments" OR "neobank"). The parentheses are MANDATORY — LinkedIn silently mis-parses an unparenthesized OR of 4+ terms and returns ~0. Quote multi-word phrases; combine groups with AND/NOT (e.g. ("fintech" OR "neobank") AND "payments"). Operators must be UPPERCASE.
filtersobject[]Id-resolved dimensions (location, industry, company, …). Same dimension repeated = OR; different dimensions = AND (each narrows the results).
cursorstringPass the cursor from a prior result to page.
limitintegerPage size (max 100 for Sales Nav; default 10). Used when paging, not by the agent.
firstNamestring
lastNamestring
roleobjectCurrent job-TITLE keywords (include/exclude) — the primary people filter. Use it for ALL title intent, NOT keywords and NOT seniority. Each include entry is a separate Boolean title clause LinkedIn ORs. Two kinds: (1) single-title pills — exact exec titles + founder, one entry per variant (e.g. "CEO","Chief Executive Officer","Founder","Fundador"); (2) ONE seniority-block pill for broad leadership roles: a marker AND a parenthesized OR-list of role synonyms, e.g. <<SENIORITY_BLOCK_EN>> AND ("growth" OR "demand generation"). The marker (one of <<SENIORITY_BLOCK_EN>> / <<SENIORITY_BLOCK_ES_EN>> / <<SENIORITY_BLOCK_PT_EN>>, by language) expands server-side to a broad leadership OR-block so you recall heads/VPs/directors/managers without enumerating titles. See the playbook for the full recipe.
seniorityobjectLinkedIn's coarse seniority ladder (cxo, vice_president, director, senior, …) — NOT job titles. Use only when you mean a seniority level; pair with role for "VP-level whose title contains X".
tenureobject[]Years at the current company in their current role, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
tenureAtCompanyobject[]Total years at the current company, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
tenureAtRoleobject[]Years in the current role, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
companyHeadcount"1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001-5000" | "5001-10000" | "10001+"[]Size band of the person's current employer (people search). For a company's OWN size use headcount. Approximate "startup" with small buckets (["1-10","11-50","51-200"]).
companyType"public_company" | "privately_held" | "non_profit" | "educational_institution" | "partnership" | "self_employed" | "self_owned" | "government_agency"[]Ownership type of the person's current employer (people search; e.g. privately_held, public_company). A companies search has no company-type facet.
profileLanguagestring[]Profile languages as 2-letter ISO codes (e.g. ["en","es"]).
networkDistance1 | 2 | 3 | "GROUP"[]Connection degree from the seat: 1 / 2 / 3 — valid for BOTH people and companies. The "GROUP" value (shared group) is PEOPLE only; do not use it on a companies search.
connectionsOfstring[]Member ids whose connections to search within.
changedJobsbooleanSpotlight: only people who recently changed jobs.
postedOnLinkedinboolean
mentionedInNewsboolean
followingYourCompanyboolean
pastColleagueboolean
sharedExperiencesboolean
viewedYourProfileRecentlyboolean
viewedProfileRecentlyboolean
messagedRecentlyboolean
includeSavedLeadsboolean
includeSavedAccountsboolean
hasJobOffersbooleanCompany search: only companies currently hiring.
headcount"1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001-5000" | "5001-10000" | "10001+"[]The company's OWN size band (companies search; the analog of people-search companyHeadcount). Approximate "startup" with small buckets (["1-10","11-50","51-200"]).
headcountGrowthobjectCompany headcount growth rate as a {min,max} percentage range.
annualRevenueobjectCompany annual revenue {currency (ISO 4217), min, max} in millions. Both min AND max are required and MUST be breakpoints: 0, 0.2, 1, 2.5, 5, 10, 20, 50, 100, 500, 1000, 1001.
followersCountobject[]Company follower count {min,max} ranges. Breakpoints — min: 1, 51, 101, 1001, 5001; max: 50, 100, 1000, 5000.
fortuneobject[]Fortune ranking {min,max} ranges (e.g. Fortune 500). Breakpoints — min: 0, 51, 101, 251; max: 50, 100, 250, 500.
recentActivities"senior_leadership_changes" | "funding_events"[]Company signals: senior_leadership_changes and/or funding_events.
departmentHeadcountobject
departmentHeadcountGrowthobject
savedAccountsstring[]
savedSearchIdstring
recentSearchIdstring
lastViewedAtnumber
withinAreanumber
companyListsobject[]Scope a PEOPLE search to the companies in your own Kairon company lists (list_list, asset "companies"). The server expands each list to its companies' LinkedIn ids as a current_company filter — no per-company resolution, no 40-filter ceiling. NOT account_lists, which means Sales Navigator's own saved lists and will not match a Kairon list. People category only.

Campaigns

What motion is running against them, and how is it doing?

campaign_activate

Activate a campaign — starts sending to real people

  • write
  • destructive
  • idempotent
  • open-world

THIS STARTS SENDING MESSAGES TO REAL PEOPLE ON LINKEDIN, and a message already sent cannot be un-sent. Activates a draft or paused campaign — { id, startNow? }; startNow (default true) enrolls today's first batch immediately instead of waiting for the campaign's next scheduled send window. Rejected with the machine code CAMPAIGN_ACTIVATION_BLOCKED (409, params.unmet listing every unmet precondition as structured data an agent can branch on) unless ALL FOUR hold: no_published_version — a published graph exists (campaign_set_graph then campaign_publish first); no_bound_list — at least one lead list is bound (campaign_bind_list); no_send_window — the campaign has a send window configured; sender_not_connected — its sending seat is still connected. Calling activate on an ALREADY-active campaign is a no-op that returns the current campaign unchanged — safe to retry after a lost response. Sending itself is throttled by the org's own daily caps and send window; this tool does not need to reason about them. 404s (COMMON_NOT_FOUND) if the campaign is not in this org.

Parameters: campaign_activate
NameTypeWhat it is for
startNowboolean= true
idrequiredstring

campaign_create

Create a draft campaign

  • write

Create a draft campaign: a name, the sending seat (channelAccountId — a connected LinkedIn account in this org), and optionally the ICP to gate enrollment on (icpId; when omitted, the org's most-used ICP is auto-selected as the default gate, if the org has one). Returns the new campaign in draft status, with no bound lists and no graph yet — follow with campaign_bind_list for an audience and campaign_set_graph for a sequence before campaign_publish. 404s (COMMON_NOT_FOUND) if channelAccountId is not a connected account in this org.

Parameters: campaign_create
NameTypeWhat it is for
namerequiredstring
channelAccountIdrequiredstring
icpIdstring

campaign_pause

Pause an active campaign

  • write
  • idempotent

Pause an active campaign — { id }. Nothing new fires going forward; runs already in flight simply wait at their current step and resume where they left off if the campaign is reactivated later (nothing already sent is recalled). Idempotent: pausing an already-paused campaign is a no-op that returns the current campaign unchanged. Rejected (COMMON_VALIDATION_FAILED, 400) if the campaign is draft or archived — only an active campaign can be paused. 404s (COMMON_NOT_FOUND) if the campaign is not in this org.

Parameters: campaign_pause
NameTypeWhat it is for
idrequiredstring

campaign_publish

Validate and publish the campaign's draft graph

  • write

Validate and publish the campaign's current draft graph — { id, migrateInFlight? }. Publish can FAIL validation: rather than stopping at the first problem, it returns published: false with EVERY validation error in errors (each { code, message, nodeKey?, condition?, params? }, codes like empty_graph, no_start, multiple_starts, unreachable, cycle, required_outcome_unwired, unknown_variable, template_body_required, ai_instruction_required, invalid_max_length, fixed_text_too_long) so every problem can be fixed in one pass — this is a value in the RESULT BODY, never a thrown exception. On success, published: true and version carries the freshly-published graph (version is null while published is false). migrateInFlight (default false) additionally repoints the campaign's in-flight runs onto the freshly published version by node key, reporting migration: { migrated, canceled } — a run parked on a node the new version removed is canceled instead of migrated. 404s (COMMON_NOT_FOUND) if the campaign is not in this org.

Parameters: campaign_publish
NameTypeWhat it is for
idrequiredstring
migrateInFlightboolean

campaign_run_get

One sequence run, step by step

  • read-only
  • idempotent

Read ONE sequence run in full — { runId } (an id from campaign_runs). Returns the lead, the sending seat, the run's position in the sequence, and steps — the node-by-node timeline with each step's status and timing. When a send is drafted or already out, body carries the actual text, with reasoning and evidence for an AI draft and verification when the send was checked. This is how you answer "what did we actually say to this person, and what happened next" — campaign_runs gives you the page, this gives you the story. 404s (COMMON_NOT_FOUND) if the run is not in this org, or belongs to another member's sending seat.

Parameters: campaign_run_get
NameTypeWhat it is for
runIdrequiredstring

campaign_runs

The campaign's sequence runs

  • read-only
  • idempotent

Read the campaign's sequence runs — one per enrolled lead — keyset-paginated: { id, bucket?, cursor?, limit? }. bucket filters to one activity tab: waiting (still in flight), didnt_accept (invite never accepted), didnt_answer (message sent, no reply), replied, interested (a reply classified interested — a subset of replied), skipped (an Enrollment Filter / ICP / exclusion verdict, or an operator stop), error (failed); omit bucket to list every run. Each run carries the lead (display card), currentNodeKey/currentNodeType/currentNodeRunStatus while live, nextActionAt for the live waiting bucket, outcome once terminal, the errorCode when outcome is failed, and skip/replyClassification detail where relevant. Page with the returned cursor. 404s (COMMON_NOT_FOUND) if the campaign is not in this org.

Parameters: campaign_runs
NameTypeWhat it is for
bucket"waiting" | "didnt_accept" | "didnt_answer" | "replied" | "skipped" | "error" | "interested"
cursorstring
limitinteger= 50
idrequiredstring

campaign_set_graph

Replace the campaign's draft sequence graph

  • write
  • idempotent

Replace the campaign's editable DRAFT sequence graph in full — { id, graph: { nodes, edges, startNodeKey } }. This OVERWRITES the whole draft; it is not a patch. Each node is { key, type, config, uiPosition: { x, y } }key is a stable string you choose (referenced by edges), type is one of send_invite | wait_connection_accepted | delay | send_message | like_last_post | visit_profile | comment_on_post | research (wait_reply isn't supported yet), and config is that type's own shape (e.g. { note, draftMode?, instruction?, language? } for send_invite — its length budget is computed automatically, not a config field — { days } for delay, { timeoutDays } for wait_connection_accepted). Edges are { fromKey, toKey, condition }, referencing node keys by the outcome they route. The wiring rules, per node type — the legal outcome-edge conditions (flagging which MUST route onward) and the legal successor types:

Read the full description

- send_invite: outcome edge "sent"; may connect to → wait_connection_accepted - wait_connection_accepted: outcome edge "accepted" (required — must route onward), "timeout"; may connect to → delay, send_message, like_last_post, visit_profile, comment_on_post, research - delay: outcome edge "default" (required — must route onward); may connect to → send_message, send_invite, like_last_post, visit_profile, comment_on_post, research - send_message: outcome edge "sent"; may connect to → delay, like_last_post, visit_profile, comment_on_post, research - like_last_post: outcome edge "done"; may connect to → send_invite, delay, send_message, visit_profile, comment_on_post, research - visit_profile: outcome edge "done"; may connect to → send_invite, delay, send_message, like_last_post, comment_on_post, research - comment_on_post: outcome edge "commented"; may connect to → send_invite, delay, send_message, like_last_post, visit_profile, research - research: outcome edge "done"; may connect to → send_invite, delay, send_message, like_last_post, visit_profile, comment_on_post Rejected with COMMON_VALIDATION_FAILED (400) on a duplicate node key, a dangling edge (fromKey/toKey with no matching node), an edge condition the source node type doesn't emit, or an edge to a node type the source may not connect to — the rules above. Saving is not publishing: the graph stays invisible to any run until campaign_publish. 404s (COMMON_NOT_FOUND) if the campaign is not in this org.

Parameters: campaign_set_graph
NameTypeWhat it is for
idrequiredstring
graphrequiredobject

campaign_stats

How the campaign is doing

  • read-only
  • idempotent

Everything about how a campaign is performing, in one read — { id, days? }. summary is the live execution snapshot: { status, audienceSize, enrolledCount, queuedCount, activeRunCount, usedToday, dailyEnrollmentLimit, sendWindow, currentVersion }currentVersion is null until campaign_publish has run, which is the thing to check before campaign_activate. funnel is the outcome funnel (enrolled → invited → accepted → messaged → replied → interested). buckets is the count per activity bucket — where everyone currently stands (queued, waiting, didnt_accept, didnt_answer, replied, interested, skipped, error) — the same vocabulary campaign_runs' bucket filter takes, so a non-zero count tells you which filter is worth reading. activity is the daily series, and is returned ONLY if you pass days (7 or 30). Its days are the campaign's own local days in its send-window timezone, not UTC. 404s (COMMON_NOT_FOUND) if the campaign is not in this org.

Parameters: campaign_stats
NameTypeWhat it is for
idrequiredstring
days7 | 30

campaign_update

Edit the campaign's configuration and audience

  • write
  • idempotent

Edit a campaign's configuration — any subset of { id, name, sendWindow, autonomy, dailyEnrollmentLimit, enrollmentFilter, icpId, stopOnReply, leadListIds }. Omitted fields are left unchanged. sendWindow is { days: [1..7 as Mon..Sun], startHour, endHour, timeZone } and is REQUIRED before campaign_activate will run. autonomy is 'autopilot' (sends fire on schedule) or 'manual' (each send waits for approval). icpId gates enrollment on an ICP (null clears the gate — enroll everyone). stopOnReply: false measures replies without halting the run. leadListIds binds lead lists to the audience and is **additive**: lists already bound stay bound, and a bound list you leave out is NOT unbound — unbinding would cancel every in-flight run drawn from that list, so it is not something this tool can do by omission. Returns the updated campaign. 404s (COMMON_NOT_FOUND) if the campaign, a lead list, or icpId is not in this org.

Parameters: campaign_update
NameTypeWhat it is for
namestring
sendWindowobject
autonomy"manual" | "autopilot"
dailyEnrollmentLimitinteger
enrollmentFilterobject
icpIdstring | null
stopOnReplyboolean
idrequiredstring
leadListIdsstring[]

Inbox & outreach

What did they say, and what do we say back?

chat_list

List LinkedIn conversations

  • read-only
  • idempotent
  • open-world

List LinkedIn conversations, newest activity first. Defaults to YOUR OWN seat — pass scope: "org" for every seat in the organization, or seatId for one teammate. Filter with unread (only chats with unread messages), leadsOnly (only chats linked to a lead), and q (case-insensitive match on the counterpart's NAME — this does not search message text). Each chat carries id (for chat_messages), counterpartName, leadId when linked, unreadCount, and lastMessagePreview. Page with the returned cursor. Reading is free: it is never metered and never counts against a sending limit.

Parameters: chat_list
NameTypeWhat it is for
seatIdstringA teammate's seat id (from a chat's channelAccountId). Defaults to your own.
scope"mine" | "org"'mine' (default) is your own seat; 'org' is every seat in the organization.
unreadboolean
leadsOnlybooleanOnly chats already linked to a lead.
qstringCase-insensitive match on the counterpart's NAME.
cursorstring
limitinteger

chat_messages

Read a conversation

  • read-only
  • idempotent
  • open-world

Read one conversation's messages, **newest first**, by the chatId from chat_list. Each message has direction ("in" = from them, "out" = from you), body, sentAt, and any attachments; a message the counterpart deleted comes back with deleted: true and an empty body. Page further back in history with the returned cursor. Opening a thread may pull fresh messages from LinkedIn, which is not metered and does not count against any sending limit.

Parameters: chat_messages
NameTypeWhat it is for
chatIdrequiredstring
cursorstring
limitinteger

inmail_send

Send an InMail

  • write
  • destructive
  • open-world

Send an InMail (optional subject). Address the recipient with target{ type, value } (url, public_identifier, or provider_id). Requires the Sales Navigator seat and a live InMail credit; refused fast otherwise. Optionally attach files with attachments — each item is EITHER { fileName, mimeType, dataBase64 } (the file bytes inline as base64; at most ONE such file per send, ≤3 MB — if you cannot produce base64 reliably, e.g. as a chat agent without direct file access, use url instead) OR { fileName, mimeType, url } (an https url we fetch server-side; up to 5 of these, ≤15 MB each) — never both on the same item. This contacts a real person and consumes a credit — there is no human approval step.

Parameters: inmail_send
NameTypeWhat it is for
targetrequiredobject
subjectstring
textrequiredstring
attachmentsobject[]

invite_send

Send a connection invitation

  • write
  • destructive
  • open-world

Send a LinkedIn connection invitation (optional note) through the caller's account. Address the recipient with target{ type, value } (url, public_identifier, or provider_id). Refused if already connected/invited; deduped per recipient; capped daily. This contacts a real person — there is no human approval step.

Parameters: invite_send
NameTypeWhat it is for
targetrequiredobject
notestring

message_delete

Delete (retract) a sent message

  • write
  • destructive
  • idempotent
  • open-world

Retract a message YOU sent, addressed by the chatId and message id that chat_messages returns. Only outbound messages can be deleted — an inbound one is a not-found. Removes it from LinkedIn and marks it deleted in the Kairon conversation. Idempotent. Metered.

Parameters: message_delete
NameTypeWhat it is for
chatIdrequiredstring
messageIdrequiredstringThe message's id from chat_messages.

message_edit

Edit a sent message

  • write
  • destructive
  • open-world

Replace the body of a message YOU sent, addressed by the chatId and message id that chat_messages returns. Only outbound messages can be edited — an inbound one is a not-found. LinkedIn allows this on Classic messages within about an hour of sending; later attempts are refused by LinkedIn. Updates the conversation in Kairon as well as on LinkedIn. Metered.

Parameters: message_edit
NameTypeWhat it is for
chatIdrequiredstring
messageIdrequiredstringThe message's id from chat_messages.
textrequiredstring

message_send

Send a direct message

  • write
  • destructive
  • open-world

Send a LinkedIn direct message. Pass EXACTLY ONE of: chatId — reply in an existing conversation (the id from chat_list), which is what you want whenever the conversation already exists; or target{ type, value } (url, public_identifier, or provider_id) to open a NEW conversation, allowed only with a 1st-degree connection or an open profile. Optionally attach files with attachments{ fileName, mimeType, dataBase64 }, up to 5 files, each ~3 MB. Returns chatId and, for a reply, the persisted message (with the id message_edit and message_delete take). This contacts a real person — there is no human approval step. Metered against the seat's daily send limit.

Parameters: message_send
NameTypeWhat it is for
chatIdstringReply in this existing conversation (from chat_list). Use this, or target.
targetobjectOpen a NEW conversation with this person. Use this, or chatId.
textrequiredstring
attachmentsobject[]
clientTokenstring

Acting as someone else

Whose seat is this running on?

impersonate_list

List who you can act as

  • read-only
  • idempotent

The users you may act as, each with their organization and whether they have a LinkedIn account connected — you can only act as someone who does. Staff see every tenant; an org owner or admin sees the members of the organizations they govern. search matches name, email or organization.

Parameters: impersonate_list
NameTypeWhat it is for
searchstringFilter by name, email address or organization name.

impersonate_start

Act as another user

  • write
  • idempotent

Act as another user from now on: every later call runs as them, on their LinkedIn seat and their organization. Staff may act as anyone; an org owner or admin only as a member of their own organization, never one ranking above them. user is an email address or a user id. Calling it again switches target. The mode belongs to this client alone and ends 60 minutes after your last call, or on impersonate_stop.

Parameters: impersonate_start
NameTypeWhat it is for
userrequiredstringThe email address or user id to act as.

impersonate_stop

Stop acting as another user

  • write
  • idempotent

Go back to being yourself. Succeeds whether or not you were acting as anyone, and reports which it was.

Takes no parameters.

Troubleshooting

No tools appear after connecting

The account behind the connection has no LinkedIn seat, or the plan is not active. Connect a seat in Kairon and connect again. This is the common one.

Your assistant says a tool refused

Refusals are deliberate and they say why: a daily cap reached, an invite already pending, a message that can no longer be edited. The reason comes back as a fixed code, so your assistant can tell "wait and retry" apart from "this will never work".

The connection seems to drop

It should not. Every call signs in again and finishes on its own, so there is no session to expire and nothing to reconnect after a deploy or after you close your laptop. If tools disappear, check the two causes above first.

It will not write your ICP or qualify accounts

That is on purpose. Those are interviews and judgment calls the agents inside Kairon do better, so they stayed in the app. An assistant can read an ICP and point a campaign at it.

What a refusal looks like
The tool result
{
  "isError": true,
  "content": [{ "type": "text", "text": "…" }]
}
That text, parsed
{
  "code": "CHANNEL_ACTION_LIMIT_EXCEEDED",
  "params": { "retryAfterSeconds": 41400 }
}

Support

Ask Kairon, the guide inside the app, answers setup questions and knows your account. If you are stuck on the connection itself, sign in and ask it there.

Building against Kairon rather than talking to it? Every tool with its full JSON Schema, exactly as the server returns it: mcp-tools.json

Privacy and data

Everything runs over HTTPS. Your assistant signs in as you and acts as the LinkedIn seat you connected, inside the same daily caps and sending hours the app uses. It cannot raise its own limits. Every invite, message and search it makes appears in your Kairon activity, marked as coming from an agent, so you can see what it did and to whom.