Kairon

Build prospecting lists from buying signals, enrich them, and run LinkedIn outreach.

Documentation

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  
  1. 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.

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.

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

Create an ICP — a target thesis spanning the companies to reach and the people inside them — at version 1. Criteria are optional: a name plus an empty definition is a valid coarse ICP you can sharpen later with icp_update. Prefer defining one *consultatively*, from the operator's own best customers, rather than guessing filters.

NameTypeWhat it is for
namerequiredstring
definitionrequiredobjectThe ICP definition. On icp_update this REPLACES the previous one wholesale, so a field you leave out is cleared, not carried forward — read the ICP first and send back everything you do not mean to change. Use null for a side with no criteria and [] for no checks.

Soft-delete an ICP. Returns referencedByCampaigns — how many campaigns referenced it as their enrollment gate (any status, draft included) and no longer have one. The delete always proceeds, so check that number and tell the operator if it is not zero: any of those campaigns that is running just widened its audience.

NameTypeWhat it is for
icpIdrequiredstring

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

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

Takes no parameters.

Qualify one LinkedIn person or company against a saved ICP version and PERSIST the verdict: **you** judge the AI checks, Kairon judges the structured filters and owns the row. Call icp_get first — its conditions are the checks to answer, and their ids are what results[].criterionId must carry. Answer EVERY check of the sides judged or the call is refused, never scored: a missing answer reads as a rejection. rationale is required. Pace a large audit against the returned headroom.

NameTypeWhat it is for
targetrequiredobject
kind"profile" | "company"Override auto-detection. Required for a company addressed by universal name or numeric id.
icpVersionIdrequiredstring
resultsobject[]= []

Revise an ICP: mints a new immutable version, which becomes the active one. This **replaces** the definition wholesale, so a field you leave out is cleared, not carried forward — call icp_get first and send back the complete definition, changing only what you mean to change. Use null for a side with no criteria and [] for no checks. Past versions stay readable, so earlier qualification verdicts remain explainable.

NameTypeWhat it is for
icpIdrequiredstring
namerequiredstring
definitionrequiredobjectThe ICP definition. On icp_update this REPLACES the previous one wholesale, so a field you leave out is cleared, not carried forward — read the ICP first and send back everything you do not mean to change. Use null for a side with no criteria and [] for no checks.

Fetch a LinkedIn person OR company through the caller's connected account — whichever the target addresses ({ type, value }: url, public_identifier, or provider_id). The kind is detected from the target; pass kind: 'company' for a company addressed by universal name or numeric id. Returns complete data, metered and cached (refresh: true forces a live read), plus headroom — what is LEFT on this seat. Budget a large audit against the SMALLEST number in it.

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

Add members to a list ({ asset, id, items }, up to 500). An item is { memberId } (already in the org), { url } (a pasted LinkedIn URL — instant and free, no provider call), or { snapshot } (a hit from search_people / search_sales_navigator / signal_search, passed as-is with its providerId, which is what makes the saved list show the person rather than a bare id). Re-adding a current member converges. This is how you assemble an audience after a search.

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

Create a named list — the campaign targeting unit. asset: 'leads' for people, asset: 'companies' for companies (build the company set first, then find people inside it by passing the list's id to search_sales_navigator as companyLists). Names are unique per org and asset, case-insensitive; a taken name fails with the existing list's id, never a silent merge. Build membership with list_add.

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

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.

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

One list by asset + id: its name, live member count, created date, and its ordered columns (the list's own custom columns — free-text fields you attach to its members). Out-of-org or deleted ids are not found. To read who is IN it, use list_members, whose rows carry each member's values for those columns.

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.

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

One keyset page of a list's members ({ asset, id, q?, tags?, cursor?, limit? }), newest-added first. Each row is { id, title, subtitle, location, linkedinUrl, values, tags } — the id removes it with list_remove or writes with list_set, the URL reads it with linkedin_fetch, values holds this list's custom columns, tags the org-wide labels the person wears. tags in the query keeps only members carrying EVERY tag named. Page with the returned cursor.

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

Remove members from a list ({ asset, id, items }). Each item names ONE member however you hold it: { memberId }, { url }, { providerId }, or { snapshot } (a hit passed back unedited — only its providerId is read). Deletes only the membership; the leads/companies stay in the org pool and their other lists. Per item: removed, not_member, or not_found.

Write custom-column values on members ALREADY in a list ({ asset, id, items }, ≤500). Each item names one member — { memberId }, { url }, { providerId } or { snapshot } — plus values, keyed by COLUMN NAME.

Read the full description

Merges: names you send are written, names you omit keep what they had, the empty string clears. Create columns first with list_update; a name this list lacks is skipped and echoed in ignored. Per item: set, not_member, not_found. Read them back with list_members.

Rename a list and/or replace its custom columns ({ asset, id, name, columns? }). A name another live list of that asset holds fails with that list's id.

Custom columns are free-text fields on the members of THIS list, addressed by NAME. columns replaces the WHOLE ordered set: omit it to leave them alone, [] removes all. { name, renameFrom } renames one and keeps its values; without renameFrom the old column and everything in it is destroyed. Max 20, names ≤40 chars.

NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
namerequiredstring
columnsobject[]The WHOLE ordered custom-column set, replacing what is there. Omit to leave the columns alone; [] removes them all. Carry renameFrom to rename a column and keep every member's values under the new name.

Make one teammate responsible for the named leads or companies. Name them by email (what you usually have) or userId; pass owner: null to leave them unassigned. The owner must be a current member of the organization. This only labels — it grants no permission and does not decide which seat sends. Never creates a lead: an unknown url comes back not_found.

NameTypeWhat it is for
assetrequired"leads" | "companies"
itemsrequiredobject[]
ownerrequiredobject | null

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.

NameTypeWhat it is for
targetrequiredobject
cursorstring

Classic LinkedIn people search — works WITHOUT a Sales Navigator seat, so reach for it when the caller has none. Takes keywords, id-resolved filters (human-readable names, resolved server-side), network distance and language; or paste a search url to run verbatim. A name LinkedIn does not recognize REFUSES the search rather than quietly returning a wider audience. Pass intoList to append the page to a leads list, or linkedin_fetch a hit to persist it. See the list-building skill.

NameTypeWhat it is for
urlstring
keywordsstring
cursorstring
limitintegerPage size (default 10). Classic search is clamped to 50 provider-side. One page costs one metered action WHATEVER its size, so raise it when you are filling a list rather than sampling — ten pages of 10 spend ten times what one page of 50 does.
filtersobject[]
networkDistance1 | 23[]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
intoListstring

Search Sales Navigator over people OR companies — the expert path: prefer it whenever a structured filter fits the intent, because filters are precise where keywords are noisy. Pass human-readable names ({ type, query }) and the server resolves them to LinkedIn ids; never pass ids yourself. Same-dimension values OR, different dimensions AND. **Read the sales-nav-search skill first** — it carries the filter grammar, the exclusions and worked examples. One page is one metered action.

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). One page costs one metered action WHATEVER its size, so raise it when you are filling a list rather than sampling.
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.
availableForWorkboolean | nullSignals availability for a new role (#OpenToWork, or no current position). true requires it, false excludes it. Screened after the search, on loaded profiles only; unknown never counts as available.
profileLanguagestring[]Profile languages as 2-letter ISO codes (e.g. ["en","es"]).
networkDistance1 | 23"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
intoListstring
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.

Find people by what they DID, not by what their profile says — the behavioural counterpart to search_people and search_sales_navigator. Pick a signal (who engaged with a profile or a keyword, who posted one, the seat's newest connections, who viewed it); each takes its own config. Results are RAW — real people who really did this, with no ICP screening, so screen them yourself. Pass intoList to append the page. The list-building skill covers which signal answers which question.

NameTypeWhat it is for
signalrequired"posted_with_keyword" | "engaged_with_keyword""engaged_with_profile""recently_connected""viewed_my_profile"
configobject
intoListstring
cursorstring

Removes the tag from the organization AND from every lead and company wearing it, in one act. Reports how many of each lost it. The name becomes free to use again immediately. This does not delete any lead or company.

NameTypeWhat it is for
idrequiredstring

Every tag this organization has, with its color. A tag is the operator's own label on a person or a company (warm, gatekeeper, met-at-event) and it follows them everywhere — unlike a list's custom column, which belongs to that one list. Read this before tag_set, which names tags by name and refuses one that does not exist.

Without id, creates a tag. With id, renames and/or recolors that one — and a rename carries it across every lead and company already wearing it, in one act. Names are unique per organization, compared case-insensitively. Colors are palette names: stone, clay, moss, sky, plum, amber, rust, slate.

NameTypeWhat it is for
idstring
namestring
color"stone" | "clay""moss""sky""plum""amber""rust""slate"

Replaces each named subject's WHOLE tag set — so send every tag it should end up with, not just the new one, and send [] to clear it. Name a subject by memberId, url, or providerId, exactly as list_remove does. Tags are named by NAME and must already exist (tag_save first); an unknown name fails the call rather than creating one. This never creates a lead or a company: an unknown url comes back not_found.

NameTypeWhat it is for
assetrequired"leads" | "companies"
itemsrequiredobject[]

THIS STARTS SENDING MESSAGES TO REAL PEOPLE ON LINKEDIN, and a message already sent cannot be un-sent. Activates a draft or paused campaign; startNow (default true) enrolls today's first batch instead of waiting for the next send window. Refused with CAMPAIGN_ACTIVATION_BLOCKED (409) unless all four preconditions hold — params.unmet names every one that does not, as data you can branch on. Activating an active campaign is a no-op, safe to retry. See the campaign-building skill.

NameTypeWhat it is for
startNowboolean= true
idrequiredstring

Per-lead copy coverage — the mirror of campaign_set_copy. Returns coverage: { slots, ready, missing } over the whole audience plus a page of leads; missingOnly keeps just the ones still short — "what is left to write". campaign_runs cannot answer this: an unfilled lead never enrolls, so it has no run, which is why such a campaign shows a large audienceSize, queuedCount: 0, and sends nothing. 400 if the published graph declares no slots.

NameTypeWhat it is for
idrequiredstring
missingOnlyboolean= false
cursorstring
limitinteger= 50

Create a draft campaign: a name and the sending seat (channelAccountId, a connected LinkedIn account in this org), optionally the ICP to gate enrollment on. Omit icpId and the org's most-used ICP becomes the gate, if it has one. Returns a campaign in draft with no audience and no sequence — bind lists with campaign_update, then campaign_set_graph, then campaign_publish. The campaign-building skill has the full order and the defaults.

NameTypeWhat it is for
namerequiredstring
channelAccountIdrequiredstring
icpIdstring

Read ONE campaign completely enough to REBUILD it. Returns its configuration, the bound lead lists, and BOTH graphs: published (what the engine is sending now) and draft (what campaign_set_graph would overwrite), each in exactly the shape that tool accepts. To copy a campaign, replay what this returns — never retype a sequence someone described, because node configs carry slots, draft modes and limits invisible in prose. The campaign-building skill has the copy recipe.

List this org's campaigns, newest first, keyset-paginated — { q?, status?, cursor?, limit? }. q matches the campaign name; status filters to one of draft | active | paused | archived; page with the returned cursor (null on the last page). Each item is the campaign’s configuration — the same shape campaign_create returns — WITHOUT its sequence graph; read one campaign in full with campaign_get. This is how you find a campaign the operator named but whose id you were not given.

NameTypeWhat it is for
qstring
status"draft" | "active""paused""archived"
cursorstring
limitinteger= 25

Pause an active campaign. Nothing new fires; runs already in flight wait at their current step and resume there if the campaign is reactivated. Nothing already sent is recalled. Idempotent — pausing a paused campaign returns it unchanged. Refused (400) if the campaign is draft or archived: only an active one can be paused.

Validate and publish the campaign's draft graph. Publish can FAIL validation, and it fails as DATA, not as an exception: published: false with EVERY error in errors (each naming its code and node), so one pass fixes all of them. On success version carries the freshly published graph. migrateInFlight additionally repoints in-flight runs onto it by node key, canceling any run parked on a node the new version removed. The campaign-building skill lists the error codes.

NameTypeWhat it is for
idrequiredstring
migrateInFlightboolean

Read ONE sequence run in full, by an id from campaign_runs. Returns the lead, the sending seat, the run's position, and 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 its reasoning and evidence for an AI draft. 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.

NameTypeWhat it is for
runIdrequiredstring

Read the campaign's sequence runs — one per enrolled lead — keyset-paginated. bucket filters to one activity tab (waiting, didnt_accept, didnt_answer, replied, interested, skipped, error); omit it for every run. Each run carries the lead, where it currently stands, when it acts next, its outcome once terminal, and the error or skip detail where relevant. Page with the returned cursor. campaign_stats tells you which bucket is worth reading.

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

Write per-lead copy, up to 500 items, one per lead. A slot is a hole YOU fill, written {{@name}} in a template body; {{@all}} alone writes the whole message. Slots come from the PUBLISHED graph, so copy before publish is refused, as is an undeclared name. Values are LITERAL text, measured RENDERED against the step's limit — over it the whole call is rejected and nothing is stored. A lead is not enrolled until it has every slot, so a campaign can be activated first and fed lead by lead.

NameTypeWhat it is for
idrequiredstring
itemsrequiredobject[]

Replace the campaign's editable DRAFT sequence graph IN FULL — this overwrites, it is not a patch. Nodes are { key, type, config, uiPosition } with a stable key you choose; edges are { fromKey, toKey, condition } routing one node's outcome to the next. Which conditions a type emits, and which types it may connect to, are fixed rules — a violation is refused (400). **The campaign-building skill carries the wiring table; read it before your first graph.** Saving is not publishing.

NameTypeWhat it is for
idrequiredstring
graphrequiredobject

Everything about how a campaign is performing, in one read. summary is the live snapshot — status, audience, what is queued, today's usage, the send window, the current version (null until campaign_publish has run, which is what to check before campaign_activate), and copy coverage, which is why queuedCount can sit far under audienceSize. funnel runs enrolled → invited → accepted → messaged → replied → interested. buckets counts where everyone stands.

NameTypeWhat it is for
idrequiredstring
days7 | 30

Edit a campaign's configuration — any subset of its fields; omitted ones are unchanged. This is also how an audience is bound, via leadListIds, and that binding is **additive**: a bound list you leave out is NOT unbound, because unbinding would cancel every in-flight run drawn from it. sendWindow is required before campaign_activate will run. autonomy is 'autopilot' (sends fire on schedule) or 'manual' (each send waits for approval, except a note-less invite).

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

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 by unread, leads-only, or q, which matches the counterpart's NAME and does not search message text. Each chat carries the id that chat_messages takes, the counterpart, any linked lead, and a last-message preview. Page with the returned cursor. Reading is free: never metered, never against a sending limit.

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

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.

NameTypeWhat it is for
chatIdrequiredstring
cursorstring
limitinteger

Send an InMail (optional subject) to the person target addresses. Requires the Sales Navigator seat and a live InMail credit; refused fast without either. An attachment is one form or the other: inline dataBase64 (at most ONE per send, ≤3 MB, it rides in the JSON body) or a url we fetch server-side (up to 5, ≤15 MB each). Reach for url when you cannot produce base64. This contacts a real person AND spends a credit, with no approval step.

NameTypeWhat it is for
targetrequiredobject
subjectstring
textrequiredstring
attachmentsobject[]

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.

NameTypeWhat it is for
targetrequiredobject
notestring

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.

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

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.

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

Send a LinkedIn direct message. Pass EXACTLY ONE of chatId — reply in an existing conversation (an id from chat_list), which is what you want when it exists — or target to open a NEW one, allowed only with a 1st-degree connection or an open profile. Attachments: at most ONE inline base64 file (≤3 MB, it rides in the JSON body), up to 5 total when the rest are https urls we fetch server-side. Returns chatId and the persisted message. This contacts a real person, with no approval step.

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

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.

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

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.

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

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

What the operator has told Kairon: their content checks (what a good post must satisfy), their competitor and source URLs, their writing guidelines (voice), and their organization's value proposition, lead magnet and learnings log. Read this FIRST when deciding what they should work on next, and ground every draft in it — a message written without it is generic. Name sections to read only some.

NameTypeWhat it is for
sections"checks" | "competitorUrls""sourceUrls""writingGuidelines""valueProposition""leadMagnet""learnings"[]Which sections to read. Omit for all seven.

Replace one or more sections of what the operator has told Kairon: checks, competitorUrls, sourceUrls, writingGuidelines, valueProposition, leadMagnet, learnings. Each section you name is replaced WHOLE; each one you omit is untouched. When the operator gives you a whole section, send it straight here. Only when ADDING to what is already stored (one more item, another learnings entry) call config_get first and send the merged text. An empty array or string clears a section.

NameTypeWhat it is for
checksobject[]Replaces every content check. An empty array removes them all.
competitorUrlsstring[]Replaces every competitor URL. Each must be a LinkedIn URL.
sourceUrlsstring[]Replaces every source URL. Any http(s) URL.
writingGuidelinesstringReplaces the caller's writing guidelines. An empty string clears them.
valuePropositionstringReplaces the organization's value proposition. An empty string clears it.
leadMagnetstringReplaces the organization's lead magnet — what it gives a prospect to earn a reply. An empty string clears it.
learningsstringReplaces the organization's GTM learnings log WHOLE. To append an entry, read the log first and write back the joined text. An empty string clears it.

One playbook in full, as markdown, by a name from skill_list — call that first, since the catalogue is published rather than compiled in and an invented name is a 404. Read the playbook before doing the job it covers, and follow it: it encodes what Kairon has learned works, which is usually not what a general model would choose. Pair a writing skill with config_get to ground it in the operator's own voice, offer and quality checks.

NameTypeWhat it is for
namerequiredstringWhich playbook to fetch — a name from skill_list.

Kairon's playbooks — setting an operator up from scratch, searching Sales Navigator, building a list, running a campaign, writing messages and posts, and more. Each entry says what it teaches and when to want it; skill_get returns the playbook itself, by the name listed here. The catalogue grows without a release, so read this rather than assuming you know what exists.

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.