Kairon
Build prospecting lists from buying signals, enrich them, and run LinkedIn outreach.
Documentation
Getting started
- 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
- 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.
| Name | Type | What 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" |
| queryrequired | string |
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.
| Name | Type | What it is for |
|---|---|---|
| namerequired | string | |
| definitionrequired | object | The 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.
| Name | Type | What it is for |
|---|---|---|
| icpIdrequired | string |
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.
| Name | Type | What it is for |
|---|---|---|
| targetrequired | object | |
| kind | "profile" | "company" | Override auto-detection. Required for a company addressed by universal name or numeric id. |
| icpVersionIdrequired | string | |
| results | object[]= [] |
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.
| Name | Type | What it is for |
|---|---|---|
| icpIdrequired | string | |
| namerequired | string | |
| definitionrequired | object | The 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.
| Name | Type | What it is for |
|---|---|---|
| targetrequired | object | |
| kind | "profile" | "company" | Override auto-detection. Required for a company addressed by universal name or numeric id. |
| refresh | boolean |
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.
| Name | Type | What it is for |
|---|---|---|
| assetrequired | "leads" | "companies" | Which kind of list: 'leads' (people) or 'companies'. |
| idrequired | string | |
| itemsrequired | object[] |
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.
| Name | Type | What it is for |
|---|---|---|
| assetrequired | "leads" | "companies" | Which kind of list: 'leads' (people) or 'companies'. |
| namerequired | string |
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.
| Name | Type | What it is for |
|---|---|---|
| assetrequired | "leads" | "companies" | Which kind of list: 'leads' (people) or 'companies'. |
| idrequired | string |
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.
| Name | Type | What it is for |
|---|---|---|
| assetrequired | "leads" | "companies" | Which kind of list: 'leads' (people) or 'companies'. |
| q | string | |
| cursor | string | |
| limit | integer= 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.
| Name | Type | What it is for |
|---|---|---|
| assetrequired | "leads" | "companies" | Which kind of list: 'leads' (people) or 'companies'. |
| idrequired | string | |
| q | string | |
| cursor | string | |
| limit | integer= 25 | |
| tags | string[] |
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.
| Name | Type | What it is for |
|---|---|---|
| assetrequired | "leads" | "companies" | Which kind of list: 'leads' (people) or 'companies'. |
| idrequired | string | |
| namerequired | string | |
| columns | object[] | 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.
| Name | Type | What it is for |
|---|---|---|
| assetrequired | "leads" | "companies" | |
| itemsrequired | object[] | |
| ownerrequired | object | 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.
| Name | Type | What it is for |
|---|---|---|
| targetrequired | object | |
| cursor | string |
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.
| Name | Type | What it is for | |
|---|---|---|---|
| url | string | ||
| keywords | string | ||
| cursor | string | ||
| limit | integer | Page 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. | |
| filters | object[] | ||
| networkDistance | 1 | 2 | 3[] | Connection degree: 1 (1st), 2 (2nd), 3 (3rd+). LinkedIn native filter CONNECTIONS. |
| profileLanguage | string[] | Profile languages as 2-letter ISO-639-1 codes (e.g. ["en","es"]). | |
| openTo | "proBono" | "boardMember"[] | ||
| connectionsOf | string[] | Member ids whose connections to search within (from people.suggest). | |
| followersOf | string[] | Member ids whose followers to search within (from people.suggest). | |
| advancedKeywords | object | ||
| intoList | string |
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.
| Name | Type | What it is for | ||||||
|---|---|---|---|---|---|---|---|---|
| category | "people" | "companies"= "people" | Search people (default) or companies. Some fields are valid for only one category. | ||||||
| keywords | string | LAST 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. | ||||||
| filters | object[] | Id-resolved dimensions (location, industry, company, …). Same dimension repeated = OR; different dimensions = AND (each narrows the results). | ||||||
| cursor | string | Pass the cursor from a prior result to page. | ||||||
| limit | integer | Page 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. | ||||||
| firstName | string | |||||||
| lastName | string | |||||||
| role | object | Current 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. | ||||||
| seniority | object | LinkedIn'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". | ||||||
| tenure | object[] | 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}. | ||||||
| tenureAtCompany | object[] | 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}. | ||||||
| tenureAtRole | object[] | 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. |
| availableForWork | boolean | null | Signals 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. | ||||||
| profileLanguage | string[] | Profile languages as 2-letter ISO codes (e.g. ["en","es"]). | ||||||
| networkDistance | 1 | 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. | ||||
| connectionsOf | string[] | Member ids whose connections to search within. | ||||||
| changedJobs | boolean | Spotlight: only people who recently changed jobs. | ||||||
| postedOnLinkedin | boolean | |||||||
| mentionedInNews | boolean | |||||||
| followingYourCompany | boolean | |||||||
| pastColleague | boolean | |||||||
| sharedExperiences | boolean | |||||||
| viewedYourProfileRecently | boolean | |||||||
| viewedProfileRecently | boolean | |||||||
| messagedRecently | boolean | |||||||
| includeSavedLeads | boolean | |||||||
| includeSavedAccounts | boolean | |||||||
| hasJobOffers | boolean | Company 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"]). |
| headcountGrowth | object | Company headcount growth rate as a {min,max} percentage range. | ||||||
| annualRevenue | object | Company 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. | ||||||
| followersCount | object[] | Company follower count {min,max} ranges. Breakpoints — min: 1, 51, 101, 1001, 5001; max: 50, 100, 1000, 5000. | ||||||
| fortune | object[] | 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. | ||||||
| departmentHeadcount | object | |||||||
| departmentHeadcountGrowth | object | |||||||
| savedAccounts | string[] | |||||||
| savedSearchId | string | |||||||
| recentSearchId | string | |||||||
| lastViewedAt | number | |||||||
| withinArea | number | |||||||
| intoList | string | |||||||
| companyLists | object[] | 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.
| Name | Type | What it is for | ||
|---|---|---|---|---|
| signalrequired | "posted_with_keyword" | "engaged_with_keyword" | "engaged_with_profile" | "recently_connected" | "viewed_my_profile" |
| config | object | |||
| intoList | string | |||
| cursor | string |
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.
| Name | Type | What it is for |
|---|---|---|
| idrequired | string |
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.
| Name | Type | What it is for | |||||
|---|---|---|---|---|---|---|---|
| id | string | ||||||
| name | string | ||||||
| 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.
| Name | Type | What it is for |
|---|---|---|
| assetrequired | "leads" | "companies" | |
| itemsrequired | object[] |
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.
| Name | Type | What it is for |
|---|---|---|
| startNow | boolean= true | |
| idrequired | string |
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.
| Name | Type | What it is for |
|---|---|---|
| idrequired | string | |
| missingOnly | boolean= false | |
| cursor | string | |
| limit | integer= 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.
| Name | Type | What it is for |
|---|---|---|
| namerequired | string | |
| channelAccountIdrequired | string | |
| icpId | string |
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.
| Name | Type | What it is for | |
|---|---|---|---|
| q | string | ||
| status | "draft" | "active" | "paused" | "archived" |
| cursor | string | ||
| limit | integer= 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.
| Name | Type | What it is for |
|---|---|---|
| idrequired | string | |
| migrateInFlight | boolean |
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.
| Name | Type | What it is for |
|---|---|---|
| runIdrequired | string |
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.
| Name | Type | What it is for | ||||
|---|---|---|---|---|---|---|
| bucket | "waiting" | "didnt_accept" | "didnt_answer" | "replied" | "skipped" | "error" | "interested" |
| cursor | string | |||||
| limit | integer= 50 | |||||
| idrequired | string |
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.
| Name | Type | What it is for |
|---|---|---|
| idrequired | string | |
| itemsrequired | object[] |
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.
| Name | Type | What it is for |
|---|---|---|
| idrequired | string | |
| graphrequired | object |
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.
| Name | Type | What it is for |
|---|---|---|
| idrequired | string | |
| days | 7 | 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).
| Name | Type | What it is for |
|---|---|---|
| name | string | |
| sendWindow | object | |
| autonomy | "manual" | "autopilot" | |
| dailyEnrollmentLimit | integer | |
| enrollmentFilter | object | |
| icpId | string | null | |
| stopOnReply | boolean | |
| idrequired | string | |
| leadListIds | string[] |
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.
| Name | Type | What it is for |
|---|---|---|
| seatId | string | A 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. |
| unread | boolean | |
| leadsOnly | boolean | Only chats already linked to a lead. |
| q | string | Case-insensitive match on the counterpart's NAME. |
| cursor | string | |
| limit | integer |
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.
| Name | Type | What it is for |
|---|---|---|
| chatIdrequired | string | |
| cursor | string | |
| limit | integer |
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.
| Name | Type | What it is for |
|---|---|---|
| targetrequired | object | |
| subject | string | |
| textrequired | string | |
| attachments | object[] |
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.
| Name | Type | What it is for |
|---|---|---|
| targetrequired | object | |
| note | string |
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.
| Name | Type | What it is for |
|---|---|---|
| chatIdrequired | string | |
| messageIdrequired | string | The 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.
| Name | Type | What it is for |
|---|---|---|
| chatIdrequired | string | |
| messageIdrequired | string | The message's id from chat_messages. |
| textrequired | string |
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.
| Name | Type | What it is for |
|---|---|---|
| chatId | string | Reply in this existing conversation (from chat_list). Use this, or target. |
| target | object | Open a NEW conversation with this person. Use this, or chatId. |
| textrequired | string | |
| attachments | object[] | |
| clientToken | string |
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.
| Name | Type | What it is for |
|---|---|---|
| search | string | Filter 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.
| Name | Type | What it is for |
|---|---|---|
| userrequired | string | The 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.
| Name | Type | What 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.
| Name | Type | What it is for |
|---|---|---|
| checks | object[] | Replaces every content check. An empty array removes them all. |
| competitorUrls | string[] | Replaces every competitor URL. Each must be a LinkedIn URL. |
| sourceUrls | string[] | Replaces every source URL. Any http(s) URL. |
| writingGuidelines | string | Replaces the caller's writing guidelines. An empty string clears them. |
| valueProposition | string | Replaces the organization's value proposition. An empty string clears it. |
| leadMagnet | string | Replaces the organization's lead magnet — what it gives a prospect to earn a reply. An empty string clears it. |
| learnings | string | Replaces 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.
| Name | Type | What it is for |
|---|---|---|
| namerequired | string | Which 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.