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?

company_followers

List a company page’s followers

  • read-only
  • idempotent
  • open-world

The people who follow a LinkedIn company page YOUR seat administers — the second opted-in audience after a newsletter, and often the bigger one. Address the page with target ({ type, value }: company URL, universal name, or provider id); anything but an id spends one company.fetch to resolve first. 50 followers per page, one page per metered action; page with cursor. LinkedIn shows this list to the page’s admins only — a page this seat does not run is refused, and the fix is to connect the account that runs it.

Parameters: company_followers
NameTypeWhat it is for
targetrequiredobjectThe company page whose followers to read — named by URL, universal name, or provider id. It must be a page this seat ADMINISTERS; LinkedIn shows followers to admins only.
cursorstringContinue from a previous page. Omit for the first page.
countintegerFollowers per page (default 50, the most LinkedIn allows for a page).
intoListstringAppend this page’s followers to one of your lead lists, by name or id. Combine with exhaust to walk the whole audience into it.
exhaustbooleanPage server-side until the results run out, instead of returning a cursor for you to send back. Requires intoList: every page is appended there and NO hits come back, only the totals — which is the point, since neither the hits nor the cursor then cross your context. Stops at maxResults and hands back a cursor to resume from. Each page still costs one metered action.
maxResultsintegerHow many hits an exhaust run collects before it stops (default 500, max 2000). Ignored without exhaust. Raise it when you know the audience is large and you mean to spend the actions.

company_posts

A company page's recent posts

  • read-only
  • idempotent
  • open-world

A page of a COMPANY page's own posts — what a competitor, a partner or a target account publishes under its own name, not what its people post. Address it with target{ type, value } (company URL, universal name, or provider id) — and page with cursor; one page is one metered action, and a target that is not already a provider id spends one company.fetch to resolve first. For a PERSON's feed use profile_posts; to reach the people who engaged with one of these posts use post_interactions. Transient — nothing here is persisted.

Parameters: company_posts
NameTypeWhat it is for
targetrequiredobjectThe company page whose posts to read — named by URL, universal name, or provider id.
cursorstring

exclusion_add

Forbid contact with people or companies

  • write
  • destructive

Forbid contact with 1..1000 people or companies. This is not only about the future: everything already lined up for them stops. Their campaign runs end, messages already drafted or scheduled for them are dropped, and they are removed from every list holding them. Removing the entry later does NOT put them back in those lists. Set facet, then send items. A PERSON needs a LinkedIn profile URL, or a complete name (fullName, or firstName + lastName) TOGETHER with a company (companyName, companyUrl or companyDomain) — a name with no company is refused. A COMPANY needs at least one of companyName, linkedinUrl or domain. Every item is judged on its own and comes back added, duplicate or invalid with a reason: one bad item never fails the others.

Parameters: exclusion_add
NameTypeWhat it is for
facetrequired"people" | "companies"What these items are. people suppresses each named individual. companies suppresses the company itself AND everyone who works there.
itemsrequiredobject[]The entries to add, up to 1000. Each is judged on its own, so one invalid item never fails the rest.

exclusion_list

Read the organization's exclusion list

  • read-only
  • idempotent

Who this organization must never contact — the org-wide suppression list, in two facets: people (named individuals) and companies (the company itself AND everyone who works there). Returns both, newest first, unless you name a facet. Read it before building a list or launching a campaign, and read it FIRST when asked why somebody is missing from a list or why a campaign skipped them: a person can be suppressed by their employer's entry, not only by their own. Each entry carries the id exclusion_remove takes.

Parameters: exclusion_list
NameTypeWhat it is for
facet"people" | "companies"Read one facet only. Omit to get both, which is what "is this person blocked?" needs — someone can be suppressed by their employer's entry rather than by their own.

exclusion_remove

Remove one entry from the exclusion list

  • write
  • destructive

Stop suppressing one person or one company, by the entry id exclusion_list returned (exper_… on people, excom_… on companies). It lifts the ban going FORWARD only: it does not put the person or the company back into any list they were swept out of, and it does not revive a campaign run that ended — add them to a list again if that is what you want. It removes ONE entry, so somebody suppressed by their employer's company entry stays suppressed until that entry goes too. An id of the wrong kind is refused; an id belonging to another organization is simply not found.

Parameters: exclusion_remove
NameTypeWhat it is for
facetrequired"people" | "companies"Which facet the entry sits on. It decides the id prefix: peopleexper_…, companiesexcom_….
idrequiredstringThe entry id, exactly as exclusion_list returned it.

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"Which Sales Navigator dimension the name belongs to.
queryrequiredstringThe human-readable NAME to resolve, e.g. "San Francisco Bay Area". Never a LinkedIn id — resolving one is what this is for, and the canonical label comes back so a wrong match is visible.

group_members

List a LinkedIn group’s members

  • read-only
  • idempotent
  • open-world

The members of a LinkedIn group, from the group’s own member list — people who joined a community around a topic, which is a targeting fact no search filter can express. Pass the group URL or id. Works on ANY connected seat for a public group: no Sales Navigator, no need to have joined. Every page reports total; 100 members per page, one page per metered action. LinkedIn serves at most the 10,000 most recent joiners of a group, newest first — for a bigger group that is the slice you get, and the walk stops there. Each member carries networkDistance (1, 2 or 3) so you know who can be messaged and who must be invited first. A private group is readable only by a member seat.

Parameters: group_members
NameTypeWhat it is for
grouprequiredstringThe group, as its URL (https://www.linkedin.com/groups/<id>/) or its bare numeric id. Public groups need no membership and no Sales Navigator; a private group is readable only by a member seat.
cursorstringContinue from a previous page. Omit for the first page.
countintegerMembers per page (default 50, max 100). LinkedIn refuses anything higher.
intoListstringAppend this page’s members to one of your lead lists, by name or id. Combine with exhaust to walk the group into it without any of it crossing your context.
exhaustbooleanPage server-side until the results run out, instead of returning a cursor for you to send back. Requires intoList: every page is appended there and NO hits come back, only the totals — which is the point, since neither the hits nor the cursor then cross your context. Stops at maxResults and hands back a cursor to resume from. Each page still costs one metered action.
maxResultsintegerHow many hits an exhaust run collects before it stops (default 500, max 2000). Ignored without exhaust. Raise it when you know the audience is large and you mean to spend the actions.

icp_create

Create an ICP

  • write

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. **Read the icp-definition skill first**: it carries the check rules and the filter defaults, and an ICP written without them disqualifies everyone while looking right.

Parameters: icp_create
NameTypeWhat it is for
namerequiredstringA short, specific name — the segment, not the pitch.
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. AI CHECKS: normally ONE per side — an identity fact confirmable from PUBLIC data, phrased so "met" reads as good. One that cannot be confirmed reads as not_met and disqualifies, emptying the ICP. Read the icp-definition skill.

icp_delete

Delete an ICP

  • write
  • destructive
  • idempotent

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.

Parameters: icp_delete
NameTypeWhat it is for
icpIdrequiredstring

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.

icp_qualify

Record a qualification verdict against an ICP

  • write
  • open-world

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.

Read the full description

**Filters run FIRST and outrank your answers.** When rejectedByFilter is set, a title/location/headcount rule from the ICP's Sales Navigator criteria decided the verdict and your check answers were discarded unread (checksNotRead). That is the ICP being narrow, not the person failing: if your reading says they fit, show the operator requirement vs actual and offer to loosen that rule with icp_update, then qualify again. Different answers cannot overturn a filter.

Parameters: icp_qualify
NameTypeWhat it is for
targetrequiredobjectWho this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id.
kind"profile" | "company"Override auto-detection. Required for a company addressed by universal name or numeric id.
icpVersionIdrequiredstring
resultsobject[]= []One entry per criterion you judged, keyed by criterionId from the ICP version.

icp_update

Update an ICP

  • write

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. Past versions stay readable, so earlier verdicts remain explainable. **Read the icp-definition skill** before you change the AI checks — it decides whether the new version still qualifies anyone.

Parameters: icp_update
NameTypeWhat it is for
icpIdrequiredstring
namerequiredstringA short, specific name — the segment, not the pitch.
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. AI CHECKS: normally ONE per side — an identity fact confirmable from PUBLIC data, phrased so "met" reads as good. One that cannot be confirmed reads as not_met and disqualifies, emptying the ICP. Read the icp-definition skill.

icp_version_restore

Bring an earlier ICP definition back

  • write

Bring an earlier version back by minting a NEW version equal to it, which becomes the one in use straight away — unlike the campaign equivalent, there is no draft to review first, so confirm with the operator before calling. Nothing is destroyed: versions are append-only, every earlier one stays, and a restore is itself undone by restoring again. Read icp_versions first to pick the right one.

Parameters: icp_version_restore
NameTypeWhat it is for
icpIdrequiredstring
versionrequiredintegerThe version to bring back, as icp_versions listed it.

icp_versions

Read an ICP's earlier definitions

  • read-only
  • idempotent

Every version of this ICP, newest first, with the date and the check counts for each — and which one is in use. An ICP is versioned on EVERY edit and none is ever rewritten, so this is the whole history. Pass version to get that one in full: its thesis, both filter sets, both check lists, as they were saved. This is how you recover a definition an icp_update cleared by leaving fields out.

Parameters: icp_versions
NameTypeWhat it is for
icpIdrequiredstring
versionintegerOmit for the list of versions. Give one to get THAT version in full.

invitations_received

Pending connection requests

  • read-only
  • idempotent
  • open-world

The connection requests waiting in your LinkedIn inbox — people who asked to connect with YOU and have not been answered. The one audience that came looking for the operator, and until now nothing read it. Each hit is one request: person (the inviter), note (what they wrote when they asked, verbatim — often the reason they want in) and receivedAt. Page with cursor; 100 per page, one page per metered action. exhaust walks every page into the list, and like every other walk it returns the counts rather than the rows — read a page at a time when you want to READ the notes. Read-only: this does not accept or decline anything.

Parameters: invitations_received
NameTypeWhat it is for
cursorstringContinue from a previous page. Omit for the first page.
countintegerInvitations per page (default 50, max 100).
intoListstringAppend the people who invited you to one of your lead lists, by name or id. Combine with exhaust to walk every pending request into it.
exhaustbooleanPage server-side until the results run out, instead of returning a cursor for you to send back. Requires intoList: every page is appended there and NO hits come back, only the totals — which is the point, since neither the hits nor the cursor then cross your context. Stops at maxResults and hands back a cursor to resume from. Each page still costs one metered action.
maxResultsintegerHow many hits an exhaust run collects before it stops (default 500, max 2000). Ignored without exhaust. Raise it when you know the audience is large and you mean to spend the actions.

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 ({ type, value }: url, public_identifier, or provider_id). The kind is detected from the target; pass kind: 'company' for one addressed by universal name or numeric id. Returns complete data, metered and cached (refresh: true forces a live read), plus headroom — how many MORE fetches you may make (own account plus Kairon's shared capacity, combined). Budget a large audit against the smallest number in it, and keep going until nextSlotAt is set: only that field means wait. A counter at 0 does not — the next call comes from shared capacity. For a person, withRecommendations: true also returns who vouched for them — each recommender named and addressable, the strongest warm-intro path LinkedIn publishes. When the person is a 1st-degree connection of this seat, relative.emails and relative.phones carry their contact panel — visible via the connection, never stored.

Parameters: linkedin_fetch
NameTypeWhat it is for
targetrequiredobjectWho this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id.
kind"profile" | "company"Override auto-detection. Required for a company addressed by universal name or numeric id.
refreshbooleanTrue bypasses the cache and forces a live read from LinkedIn. Slower, and it spends a lookup against your headroom.
withRecommendationsbooleanTrue also returns who recommended this person (and who they recommended), with each recommender identified. Warm-intro paths. Always spends a live lookup — never served from cache.

list_add

Add members to a list

  • write

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.

Parameters: list_add
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
itemsrequiredobject[]The rows to add, each naming its subject by ONE of memberId, url, providerId, or snapshot.

list_create

Create a list

  • write

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.

Parameters: list_create
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
namerequiredstringThe list's name, for the operator to recognise it.

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, 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. Custom list columns reach a message two ways: {{@columnName}} sends that column's value word for word, and a normal AI slot that mentions a column can use it as a fact, written in the writer's own words.

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'.
qstringCase-insensitive substring of the list's name.
cursorstring
limitinteger= 25

list_members

Read a list's members

  • read-only
  • idempotent

One keyset page of a list's members. Row: { id, title, subtitle, location, linkedinUrl, target, company, companyId, companyRecordId, email, values, tags, owner, qualification, employeeCount, currentTitle, crmStage } — the id removes it with list_remove or writes with list_set. Leads carry currentTitle (the job; subtitle is the headline) and crmStage (what your team DID); companies carry employeeCount. Pass target to linkedin_fetch AS-IS; linkedinUrl is null for Sales Nav members.

Read the full description

NARROW HERE — every filter below runs in SQL; never page a list and sift it yourself. qualification is only the verdict. includeQualification: true (both assets) adds qualificationDetail per row — criteria, rationales, evidence — which answers 'why were these disqualified' in ONE call. A filter or sort the asset lacks is REFUSED, never ignored. employeeCount is null when nobody has learned it, and null matches NEITHER size bound, so the halves never add up. Page with cursor.

Parameters: list_members
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
qstringCase-insensitive substring over the member's display fields.
cursorstring
limitinteger= 25
tagsstring[]Keep only members carrying EVERY tag named — an AND, not an OR. Matched case-insensitively; a name no tag has yields an empty page rather than an error.
sort"person" | "title" | "company" | "email" | "industry" | "qualification" | "owner" | "added" | "source"Which column to order by. Leads: person, title, company, qualification, owner, added, source. Companies: company, industry, qualification, owner, added. Naming a field the asset does not have is refused. Omit for the default, newest-added first.
direction"asc" | "desc"Order direction; read as asc once a sort field is named.
qualificationStatusstring | string[]Keep members whose ICP verdict is ANY OF these: qualified, disqualified, unevaluated. unevaluated means nobody has judged them — never that they failed.
ownerIdstring | string[]Keep members owned by ANY OF these user ids. Ownership rides the entity, not the list.
tagIdstring | string[]Keep members carrying ANY OF these tag ids — an OR, and the counterpart to tags above, which is an AND over NAMES.
addedAfterstringKeep members added to THIS list at or after this ISO timestamp.
addedBeforestringKeep members added to THIS list strictly before this ISO timestamp.
hydratedbooleantrue keeps members whose LinkedIn facts have been loaded; false keeps the ones nobody has fetched yet (they have no size or HQ to filter on).
columnobject[]Keep members matching EVERY predicate over this list's custom columns. A name the list does not have is refused, so a typo cannot read as 'no matches'.
includeQualificationbooleanAttach each member's stored qualification scorecard — the same body qualification_get returns, so a half-rejected list explains itself in ONE call instead of one per member. This is how you answer 'why were these disqualified'. Works on both assets. Off by default because it is BIG — every criterion with its rationale and evidence — so pair it with a small limit (10-25) rather than a full page.
signalKeystring | string[]Leads only. Keep members that arrived from ANY OF these signals.
crmStage"new" | "invite_sent" | "connected" | "contacted" | "replied" | "interested" | "meeting_booked" | "won" | "on_hold" | "lost" | "discarded"[]Leads only. Keep members standing at ANY OF these pipeline stages.
employeeMinintegerCompanies only. Keep companies with AT LEAST this many employees.
employeeMaxintegerCompanies only. Keep companies with AT MOST this many employees.
industrystring[]Companies only. Keep companies carrying ANY OF these industries, matched case-insensitively against the whole industry list, not just the one shown as subtitle.
hqCountrystring[]Companies only. Keep companies whose HQ is in ANY OF these ISO-2 country codes.

list_qualify

Qualify a list against an ICP

  • write
  • open-world

Judge a list's members against an ICP and return the run that started. Omit subjectIds to cover every live member; name them to qualify a subset. This SPENDS — each member Kairon has to judge costs a model call and, for anyone not loaded yet, a LinkedIn read.

Read the full description

Members already carrying a verdict against this ICP's ACTIVE version are skipped for free, so re-qualifying a list you have judged before costs only what is new. The run is NOT instant: it returns immediately with status: "pending" and a total, and you read what it did with list_qualify_runs. Ids that are no longer members are dropped silently and total counts what survived.

Parameters: list_qualify
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
icpIdrequiredstringThe ICP to judge against. Its ACTIVE version is pinned when the run starts.
subjectIdsstring[]The members to judge. Omit to cover every live member of the list. Ids that are no longer members are dropped, and the run reports what survived as its total.
ignoreIcpMismatchboolean= falseStart the run even if this list turns out not to match the ICP. Kairon normally stops a run once disqualifications run well past its allowance, and says which criterion did the damage. Set this ONLY after reading that answer and deciding the list is meant to be thin — never on a first attempt, and never to get past a stop you have not looked at.

list_qualify_cancel

Stop a qualification run

  • write

Stop a run that is still going. Every verdict it already reached is KEPT — cancelling costs nothing and undoes nothing, it only stops the spending from here on. The members it never reached keep no verdict and stay unevaluated. Refused on a run that already finished, because answering "fine" to a brake that stopped nothing would report a saving that never happened.

Parameters: list_qualify_cancel
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
runIdrequiredstringThe run to stop.

list_qualify_runs

Read a list's qualification run

  • read-only
  • idempotent

How a qualification run is going, or how it ended. Without runId, this list's most recent run; with one, that run. Returns null when the list has never been qualified, which is an answer and not a failure.

Read the full description

Read the counts as four different things: judged is what it paid to decide (split into qualified and disqualified), alreadyCurrent was skipped free because a verdict against this ICP version already stood, and failed could not be READ — those members keep no verdict and stay unevaluated, so they are worth retrying later. A completed run with stopReason: "allowance_exhausted" stopped short: start another over the same members to resume, and it will be nearly free for whatever the first run reached.

Parameters: list_qualify_runs
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
runIdstringA specific run. Omit for this list's most recent one.

list_remove

Remove members from a list

  • write

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.

Parameters: list_remove
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
itemsrequiredobject[]The rows to remove, each named by any one identity the member could have been added with.

list_set

Set members' custom-column values

  • write

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. When a value is a fact you looked up rather than one the operator told you, put the source in the cell — the quote or number, the URL you read it on, and the date. Values the operator owns (a stage, a note, your own summary) need none of that. Custom list columns reach a message two ways: {{@columnName}} sends that column's value word for word, and a normal AI slot that mentions a column can use it as a fact, written in the writer's own words.

Parameters: list_set
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
itemsrequiredobject[]The rows to write, each naming its subject plus the custom-column values to set on it.

list_update

Rename a list, or set its custom columns

  • write

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.

Read the full description

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. Custom list columns reach a message two ways: {{@columnName}} sends that column's value word for word, and a normal AI slot that mentions a column can use it as a fact, written in the writer's own words.

Parameters: list_update
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstring
namerequiredstringThe list's name. ALWAYS required here, even when you only mean to change columns — send the current name back to keep it.
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.

newsletter_subscribers

List a newsletter’s subscribers

  • read-only
  • idempotent
  • open-world

The people who subscribed to YOUR OWN LinkedIn newsletter — the warmest audience LinkedIn holds, since each one read your writing and chose to keep receiving it. Pass the newsletter URL or its id. Every page reports total, the size of the whole audience, so you can decide whether to walk it before spending the actions: 100 subscribers per page, one page per metered action. Each subscriber comes back with networkDistance: 1 when they are ALREADY a connection (message them) and null when they are not (invite first). Works only for newsletters the connected seat OWNS — LinkedIn serves this list to the author and nobody else, so there is no way to read a competitor’s.

Parameters: newsletter_subscribers
NameTypeWhat it is for
newsletterrequiredstringThe newsletter, as its URL (https://www.linkedin.com/newsletters/<name>-<id>/) or its bare numeric id. It must be a newsletter the connected seat OWNS — LinkedIn serves this list to the author only.
cursorstringContinue from a previous page. Omit for the first page.
countintegerSubscribers per page (default 50, max 100). LinkedIn refuses anything higher — asking for 250 fails the whole call, it does not clamp.
intoListstringAppend this page’s subscribers to one of your lead lists, by name or id. Combine with exhaust to walk the whole audience into it without any of it crossing your context.
exhaustbooleanPage server-side until the results run out, instead of returning a cursor for you to send back. Requires intoList: every page is appended there and NO hits come back, only the totals — which is the point, since neither the hits nor the cursor then cross your context. Stops at maxResults and hands back a cursor to resume from. Each page still costs one metered action.
maxResultsintegerHow many hits an exhaust run collects before it stops (default 500, max 2000). Ignored without exhaust. Raise it when you know the audience is large and you mean to spend the actions.

owner_set

Assign an owner to leads or companies

  • write
  • idempotent

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.

Parameters: owner_set
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of row these items are — people or companies.
itemsrequiredobject[]The rows to assign, each naming its subject by ONE of memberId, url, or providerId.
ownerrequiredobject | nullWho to assign every item to — one org member, by userId or email. The same owner is set on all of them.

pipeline_board

Read the pipeline board

  • read-only
  • idempotent

Where every one of this org's people stands. Returns a count for each stage — new, invite_sent, connected, contacted, replied, interested, meeting_booked, won, on_hold, lost, discarded — and, when you name a stage, that column's people with their role, their owner, and when they were last contacted. EVERY lead is on this board, so new is where the ones nobody has acted on yet collect; the early columns then separate an invitation nobody accepted (invite_sent) from an accepted one nobody followed (connected) from a message that got no reply (contacted). Use lastContacted to ask about a window ("who at interested have we not touched in 30 days"). A column comes back one page at a time: hasMore says whether more remain, and passing the returned cursor back reads the next page. Reads only: it never moves anyone.

Parameters: pipeline_board
NameTypeWhat it is for
stage"new" | "invite_sent" | "connected" | "contacted" | "replied" | "interested" | "meeting_booked" | "won" | "on_hold" | "lost" | "discarded"Return this column's people as well as the counts. Omit for counts only, which is what "how is the pipeline doing" needs.
lastContactedstringNarrow to people last contacted inside a window: 7d, 30d, 90d, never, or an explicit YYYY-MM-DD..YYYY-MM-DD. Narrows the counts as well as the people.
cursorstringResume the named column from a previous call — pass the cursor it returned.
limitinteger= 25How many people to return when stage is named.

pipeline_stage_set

Move people on the pipeline board

  • write
  • idempotent

Record where people now stand: new, invite_sent, connected, contacted, replied, interested, meeting_booked, won, on_hold, lost, or discarded. Name them by url (what you usually have), memberId, or providerId. Moving someone backward is allowed. Kairon advances the early stages on its own from what actually happens — an invitation going out, them accepting it, a message going out, them writing back — so use this for the judgment calls it cannot make: a meeting booked, a deal won or lost, someone parked. Two stages are not rungs on the ladder: on_hold keeps someone and does nothing for now — Kairon also puts people there itself when a campaign runs out of things to say and they never replied — and discarded means never contact them again — it also adds them to the exclusion list, which ends any campaign they are in and drops messages already scheduled for them. Never creates a lead: an unknown url comes back not_found.

Parameters: pipeline_stage_set
NameTypeWhat it is for
itemsrequiredobject[]The people to move, each naming its subject by ONE of memberId, url, or providerId.
stagerequired"new" | "invite_sent" | "connected" | "contacted" | "replied" | "interested" | "meeting_booked" | "won" | "on_hold" | "lost" | "discarded"Where they now stand. The ladder runs new → invite_sent → connected → contacted → replied → interested → meeting_booked → won | lost. new is where a lead nobody has acted on sits; invite_sent is an invitation not yet accepted; connected is accepted with nothing said yet; contacted means there is a message they can read. You may move someone backward as well as forward. Two stages sit outside the ladder, for people nobody is working: on_hold keeps them and does nothing for now, and discarded means never contact them again — which also adds them to the exclusion list, ending any campaign they are in.

post_interactions

Who engaged with one post

  • read-only
  • idempotent
  • open-world

Everyone who commented on or reacted to ONE named post, by its LinkedIn URL — the audience a post already earned, each person carrying HOW they engaged (their comment text, or which reaction), which is the warmest opening line outreach gets. Use this when you have the post; use signal_search with engaged_with_profile when you want whoever keeps engaging with a profile. A shortened lnkd.in link carries no post id — expand it first. Comments are walked to exhaustion, then reactions; pass intoList with exhaust to sweep the whole post into a list in one call, and expect several metered reads.

Parameters: post_interactions
NameTypeWhat it is for
urlrequiredstringThe post — its LinkedIn permalink (…/posts/… or …/feed/update/…) or its bare urn:li:activity|ugcPost|share:<id>. A shortened lnkd.in link does NOT work: it carries no post id, so expand it first.
intoListstringWhere this page's PEOPLE are appended — always a leads list. Only the person is added; the post and how they engaged are why they are here, not members. Pair with exhaust to sweep the whole post in one call.
cursorstring
exhaustbooleanPage server-side until the results run out, instead of returning a cursor for you to send back. Requires intoList: every page is appended there and NO hits come back, only the totals — which is the point, since neither the hits nor the cursor then cross your context. Stops at maxResults and hands back a cursor to resume from. Each page still costs one metered action.
maxResultsintegerHow many hits an exhaust run collects before it stops (default 500, max 2000). Ignored without exhaust. Raise it when you know the audience is large and you mean to spend the actions.

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
targetrequiredobjectWho this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id.
cursorstring

qualification_get

Read one member's qualification verdict

  • read-only
  • idempotent

WHY one lead or company carries the verdict it does ({ asset, id }) — the stored scorecard. Ids come from list_members. Reads rows we hold: spends nothing, changes nothing.

Read the full description

Returns status, the tallies total / met / notMet / notChecked, and criteria — one { criterion, result } per criterion the ICP version defines — a filter's dimension / mode / values and the member's actual, an AI check's description and the judge's rationale + evidence. result: null is NOT CHECKED — the walk stopped earlier — never a failure, though it keeps them out of qualified. On a lead, manualQualification (with manualQualificationBy) means a PERSON set status by hand; the tallies still describe what the criteria found, which may disagree. Null means Kairon decided. icpVersionId is the version pinned when it was judged, not today's active one. An unevaluated member answers status: 'unevaluated' with no criteria — an answer, not a failure; next is list_qualify.

Parameters: qualification_get
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of list: 'leads' (people) or 'companies'.
idrequiredstringThe member's id, exactly as list_members returned it — a lead id under asset: 'leads', a company id under asset: 'companies'.

qualification_set

Decide about people yourself

  • write
  • idempotent

Say whether these people are worth reaching out to, in your own judgement, instead of waiting for Kairon to decide. qualified lets someone through even if your ideal-customer profile would reject them; disqualified keeps someone out even if it would accept them. Pass null to take your decision back. Name them by url, memberId, or providerId.

Read the full description

Your decision is what the lists show, what the filters match, and what the campaigns obey, for as long as it stands. Kairon keeps judging underneath it — qualification_get still shows what the criteria found, though its status now reads your decision — so taking your decision back hands the person straight back to what it had decided, with nothing to re-run and nothing to pay for. Use it for the calls Kairon cannot make: someone you know personally, someone whose profile does not say what you know about them, or a good lead you need in a campaign that leaves today. Never creates a lead: an unknown url comes back not_found.

Parameters: qualification_set
NameTypeWhat it is for
itemsrequiredobject[]The people to decide about, each naming its subject by ONE of memberId, url, or providerId.
qualificationrequired"qualified" | "disqualified" | nullYour own verdict: qualified to say these people are worth reaching out to even if the ICP disagrees, disqualified to keep them out even if it agrees. Pass null to take your verdict back and let the ICP decide again. Your verdict wins for as long as it stands: it is what the lists show, what the filters match, and what the campaigns obey — while Kairon goes on judging underneath, so taking it back costs nothing and restores what the ICP had decided.

search_people

Search people (classic)

  • read-only
  • idempotent
  • open-world

Classic LinkedIn people search — the door for ONE kind of ask: something measured from the operator themselves. networkDistance, connectionsOf and followersOf are measured from the operator here, and shared capacity REFUSES them — so a network-scoped ask that search_sales_navigator has refused belongs here, and **nothing else does**. **capabilities.viewerRelativeFilters: false is NOT a reason to come here**: Sales Navigator search runs on every seat and with no seat at all, so every company and people search that is not network-scoped still belongs there — coming here instead trades the role / seniority filters for a noisier keyword answer. Takes keywords, id-resolved filters, network distance and language; or paste a search url to run verbatim. Pass intoList to append the page.

Parameters: search_people
NameTypeWhat it is for
urlstring
keywordsstring
cursorstring
limitintegerPage size. Defaults to 10 when sampling, 50 when intoList is set — previewing an audience and collecting one are different jobs. Classic search is clamped to 50 provider-side. COLLECTING AN AUDIENCE? The response carries total: compare it to the hits you have, and keep calling with the returned cursor and the SAME intoList until cursor is null. Pages append server-side, so the list grows without hits crossing your context, and one page costs one metered action whatever its size.
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[]LinkedIn provider ids (ACoAA…, from people.suggest — NOT numeric member ids) whose connections to search within. Aimed at someone you are NOT connected to, this returns your MUTUAL connections with them — the people who could introduce you.
followersOfstring[]LinkedIn provider ids (ACoAA…, from people.suggest — NOT numeric member ids) whose followers to search within.
advancedKeywordsobject
intoListstring
exhaustbooleanPage server-side until the results run out, instead of returning a cursor for you to send back. Requires intoList: every page is appended there and NO hits come back, only the totals — which is the point, since neither the hits nor the cursor then cross your context. Stops at maxResults and hands back a cursor to resume from. Each page still costs one metered action.
maxResultsintegerHow many hits an exhaust run collects before it stops (default 500, max 2000). Ignored without exhaust. Raise it when you know the audience is large and you mean to spend the actions.

search_posts

Search LinkedIn posts

  • read-only
  • idempotent
  • open-world

Search LinkedIn POSTS — what is being said, by whom, when. keywords is required (boolean OR/AND/NOT allowed); everything else NARROWS. mentioningCompany / mentioningMember are @-mentions resolved by NAME to a LinkedIn id — the way to watch what the market says about a competitor, a partner, or you. fromCompany / fromMember restrict the AUTHOR instead; authorCompany / authorIndustry / authorKeywords describe who wrote it. A name that resolves to nothing REFUSES the search rather than silently widening it. One page is one metered action; page with cursor. Want the PEOPLE who engaged with these posts instead? signal_search with engaged_with_keyword.

Parameters: search_posts
NameTypeWhat it is for
datePosted"past-24h" | "past-week" | "past-month"One of LinkedIn's own recency windows.
sortBy"relevance" | "date"Result order. Defaults to relevance.
contentType"videos" | "images" | "live_videos" | "collaborative_articles" | "documents"Only posts carrying this kind of attachment.
firstDegreeOnlybooleanOnly posts by the seat's 1st-degree connections.
postedByMebooleanOnly the seat's own posts.
postedByFollowedbooleanOnly posts by people the seat follows.
fromMemberstring[]Author NAMES — only posts written by these people.
fromCompanystring[]Company NAMES — only posts published by these pages.
mentioningMemberstring[]Only posts that @-mention these people, by NAME.
mentioningCompanystring[]Only posts that @-mention these companies, by NAME.
authorIndustrystring[]Industry NAMES — only posts whose AUTHOR works in one of them.
authorCompanystring[]Company NAMES — only posts whose AUTHOR works at one of them.
authorKeywordsstringFree text matched against the AUTHOR's profile, e.g. a job title.
keywordsrequiredstringText matched against the post body. Boolean OR/AND/NOT allowed.
cursorstring

search_sales_navigator

Search (Sales Navigator)

  • read-only
  • idempotent
  • open-world

Search Sales Navigator over people OR companies — **the door for EVERY people and company search, on every seat and with none**. No licence to check, nothing to ask the operator for. A connection degree is a structured filter like any other, so "my 1st-degree X" belongs here too (networkDistance), keeping the role / seniority precision classic search cannot express. ONE exception announces itself: the viewer-measured filters (networkDistance, connectionsOf, saved lists) are sometimes refused mid-call — that refusal, never a flag read beforehand, is what sends a degree-scoped ask to search_people. Prefer a structured filter over keywords: filters are precise where keywords are noisy. Pass human-readable names ({ type, query }); the server resolves ids. Same-dimension OR, different dimensions AND. **Read the sales-nav-search skill first.** One page is one metered action. **One search at a time** — a concurrent call is refused (CHANNEL_ACTION_SEARCH_IN_PROGRESS).

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. Defaults to 10 when sampling, 100 when intoList is set — previewing an audience and collecting one are different jobs. Sales Navigator allows up to 100. COLLECTING AN AUDIENCE? The response carries total: compare it to the hits you have, and keep calling with the returned cursor and the SAME intoList until cursor is null. Pages append server-side, so the list grows without hits crossing your context, and one page costs one metered action whatever its size.
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. MATCHING: a pill matches a SET OF WORDS, not a phrase, and it matches a person across ALL of their CURRENT jobs. When you also scope by company, only their position AT THAT COMPANY is returned — so a hit whose shown title does not match your pills is usually a CORRECT match through a side venture, not a broken filter. Founder/Co-Founder/Owner/CEO anchored to a company list therefore mean "anyone there who founded or owns anything, anywhere"; to find who RUNS a company use Gerente General/Director General/Managing Director/Country Manager, and linkedin_fetch to check one odd hit.
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. Measured from the SEAT running the search, not from the operator. A degree IS a structured filter, so with the org's OWN Sales Navigator seat this is the right place to ask for "my 1st-degree X" — you keep the role / seniority precision classic search cannot express. Do NOT downgrade to search_people for it. identity_get says whether it can be answered here (capabilities.viewerRelativeFilters) — but do not gate the SEARCH on it: every other filter runs either way, so drop this one and keep the rest rather than leaving. When it cannot be answered the search says so and refuses THIS filter only; classic search_people with networkDistance is then the route, because it always runs on the operator's own seat. Never treat a Sales-Nav hit as connected: confirm before building a campaign that sends a direct message.
connectionsOfstring[]LinkedIn provider ids (ACoAA…, from people.suggest — NOT numeric member ids) whose connections to search within. Aimed at someone you are NOT connected to, this returns your MUTUAL connections with them — the people who could introduce you — matching the shared_connections_count on their profile. networkDistance defaults to [1] and must STAY [1] for such a target: at any other degree Sales Navigator silently discards this filter and answers with your entire 2nd-degree network, which looks like a real result. The one exception is a target you ARE connected to, where [2] validly returns everyone they know that you do not. Looking for an introduction to someone? Read the warm-intro skill first — this filter is one step of it, and the rest decides which of the people it returns can actually help. Like networkDistance this is relative to the SEAT, and works here whenever identity_get's capabilities.viewerRelativeFilters is true. When it is not, the search refuses THIS filter and nothing else — classic search_people is the route for the introduction hunt, while every other Sales Navigator filter still runs here.
changedJobsbooleanSpotlight: only people who recently changed jobs.
postedOnLinkedinboolean
mentionedInNewsboolean
followingYourCompanyboolean
pastColleaguebooleanSpotlight, relative to YOU: only people you actually worked with. Combined with connectionsOf it answers "which of this person's connections are my own former colleagues" — the shortest warm path there is. Expect a very small result; that is the point, not a misfire.
sharedExperiencesbooleanSpotlight, relative to YOU: only people who share a school, employer or group with you. The wider net beneath pastColleague, and the same combination with connectionsOf applies. The response never says WHICH experience is shared — membership in the result is the signal; load a profile to name the overlap.
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
exhaustbooleanPage server-side until the results run out, instead of returning a cursor for you to send back. Requires intoList: every page is appended there and NO hits come back, only the totals — which is the point, since neither the hits nor the cursor then cross your context. Stops at maxResults and hands back a cursor to resume from. Each page still costs one metered action.
maxResultsintegerHow many hits an exhaust run collects before it stops (default 500, max 2000). Ignored without exhaust. Raise it when you know the audience is large and you mean to spend the actions.
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.

source_delete

Detach a lead list's signal source

  • write
  • destructive
  • idempotent

Detach the source from a lead list. The list KEEPS every member the source already found — only the feed stops, so the list becomes an ordinary static list rather than losing anything. Frees the one-source-per-list slot, so a new source can be attached afterwards with source_set. To pause a source temporarily instead, source_set it to a paused status.

Parameters: source_delete
NameTypeWhat it is for
listIdrequiredstringThe lead list whose source this addresses, from list_list or list_create. At most one source exists per list, so this is the source's address — there is no separate source id.

source_get

Read a lead list's signal source

  • read-only
  • idempotent

The source attached to a lead list — the recurring feed that makes it a SIGNAL-BASED list, growing itself over time. Returns null when the list has none, which means it is an ordinary static list, not that anything failed. The result carries the ICP the source targets and every signal on it, each with its own id — those ids are what source_set echoes back to keep a signal's place in its feed. Read this before changing a source: source_set replaces the whole signal set.

Parameters: source_get
NameTypeWhat it is for
listIdrequiredstringThe lead list whose source this addresses, from list_list or list_create. At most one source exists per list, so this is the source's address — there is no separate source id.

source_run

Run a lead list's signal source now

  • write
  • open-world

Run the source now instead of waiting for its daily run, and return the run that started. This SPENDS — it is a real provider search on the seat the source was created on, and it is the only tool here that costs anything. Attaching a source does not run it, so this is how a newly attached list first fills. Refused while the source is paused, or while one of its runs is already in flight; a run is not instant, so read what it did with source_runs rather than expecting members back from this call.

Parameters: source_run
NameTypeWhat it is for
listIdrequiredstringThe lead list whose source this addresses, from list_list or list_create. At most one source exists per list, so this is the source's address — there is no separate source id.

source_runs

Read a signal source's runs, or one run's candidates

  • read-only
  • idempotent

What the source actually did. Without runId, one page of run history newest-first — when each run happened and how many people it appended. With runId, that run's CANDIDATES instead: one row per person it considered and the outcome that decided them, which is where "why is this person in my list, and why is that one not?" is answered. Page with the returned cursor; a run's candidates come back in the order the run touched them, so the page order IS the funnel order. Reads only — it never starts a run and spends nothing.

Parameters: source_runs
NameTypeWhat it is for
listIdrequiredstringThe lead list whose source this addresses, from list_list or list_create. At most one source exists per list, so this is the source's address — there is no separate source id.
runIdstringA run id from this tool's own summary page. Given, the result is THAT run's candidates — one row per person the run considered, with the outcome that decided them. Omitted, the result is the run history: when each ran, and how many it appended.
limitintegerRows per page, 1-100. Defaults to 25.
cursorstringThe cursor from the previous page. A cursor minted for the run history cannot be used against a run's candidates, or the other way round — a mismatched token restarts from the first page rather than paging the wrong thing.

source_set

Attach or replace a lead list's signal source

  • write
  • idempotent

Make a lead list SIGNAL-BASED, or change the one it already is: give it an ICP to target and the signals that should feed it. This REPLACES the whole signal set — any signal you do not list is removed — so call source_get first and send back every signal you want to keep, each with the id it gave you. A kept signal holds its place in its feed; one sent without its id is treated as new and restarts from the top, which costs a re-walk. Attaching does NOT run the source: nothing is searched or spent until source_run. Swapping to a different ICP restarts every signal, because the audience changed. Refusals are total — a bad config or an unknown signal id leaves the source exactly as it was.

Parameters: source_set
NameTypeWhat it is for
listIdrequiredstringThe lead list whose source this addresses, from list_list or list_create. At most one source exists per list, so this is the source's address — there is no separate source id.
icpIdrequiredstringThe ICP this source targets, from icp_list. Its ACTIVE version is both the audience searched and the gate every candidate must pass — so revising the ICP changes future runs. Swapping to a DIFFERENT ICP restarts every signal from the top of its feed, because the audience changed.
signalsrequiredobject[]The COMPLETE set of signals this source should carry — at least one. Any signal not listed here is removed. Echo each kept signal with the id source_get gave it, or it is treated as new and restarts from the top of its feed.
status"active" | "paused"Whether the source keeps feeding the list. paused stops it without losing anything — the signals, their places in their feeds, and every member already found all survive, and resuming carries on where it left off. Use this rather than source_delete to stop a list growing for a while. Omitted leaves the current setting; a new source starts active.
contactFilterobjectWho this list may add, by how much you have already talked to them — the same rule a campaign enrolls by. Omitted leaves the current setting; a new source adds everyone. Narrow it to stop a list filling up with people already in a conversation.

tag_delete

Delete a tag

  • write
  • destructive

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.

Parameters: tag_delete
NameTypeWhat it is for
idrequiredstring

tag_list

Read the organization's tags

  • read-only
  • idempotent

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.

Takes no parameters.

tag_save

Create or update a tag

  • write

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.

Parameters: tag_save
NameTypeWhat it is for
idstring
namestringThe tag name. Required when creating; on an existing id it renames the tag everywhere.
color"stone" | "clay" | "moss" | "sky" | "plum" | "amber" | "rust" | "slate"Display colour for the tag. Omit and one is chosen.

tag_set

Set which tags a lead or company carries

  • write
  • idempotent

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.

Parameters: tag_set
NameTypeWhat it is for
assetrequired"leads" | "companies"Which kind of row these items are — people or companies.
itemsrequiredobject[]The rows to tag, each naming its subject by ONE of memberId, url, or providerId.

webdata_find

Find a data endpoint outside LinkedIn

  • read-only
  • idempotent
  • open-world

Search, in plain words, for a third-party data endpoint Kairon can run for you — data OUTSIDE LinkedIn: tweets by handle, a Google Maps listing, a company's reviews, a person's record at a data broker, and hundreds more. Say what you want ("tweets by a handle", "amazon reviews for a product"), not a provider name. Each match carries a provider, an endpoint, a relevance score and the provider's price per call or per result. Free. Then webdata_inspect the match you like to learn its inputs.

Parameters: webdata_find
NameTypeWhat it is for
queryrequiredstringWhat data you are after, in plain words. Up to 1000 characters.
limitintegerHow many matches to return. Default 5, at most 40.

webdata_inspect

Read one data endpoint's inputs and price

  • read-only
  • idempotent
  • open-world

The full card for one endpoint webdata_find returned, by its provider and endpoint: what it does, the exact input schema webdata_run expects (path, query and body parameters), the provider's price, a link to the provider's own docs, and any notes. Free. Read this BEFORE quoting an endpoint — the schema is what you must send, and the input you send decides the cost.

Parameters: webdata_inspect
NameTypeWhat it is for
providerrequiredstringThe provider slug from webdata_find, e.g. apify.
endpointrequiredstringThe endpoint path from webdata_find, e.g. /apidojo/tweet-scraper.

webdata_quote

Price a data run before running it

  • read-only
  • idempotent
  • open-world

What THIS organization will pay for one run of an endpoint with exactly this input — the provider's price plus Kairon's margin — and the quoteId that webdata_run requires. Per-call endpoints are quoted exactly. Per-result endpoints are quoted per result; pass expectedResults (the limit you set in input, or your honest estimate) to get an estimatedUsd. THAT TOTAL IS AN ESTIMATE, NOT A PRICE: say it to the operator as approximate ('about $0.60'), never as the exact amount, because the provider bills what actually comes back — more or fewer results than you expected. An endpoint that states no price comes back with quoteId: null and CANNOT be run — say so and pick another. Free. YOU MUST show the operator the quoted price in plain words and get their yes BEFORE calling webdata_run — the run refuses without a quote, and a quote is only valid for 30 minutes and for this exact input.

Parameters: webdata_quote
NameTypeWhat it is for
providerrequiredstringThe provider slug from webdata_find, e.g. apify.
endpointrequiredstringThe endpoint path from webdata_find, e.g. /apidojo/tweet-scraper.
inputobjectThe endpoint's parameters, as { body?, queryParams?, pathParams? } — each section filled per the matching section of its webdata_inspect schema. Omit for an endpoint that takes none. Must be IDENTICAL between webdata_quote and webdata_run.
expectedResultsintegerFor a per-result endpoint: how many results you expect back — the cap you put in input, or your honest estimate. Turns a per-result price into an estimatedUsd.

webdata_result

Read back a data run that was still going

  • read-only
  • idempotent
  • open-world

Fetch the outcome of a webdata_run that came back with status other than COMPLETED, FAILED, BLOCKED, STOPPED or TIMED_OUT — a run the provider was still working on when the 60-second wait ended. Pass the runId it returned. One read, no waiting: if it is still going, wait a few seconds and call again. Reading is free; the run itself is billed once, when it completes, at the quoted rate — billedUsd says how much.

Parameters: webdata_result
NameTypeWhat it is for
runIdrequiredstringThe runId a still-running webdata_run returned.

webdata_run

Run a data endpoint (spends the organization's allowance)

  • write
  • open-world

Run one endpoint and get its data back. REQUIRES the quoteId from webdata_quote for this exact provider, endpoint and input, and you must have shown the operator that price and received their yes — a run with no quote, a stale quote or a quote for different input is refused. THIS SPENDS the organization's monthly allowance, at the quoted rate, on what the provider actually returns. status: COMPLETED means the endpoint was called and answered; check providerStatus for whether it answered well (a 404 there is "not found", not a failure of this tool). billedUsd says what the run cost the organization. An output over 64 KB is cut and flagged truncated: true: a list keeps its shape and loses its tail (omittedItems says how many went), anything else is left out entirely and only outputPreview plus outputBytes come back. Narrow the input rather than re-running — a re-run is a second charge.

Parameters: webdata_run
NameTypeWhat it is for
providerrequiredstringThe provider slug from webdata_find, e.g. apify.
endpointrequiredstringThe endpoint path from webdata_find, e.g. /apidojo/tweet-scraper.
inputobjectThe endpoint's parameters, as { body?, queryParams?, pathParams? } — each section filled per the matching section of its webdata_inspect schema. Omit for an endpoint that takes none. Must be IDENTICAL between webdata_quote and webdata_run.
quoteIdrequiredstringThe quoteId webdata_quote returned for this exact provider, endpoint and input.

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; startNow (default true) enrolls today's first batch instead of waiting for the next window. Refused with CAMPAIGN_ACTIVATION_BLOCKED (409) unless all four preconditions hold — params.unmet names each one. **Call campaign_stats first and show the operator what activating will do**: under an ICP gate it returns who the ICP accepted, rejected and never judged, plus when the last person gets in — and no dates while anyone is unjudged, because the gate freezes whoever it rejects. Above zero unjudgedCount, say so and offer list_qualify on the bound lists (campaign_getleadListIds); a current verdict is a free cache hit. unreadableCount is NOT work to offer: those profiles cannot be read, and they never withhold the dates. Activating an active campaign is a no-op, safe to retry. See the campaign-building skill.

Parameters: campaign_activate
NameTypeWhat it is for
startNowboolean= trueDefault true: enroll today's first batch immediately instead of waiting for the next send window to open.
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.

campaign_copy

Who still needs a per-lead line

  • read-only
  • idempotent

Per-lead slot coverage. A {{@slot}} in a send body (either draft mode — an ai_template seed fills it before the writer runs) is filled by each lead's LIST COLUMN of the same name (case-insensitive) — write values with list_set, never a separate copy call. 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.

Parameters: campaign_copy
NameTypeWhat it is for
idrequiredstring
missingOnlyboolean= falseTrue returns only the leads still MISSING copy — the "what is left to write" read. A lead is not enrolled until every slot is filled, so these are the people the campaign is silently holding back.
cursorstring
limitinteger= 50

campaign_create

Create a draft campaign

  • write

Create a draft campaign: a name and the sending seat (channelAccountId, a connected LinkedIn account in this org — seat_list returns the ones you can pick), and the ICP to gate enrollment on (icpId). There is no default and nothing is selected for you: omitting icpId creates an UNGATED campaign that contacts everyone on its lists. ASK the operator first, every time — show them their ICPs (icp_list) and say in one line what each answer means: gated, and only people who qualify are contacted while everyone else on their lists is skipped and never messaged; ungated, and everyone is contacted. "No gate" is a fine answer, but it has to be one they gave. 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.

Parameters: campaign_create
NameTypeWhat it is for
namerequiredstringThe campaign's name, for the operator to recognise it in a list. Not seen by any lead.
channelAccountIdrequiredstringThe sending seat — a connected LinkedIn account in this org. Every send comes FROM it. Get the id from seat_list — the seats that can send right now. (identity_get's seatId names your own account even when it is not sendable, so a campaign built on it is created, publishes fine, and is then refused by campaign_activate with sender_not_connected.)
icpIdstringThe ICP to gate enrollment on. There is no default and nothing is selected for you: omitting this creates an UNGATED campaign that contacts everyone on its lists. ASK the operator before calling, every time — never decide it for them and never leave it unasked. Show them their ICPs (icp_list) and say in one line what each answer means: gated, and only people who qualify for that ICP are contacted while everyone else on their lists is skipped and never messaged; ungated, and everyone on their lists is contacted. "No gate" is a fine answer, but it has to be one they gave.

campaign_get

One campaign in full — config, audience and both graphs

  • read-only
  • idempotent

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.

Parameters: campaign_get
NameTypeWhat it is for
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.

campaign_lead_start

Start one queued person today, ahead of the pacing

  • write
  • open-world

Enroll one person from the campaign's audience now, instead of waiting for tomorrow's batch. It skips the DAILY PACING, never the gate: the same ICP and enrollment-filter checks the morning tick runs still judge the lead. So the outcome is data, not a promise — enrolled, skipped (with the reason, e.g. they do not match the ICP), excluded, or failed. A lead who does not fit is marked Skipped rather than messaged.

Parameters: campaign_lead_start
NameTypeWhat it is for
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.
leadIdrequiredstringThe lead to start, as campaign_runs or list_members returned its id.

campaign_list

List the org’s campaigns

  • read-only
  • idempotent

List this org's campaigns, keyset-paginated — { q?, status?, sort?, direction?, cursor?, limit? }. The default order is the one the app shows: the campaigns that are RUNNING first — active, then draft, then paused, then finished, then archived — and the most recent first inside each. q matches the campaign name; status filters to one of draft | active | paused | archived; sort reorders by name | status | sender | updated; 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.

Parameters: campaign_list
NameTypeWhat it is for
qstringCase-insensitive substring of the campaign's name.
statusstring | string[]Return only campaigns in these statuses — one, or several as a comma-separated list (e.g. draft,active,paused). Omit for every status, including the finished and archived ones the app hides by default.
initiativestring | "none"Return only campaigns serving this initiative. The literal none returns the campaigns serving no initiative yet — the candidates an initiative can attach (PRD-79 IN8), since a campaign serves at most one.
cursorstringOpaque keyset token from the previous page's nextCursor. Omit for the first page.
limitinteger= 25Campaigns per page, 1-100.
sort"name" | "status" | "sender" | "updated"Order the page by this column instead of the default one. The default puts the campaigns that are running first — active, then draft, then paused, then finished, then archived — and the most recent first inside each of those.
direction"asc" | "desc"Which way sort runs. Omit it to get the column read the way the app reads it: names and senders A→Z, statuses running-first, dates newest-first.

campaign_pause

Pause an active campaign

  • write
  • idempotent

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.

Parameters: campaign_pause
NameTypeWhat it is for
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.

campaign_preview

Read what a step would actually send, before anyone gets it

  • read-only
  • idempotent
  • open-world

READ THE MESSAGE BEFORE YOU ACTIVATE. Drafts one saved step for up to 3 real audience leads through the SAME pipeline a real send uses, and sends nothing. Name the step by its nodeKey from campaign_get; omit leadIds to sample the audience. Per lead you get the body that would fire, its reasoning and evidence, an error where no message could be produced, and the send-verifier's verification verdict (advisory here — a preview never blocks). The reply names which graph it read. **SLOW: expect up to ~2 minutes**, because the LinkedIn profile reads behind the drafts are paced one at a time per seat — do not retry a call that has not returned.

Parameters: campaign_preview
NameTypeWhat it is for
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.
nodeKeyrequiredstringWhich step to preview — the key of a node in the stored graph, as campaign_get returned it. Must be a step that writes something: send_invite, send_message or comment_on_post. Any other type is refused, because it has no message to show you.
leadIdsstring[]Which real leads to draft for, at most 3. Each must already sit in this campaign's bound audience; one that does not comes back as lead_not_in_audience rather than failing the call. OMIT IT and the first few of the audience are sampled for you, which is what you want unless you are checking one named person.
graph"published" | "draft"Which stored graph to read the step from. Defaults to published — what the engine is actually sending — and falls back to draft when nothing has been published yet. Ask for draft to check an edit you have not published. A {{@slot}} in a draft-graph step previews UNRESOLVED when nobody has written that line yet, which is every slot before the first publish, since per-lead copy is written against the published graph; a slot whose name already carries copy from an earlier publish renders normally here.

campaign_publish

Validate and publish the campaign's draft graph

  • write

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.

Parameters: campaign_publish
NameTypeWhat it is for
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.
migrateInFlightbooleanDefault false: publishing pins the new version for FUTURE enrollments only, and runs already in flight finish on the version they started. True repoints them onto the new version by node key — a run parked on a step the new version removed is CANCELED, not migrated.

campaign_review_feed

The review desk — what is about to fire, and what is waiting on you

  • read-only
  • idempotent

The operator's Tasks page, as one read: every send booked to fire (autopilot), every manual run waiting for approval (review), every AI draft that errored (failed), and everything already acted on today (done), plus each seat's daily budget. Filter with kind and campaignId. This is where the runId and nodeRunId that every per-run tool takes come from — a review row's parked send has no other index. Each row's body holds the words that will go out — an invite's note, a message, a comment — told apart by actionType, so "what are we about to send" is one call. It is a live WINDOW: limit applies per kind so one busy kind cannot hide another, and windowCapped: true means total is a floor. To read one campaign exhaustively use campaign_runs, which pages with a cursor; for one row's reasoning, citations and timeline, open it with campaign_run_get.

Parameters: campaign_review_feed
NameTypeWhat it is for
kind"autopilot" | "review" | "failed" | "done"Which slice: autopilot (booked, about to fire), review (waiting for approval on a manual campaign), failed (the AI draft errored), done (already acted today). Omit for all four.
campaignIdstringOnly rows belonging to this campaign — applied to the desk WINDOW, not to the campaign. The desk holds a bounded slice of the org, so a campaign whose rows fall outside it comes back empty; windowCapped: true is how you tell that apart from "nothing is queued". For an exhaustive per-campaign read use campaign_runs, which pages with a cursor.
limitintegerRows PER KIND, 1-200, default 25 — so an unfiltered call can return up to FOUR times this. Spent per kind on purpose: one busy kind must not push the others off the page, and a review row is reachable from no other index. Pass kind to spend the whole budget on one. Raise it only when you actually need the long tail: every row carries the draft it will send.

campaign_run_approve

Approve a manual campaign’s parked send — it goes out

  • write
  • destructive
  • open-world

THIS SENDS THE MESSAGE. On a manual campaign every send waits for a human; this releases one. Read the draft first with campaign_run_get — approving without reading is how an agent sends words nobody checked. If the send was held by the pre-send check, approving is an OVERRIDE and is recorded against your name on the message itself. Refused (409) if the run is not awaiting approval, or if its campaign is paused — resume it first. Edit the words first with campaign_send_edit.

Parameters: campaign_run_approve
NameTypeWhat it is for
runIdrequiredstringOne run's id — starts with srun_, as campaign_runs or campaign_review_feed returned it. A run is one enrolled lead. NOT a feed row's id or nodeRunId.

campaign_run_cancel

End these people’s sequences — nothing further is sent to them

  • write
  • destructive
  • idempotent

Stop up to 100 enrolled leads: each run ends, anything drafted or booked for them is dropped, and their parked workflow is torn down so no timer fires later. Nothing already SENT is recalled. This is final for that campaign — a canceled person moves to skipped and it will not pick them up again (re-adding them via another bound list is the only way back). Ids are DEDUPED, so read canceled rather than counting what you sent. Every distinct id comes back with its own outcome: canceled, not_found, not_cancelable once a run has already finished, or failed — that one alone is unknown, so re-send it. A wrong-KIND id never gets that far: ids are checked for the srun_ prefix at the door, so a feed row’s nodeRunId is refused outright rather than counted as a retryable failure. Idempotent on an already-canceled run.

Parameters: campaign_run_cancel
NameTypeWhat it is for
runIdsrequiredstring[]The runs to end — each starts with srun_, as campaign_runs or campaign_review_feed returned them. NOT a feed row's id or nodeRunId: on an autopilot row those name the SEND, and a batch that accepted them would report a retryable failure for an id that can never succeed.

campaign_run_get

One sequence run, step by step

  • read-only
  • idempotent

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.

Parameters: campaign_run_get
NameTypeWhat it is for
runIdrequiredstringOne run's id, as campaign_runs returned it — a run is one enrolled lead.

campaign_run_skip_wait

Collapse the delay this run is waiting on

  • write

Cut short the delay step a run is parked on so the sequence carries straight on to its next step. Refused on a run that is NOT on a delay: a booked send is campaign_send_now, and a wait_connection_accepted cannot be skipped at all — the skip would not match the wait it is parked on, and no amount of skipping makes someone accept an invite. DO NOT RETRY a refusal: once the wait is skipped the run has moved past it, so a second call reports CAMPAIGN_RUN_NOT_ACTIONABLE for work that already succeeded.

Parameters: campaign_run_skip_wait
NameTypeWhat it is for
runIdrequiredstringOne run's id — starts with srun_, as campaign_runs or campaign_review_feed returned it. A run is one enrolled lead. NOT a feed row's id or nodeRunId.

campaign_runs

The campaign's sequence runs

  • read-only
  • idempotent

Read the campaign's sequence runs — one per enrolled lead — keyset-paginated; page with the returned cursor. Each run carries the lead, where it stands, when it acts next, its outcome once terminal, and any error or skip detail. bucket filters to one activity tab (waiting, didnt_accept, didnt_answer, replied, interested, skipped, error); omit for every run. stage takes the same words as a SET; q, dateFrom/dateTo and listId narrow further, and filters stack. nodeKey + dateFrom/dateTo asks **who completed THAT step on those days** — the only way to tell a first message from a follow-up when a sequence has several, since campaign_stats counts them as one number. Without nodeKey the day window holds runs whose LAST event or next BOOKED action fell in it. campaign_stats says which bucket is worth reading. includeQualification: true attaches each lead's STORED scorecard — qualification_get's body — which is big, so pair it with a small limit.

Parameters: campaign_runs
NameTypeWhat it is for
bucket"waiting" | "didnt_accept" | "didnt_answer" | "replied" | "skipped" | "error" | "interested"Filter to one activity bucket: waiting, didnt_accept, didnt_answer, replied, interested (a subset of replied), skipped (an ICP / enrollment-filter verdict or an operator stop), or error. Omit for every run.
stagestring | string[]Activity states to include; several read as "any of these".
nodeKeystringOnly runs that reached this step, by the node key the graph gave it — including runs that have since moved past it or finished. Pair it with dateFrom/dateTo to ask who completed THIS step on those days, which is the only way to tell a first message apart from a follow-up in a sequence with more than one message step.
qstringFind a person by name, headline, company or LinkedIn handle.
dateFromstringEarliest day to include, inclusive (YYYY-MM-DD). Dates each run by its LAST event — unless nodeKey is also set, which dates it by that step instead.
dateTostringLatest day to include, inclusive (YYYY-MM-DD). Dates each run by its LAST event — unless nodeKey is also set, which dates it by that step instead.
listIdstring | string[]Only people enrolled from these source lists.
cursorstring
limitinteger= 50
idrequiredstring
includeQualificationbooleanAttach each run's stored qualification scorecard (the same body qualification_get returns for its lead). Off by default because it is BIG — every criterion with its rationale and evidence — so pair it with a small limit (10-25) rather than a full page.

campaign_send_edit

Rewrite a booked send before it fires

  • write
  • idempotent

Replace the words a drafted or scheduled send will fire with — the edited text is exactly what goes out, never re-written afterwards. Addressed by nodeRunId (from campaign_review_feed), NOT the run id. Refused (409) once the send is out; there is no editing a sent message. On a send held by the pre-send check, editing CLEARS the warning: it described a sentence that no longer exists, and your own words are yours. An InMail may carry a new subject; every other send type refuses one.

Parameters: campaign_send_edit
NameTypeWhat it is for
nodeRunIdrequiredstringThe booked send — starts with nrun_, as campaign_review_feed returned its nodeRunId. NOT the run id. Null on a feed row means nothing is drafted yet, so there is nothing to act on.
bodyrequiredstringThe exact text that will be sent — this REPLACES the draft, it is not an instruction to rewrite it. Empty is allowed only on a connection request, which then fires with no note. On a MESSAGE: To send this as SEVERAL messages rather than one, separate them with a line containing only ---. Each part arrives as its own LinkedIn message a couple of seconds later, in the same conversation — what a person does when they type a short opener and then the reason they wrote. The whole thing still counts as ONE send against the daily limit. This is the same thing the "Add bubble" button does in the campaign editor. On every other send type a --- line is sent as written.
subjectstringAn InMail subject, and only that: refused on every other send type. It is the whole of what the recipient sees before deciding to open, and a credit is spent either way.

campaign_send_now

Fire this person’s next send immediately

  • write
  • destructive
  • open-world

THIS SENDS THE MESSAGE, sooner than anything else would have: it fires this run's next booked send NOW, outside the send window and the usual pacing. It is a HAND-OFF — the run is armed and the sender fires it a moment later, so campaign_review_feed shows expediteNextSend: true until it goes. DO NOT RETRY: a second call is refused with CAMPAIGN_RUN_NOT_ACTIONABLE because the run is already armed, which means the send is on its way, not that it failed. Only a run parked on a send with a reviewed draft can be expedited; one sitting on a delay is refused (use campaign_run_skip_wait).

Parameters: campaign_send_now
NameTypeWhat it is for
runIdrequiredstringOne run's id — starts with srun_, as campaign_runs or campaign_review_feed returned it. A run is one enrolled lead. NOT a feed row's id or nodeRunId.

campaign_set_graph

Replace the campaign's draft sequence graph

  • write
  • idempotent

Replace the campaign's editable DRAFT sequence graph IN FULL — this overwrites, it is not a patch. Nodes are { key, type, config } with a stable key you choose; edges are { fromKey, toKey, condition } routing one node's outcome to the next. Layout is automatic. Which conditions a type emits, and which types it may connect to, are fixed rules — the edges field carries the table, and a violation is refused (400). The response is the saved graph, in full, in exactly the shape this tool accepts — read the echo back rather than assume your input was stored as sent. **The campaign-building skill has the order to build a campaign in, and how to write the copy.** Saving is not publishing.

Parameters: campaign_set_graph
NameTypeWhat it is for
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.
graphrequiredobjectThe whole draft sequence. This REPLACES the existing draft graph — it is not a patch, so send every step you want to keep. Saving is not publishing: the graph stays invisible to every run until campaign_publish.

campaign_stats

How the campaign is doing

  • read-only
  • idempotent

How a campaign is performing, in one read — and **the read before campaign_activate**. 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), and copy coverage, which is why queuedCount can sit under audienceSize. Under an ICP gate it also carries the forecast: qualifiedCount / disqualifiedCount / unjudgedCount / unreadableCount over the unenrolled audience, enrollmentEndsOn (when the last person gets in) and finishesOn. They are null when there is no split — no ICP gate, or nobody left to enrol. **Both dates are null while unjudgedCount is above zero** — the gate freezes whoever it rejects, so a date then counts people it will drop; qualifyEtaMinutes is that check's cost. unreadableCount never withholds a date: re-checking it cannot help. funnel runs enrolled → invited → accepted → messaged → replied → interested. buckets counts where everyone stands.

Parameters: campaign_stats
NameTypeWhat it is for
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.
days7 | 30Window for the daily activity series, 7 or 30, in the campaign's own local days. OMIT IT and the series is left out entirely rather than defaulted — do not pay to read one you were not asked for.

campaign_update

Edit the campaign's configuration and audience

  • write
  • idempotent

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. It is also how a campaign is pointed at the bet it serves, via initiativeId (null detaches) — do that for every campaign you build under an initiative, or the bet reports an empty funnel forever. 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).

Parameters: campaign_update
NameTypeWhat it is for
namestringThe campaign's name, for the operator to recognise it. Not seen by any lead.
sendWindowobjectWhen sends may fire. The ONE piece of configuration with no default — campaign_activate is blocked with no_send_window until it exists. Monday-Friday 12:00-18:00 in the operator's own zone is a sane start.
autonomy"manual" | "autopilot"autopilot (the default) fires each send on schedule. manual parks EVERY send for the operator to approve first, so a manual campaign looks active and sends nothing until someone works the queue.
allowsReEntrybooleanMay a person enter this campaign more than once? false (the default) means someone who finished it never comes back — the rule every campaign has always had. true lets them re-enter later, which is what a nurture campaign is for. It never permits two messages at once: one live run per person, always, whatever this says.
dailyEnrollmentLimitintegerHow many leads may enroll per day, 1-200 (default 25). Caps the START of new runs, not sends — the org's own per-seat limits still apply on top.
priorityinteger | nullWhich campaign this LinkedIn account serves first when two of them are due on the same day. 0 is highest. Lower sorts first, matching every other priority in Kairon. null clears it — no priority, served after Low; omitted leaves it unchanged. It is an ORDER, never an allowance: it does not change how many leads any campaign enrols (dailyEnrollmentLimit does that), and it never moves a send that is already scheduled.
enrollmentFilterobjectWhich prior-contact history may enroll, and whose. Default is everyone. A narrowed filter SKIPS the people it excludes rather than queueing them, so it is a common reason a big audience enrolls almost nobody.
icpIdstring | nullThe ICP that gates enrollment. null clears it (enroll everyone); omitted leaves it unchanged. Must be an ICP in this org. Leads that fail it are marked Skipped, never messaged.
stopOnReplybooleanDefault true — a reply halts that person's run so nothing follows a real conversation. false keeps the sequence going after they answer; set it only when the operator asked.
connectionDegree"any" | "only_first" | "exclude_first"Whether the sender's existing LinkedIn connection matters. any (the default) ignores it. only_first enrolls ONLY people the sender is already connected to. exclude_first enrolls only people they are NOT connected to — the usual choice for cold outreach that opens with an invite. Anything but any costs one extra LinkedIn read per candidate, and skips the people it excludes rather than queueing them.
stopOnCompanyReply"off" | "any_reply" | "interested"Whether one person answering stops their colleagues. off (the default) works every lead on their own merits. any_reply stops the rest of a company once anyone there answers THIS campaign. interested stops it only when the answer was an interested one, so a colleague's "no thanks" leaves the others working. Colleagues are matched on their LinkedIn employer, so someone whose employer Kairon has never seen is never stopped by it. It stops people waiting to enter AND people already inside — but never someone who has written to you, whose conversation always continues.
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.
leadListIdsstring[]Lead lists to bind, ADDITIVELY. A list already bound stays bound, and a bound list you leave out is NOT unbound — omission cannot unbind, because that would cancel every in-flight run drawn from the list.
initiativeIdstring | nullThe initiative this campaign serves. null detaches it; omitted leaves it unchanged. A campaign serves at most ONE bet, so a different id MOVES it. Detaching changes nothing else — a campaign that outlives its bet keeps sending.

campaign_version_restore

Bring an earlier sequence graph back as the draft

  • write
  • destructive
  • idempotent

Copy an earlier published version back into the campaign's editable DRAFT, steps and routing and layout intact — the way to undo an edit that replaced copy someone wrote. It does NOT publish: leads keep getting the live version until someone publishes the draft, and in-flight runs are untouched either way. It REPLACES any unpublished draft, so read campaign_versions first if one exists. Publishing afterwards mints the next version number; the old one stays where it is.

Parameters: campaign_version_restore
NameTypeWhat it is for
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.
versionNumberrequiredintegerThe version to bring back, as campaign_versions listed it.

campaign_versions

Read the campaign's earlier sequence graphs

  • read-only
  • idempotent

Every version this campaign has published, newest first, plus its unpublished draft — with a line per version saying what it changed (steps added/removed/edited, routing). Pass versionNumber to get that version's whole graph back, message bodies and all, in exactly the shape campaign_set_graph accepts — so you can hand it straight back instead of retyping copy. History is publish-boundary: edits between two publishes overwrote one draft and were never stored.

Parameters: campaign_versions
NameTypeWhat it is for
idrequiredstringThe campaign id, as campaign_list or campaign_create returned it.
versionNumberintegerOmit for the list of versions. Give one to get THAT version’s full graph, in exactly the shape campaign_set_graph accepts.

checkpoint_delete

Withdraw a checkpoint

  • write
  • destructive

Remove one checkpoint from a bet’s history. Deleting the newest one moves the initiative’s health back to whatever the one before it said — or to unknown when there is none. Use it for an entry published against the wrong bet or by accident, never to tidy away a verdict somebody did not like.

Parameters: checkpoint_delete
NameTypeWhat it is for
initiativeIdrequiredstringThe initiative the checkpoint belongs to.
checkpointIdrequiredstringThe checkpoint id (ckpt_…), as initiative_get returned it in the history.

checkpoint_edit

Correct a checkpoint

  • write
  • idempotent

Fix what one checkpoint says, or the verdict it froze — for a typo or a verdict typed by mistake. The entry keeps the date it was made and its author, and every surface marks it as edited. A checkpoint that has gone out of date is NOT fixed here: publish a newer one instead, so the history shows the change of mind. A rewritten body carries @mentions the same way checkpoint_publish does.

Parameters: checkpoint_edit
NameTypeWhat it is for
bodystringWhat happened and the ONE thing to do next, in markdown. Say the numbers you read and how they compare to this org’s own baseline, and whether the last checkpoint’s recommendation was actually done. To put a teammate’s name on a next step, mention them: [@Ana](#[email protected]) — a normal markdown link whose destination is #kairon- plus their user id, email, or name (spaces as +). Whichever you use is stored as their id, and a name nobody here matches, or one two teammates share, is refused rather than saved as prose. Read user ids and names off seat_list.
health"on_track" | "at_risk" | "off_track"The verdict, frozen at publish: on_track, at_risk or off_track. Judge VOLUME before rates — a low number of sends is not a bad result, it is not a result yet.
initiativeIdrequiredstringThe initiative the checkpoint belongs to.
checkpointIdrequiredstringThe checkpoint id (ckpt_…), as initiative_get returned it in the history.

checkpoint_publish

Publish a checkpoint on a bet

  • write

Record a dated verdict on an initiative: what happened, the one thing to do next, and a health of on_track, at_risk or off_track. The verdict is FROZEN — publishing a newer one never rewrites an older one, which is what makes the history evidence. Read the gtm-checkpoint skill first; it carries the rule about judging volume before rates.

Parameters: checkpoint_publish
NameTypeWhat it is for
bodyrequiredstringWhat happened and the ONE thing to do next, in markdown. Say the numbers you read and how they compare to this org’s own baseline, and whether the last checkpoint’s recommendation was actually done. To put a teammate’s name on a next step, mention them: [@Ana](#[email protected]) — a normal markdown link whose destination is #kairon- plus their user id, email, or name (spaces as +). Whichever you use is stored as their id, and a name nobody here matches, or one two teammates share, is refused rather than saved as prose. Read user ids and names off seat_list.
healthrequired"on_track" | "at_risk" | "off_track"The verdict, frozen at publish: on_track, at_risk or off_track. Judge VOLUME before rates — a low number of sends is not a bad result, it is not a result yet.
initiativeIdrequiredstringThe initiative being judged (init_…).

initiative_delete

Withdraw a go-to-market bet

  • write
  • destructive

Remove one initiative from the board. A soft delete: the campaigns serving it detach and keep running, and its checkpoints survive. Use it for a bet created by mistake — most often a duplicate left behind by an initiative_set that omitted initiativeId. Never use it to retire a bet that ran: set its status to completed or canceled with initiative_set, so the board keeps what was learned.

Parameters: initiative_delete
NameTypeWhat it is for
initiativeIdrequiredstringThe initiative to remove, by id (init_…).

initiative_get

Read one go-to-market bet

  • read-only
  • idempotent

One initiative whole: the thinking in its body, the campaigns serving it as the campaigns list knows them, and every checkpoint newest-first with the verdict each one froze. Read this before publishing a checkpoint — the last one says what was recommended, and whether it was done is the question a checkpoint has to answer.

Parameters: initiative_get
NameTypeWhat it is for
initiativeIdrequiredstringThe initiative to read, by id (init_…).

initiative_list

List the go-to-market bets

  • read-only
  • idempotent

Every initiative in the organization: what is being bet on, its status, the health of its latest checkpoint and how many days old that verdict is, plus the summed funnel of the campaigns serving it. Read this before proposing work — a campaign that serves no bet is how fifteen open fronts happen.

Parameters: initiative_list
NameTypeWhat it is for
status"draft" | "active" | "completed" | "canceled"Return only initiatives in this state. Omit for all of them, including completed and canceled ones.

initiative_set

Create or edit a go-to-market bet

  • write

Write down a bet: a name, the thinking in markdown, and who is accountable for it. Omit initiativeId to create, pass one to edit. Call initiative_list FIRST and reuse the id of any bet that already covers this — a second call that omits the id creates a duplicate rather than updating what you just wrote. Health is NOT a field here and never will be — an initiative reports the verdict of its newest checkpoint, so the way to move it is checkpoint_publish. Read the gtm-initiative skill before creating one.

Parameters: initiative_set
NameTypeWhat it is for
namestringShort, scannable name for the bet — "Expansión Centroamérica". REQUIRED when creating (no initiativeId); optional when editing. A create without one is refused rather than defaulted.
bodystring | nullThe thinking, in markdown: what is being bet on, why, what would prove it worked, and what has been learned. Free-form on purpose — write what an operator would want to reread in three weeks. Put a teammate on a step by mentioning them: [@Ana](#[email protected]), a normal markdown link whose destination is #kairon- plus their user id, email, or name (spaces as +). It is stored as their id; a name nobody here matches, or one two teammates share, is refused. seat_list names them.
status"draft" | "active" | "completed" | "canceled"draft while it is being shaped, active once it is being worked.
priorityinteger | null0 is highest. Lower sorts first, matching every other priority in Kairon.
startDatestring | nullYYYY-MM-DD, this century. A bet starts on a date, not at a moment.
endDatestring | nullYYYY-MM-DD, this century. When the bet should have paid off, or be called. Must not be before startDate.
initiativeIdstringOmit to create a new initiative. Pass an id (init_…) to edit that one. Call initiative_list FIRST and reuse the id of any bet that already covers this — omitting it a second time creates a duplicate, it does not update.
ownerobject | nullWho is accountable for this bet — one org member, by email (what you usually have) or userId. null leaves it unassigned; omit to leave it as it is.

insights_report

How the whole account is performing

  • read-only
  • idempotent

The org-wide outreach funnel over a date window — invites sent → accepted → messages → replied → interested — with the daily activity series and a per-seat breakdown, each seat carrying its campaigns and their acceptance, reply and interested RATES. This is the account-level question campaign_stats cannot answer: that one needs a campaign named first, and this spans every campaign and every seat. Narrow to one seat with userId (from the breakdown). Days are bucketed in your own stored timezone, not UTC.

Parameters: insights_report
NameTypeWhat it is for
fromrequiredstringFirst day of the window, YYYY-MM-DD, inclusive.
torequiredstringLast day of the window, YYYY-MM-DD, inclusive. Must be on or after from, and the window must not exceed 366 days.
userIdstringNarrow to one seat owner. Omit for every seat in the organization.

Inbox & outreach

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

chat_list

List conversations

  • read-only
  • idempotent
  • open-world

List conversations, newest activity first, across every channel that holds them. Each chat says which one it is in channel ("linkedin" or "email"). Defaults to YOUR OWN seat — pass scope: "org" for every seat in the organization, or seatId for one teammate. **Email conversations belong to no seat, so they appear only under scope: "org".** 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; canReply says whether you can answer it from here. Page with the returned cursor. Reading is free: never metered, never against a sending limit.

Parameters: chat_list
NameTypeWhat it is for
seatIdstringOne teammate's seat id, from seat_list. Defaults to your own.
scope"mine" | "org"'mine' (default) is your own seat; 'org' is every seat in the organization.
unreadbooleanTrue returns only chats with unread messages.
answered"yes" | "no"'no' returns chats waiting on a reply from you (they spoke last); 'yes' returns the ones you already answered. Chats whose history has not been loaded yet match neither.
leadsOnlybooleanOnly chats already linked to a lead.
needsHumanbooleanOnly chats where Kairon handed the conversation back and is waiting on you.
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 — LinkedIn or email. 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. An email message also carries its subject, and isAutoReply when the channel flagged it machine-sent — an out-of-office is a real message but not an answer. 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) to the person target addresses. Requires a live InMail credit on the seat and nothing else - any paid LinkedIn plan that grants them will do; refused fast without one. 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.

Parameters: inmail_send
NameTypeWhat it is for
targetrequiredobjectWho this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id.
subjectstringThe InMail subject line — the one thing that decides whether it is opened.
textrequiredstringThe InMail body, exactly as it will be sent. Nothing rewrites it.
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
targetrequiredobjectWho this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id.
notestringThe connection-request note, up to 300 characters. Omit it entirely for a note-less invite, which is valid and often accepts better.

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.
textrequiredstringThe replacement body. It overwrites the message for the recipient too.

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 (an id from chat_list), which is what you want when it exists; an EMAIL chat is refused, because Kairon cannot send email yet — 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.

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.
textrequiredstringThe message body, exactly as it will be sent. Nothing rewrites it.
attachmentsobject[]
clientTokenstringYour own idempotency key. Send the SAME token when retrying after a lost response and the message is not sent twice.

Acting as someone else

Whose seat is this running on?

identity_get

Who you are acting as

  • read-only
  • idempotent

Who this connection acts as: the user, their organization, the seat (seatId plus its state: connected, none, or locked — needs a subscription), linkedin, and capabilities. Call it after impersonate_start/impersonate_stop and before anything that sends, so you know whose account a message leaves from. seat also explains a refusal. **search_sales_navigator always works — on every seat and with none.** capabilities.viewerRelativeFilters covers ONE part of a Sales Navigator search: whether filters measured from you (networkDistance, connectionsOf, saved lists) can be answered. False leaves the rest of the grammar — industry, headcount, seniority, function, tenure — running as on a paid seat, and is NEVER a reason to search elsewhere. When linkedin.hydrated is false, pass its publicIdentifier to linkedin_fetch. seatId names your own account even when it cannot send now (one awaiting a reconnect or 2FA keeps its id); seat_list is the seats that CAN send.

Takes no parameters.

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.

seat_list

List the seats you can send from

  • read-only
  • idempotent

The LinkedIn seats this organization can send FROM — the same list the campaign wizard's sender picker offers, every one connected and ready. Call it BEFORE campaign_create: a seat's id is exactly what that tool takes as channelAccountId, and what chat_list takes as seatId. ownerName is the teammate it belongs to, ownerUserId is that teammate's own id (what an @mention in an initiative or checkpoint body stores), and isSelf marks your own; any member may send from any of them, and the chosen seat's owner is the campaign's author. canSendInmail says whether an InMail step is legal on that seat — it gates that step and nothing else, never a search. authCountry is the country it authenticates from — use it to open a send window on noon where the seat lives. An empty list means no seat here is ready to send from. If you expected one, ask the operator to contact support at [email protected]. Free: it reads nothing on LinkedIn and spends no daily limit.

Takes no parameters.

Craft & config

How does this work, and what have you told me?

config_get

Read the operator's own material

  • read-only
  • idempotent

What the operator has told Kairon: their competitor and source URLs, and their organization's value proposition, lead magnet and learnings log. Read this FIRST when deciding what they should work on next. It holds no writing rules: for the rules any draft is held to, theirs and Kairon's own, call writing_check_list. Name sections to read only some.

Parameters: config_get
NameTypeWhat it is for
sections"competitorUrls" | "sourceUrls" | "valueProposition" | "leadMagnet" | "learnings"[]Which sections to read. Omit for all five.

config_set

Save the operator's own material

  • write
  • idempotent

Replace one or more sections of what the operator has told Kairon: competitorUrls, sourceUrls, 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. It writes no writing rules: writing_check_set is the only door to those, in every section.

Parameters: config_set
NameTypeWhat it is for
competitorUrlsstring[]Replaces every competitor URL. Each must be a LinkedIn URL.
sourceUrlsstring[]Replaces every source URL. Any http(s) URL.
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.

doc_get

Read one product doc

  • read-only
  • idempotent

One product doc in full, as markdown, by a slug from doc_list. Ground what you tell the operator in what it says — do not invent features, steps, limits or settings that are not in it, and when the doc states a constraint, report the constraint rather than working around it. Pair with skill_get when the question is not "what does Kairon do" but "how do I do this well".

Parameters: doc_get
NameTypeWhat it is for
slugrequiredstringWhich doc to read — a slug from doc_list, e.g. "campaigns".

doc_list

List the product docs

  • read-only
  • idempotent

Kairon's product reference — how each part behaves and where it stops. doc_get returns one in full, by a slug listed here. Call this BEFORE answering how Kairon works, what it can do, or why it is doing something — troubleshooting included ("nothing is sending", "is this a bug"). You do not know this product: it has documented rules that cause exactly those symptoms on purpose, and they are not guessable from other outreach tools. A guess tells an operator their working product is broken.

Takes no parameters.

email_domain_buy

Buy the sending domain

  • write
  • destructive
  • open-world

Order a sending domain and its mailboxes. **KAIRON pays, not the operator** — never quote them a price as something they will be charged; email_domain_quote's figures are Kairon's cost, and agreedTotal only makes the order refuse if that cost moved. What DOES need their explicit yes: the order cannot be undone, and the domain is registered and kept by Kairon's sending provider, so if they leave Kairon they cannot take it or its email history with them. Four endings, and only placed ordered anything — rejected means the name was refused, so pick another; payment_failed and checkout_required are both Kairon's to fix. The operator is charged nothing in any of them. Say which happened rather than assuming success. Mailboxes appear a few minutes after a successful order; then call email_warmup_start, because a mailbox that is not warming builds no reputation.

Parameters: email_domain_buy
NameTypeWhat it is for
domainrequiredstringThe domain name to buy and send from, like acme-outreach.com. Only .com and .org can be registered, and a name carrying a well-known trademark is refused. Not the customer's real website — a sending domain is a separate name, so a problem with it never touches their main domain's reputation.
mailboxesrequiredobject[]The mailboxes to create on the domain — one to five, each a real person on the team, since a recipient who replies is replying to that name. More mailboxes means more sending capacity and more monthly cost.
forwardingDomainstringThe customer's real website, where a visitor to the sending domain is sent — acme.com. Worth setting: a sending domain that resolves to nothing is exactly what a suspicious recipient checks. Any extension, not just .com/.org. Ask the operator; never guess it from their email address.
agreedTotalrequirednumberThe dueNow from the quote the operator agreed to, in whole dollars. The order is re-priced against the vendor immediately before buying and refused if the number moved, so a stale quote can never charge a different amount than the one shown.

email_domain_check

Check a sending domain’s DNS records

  • read-only
  • idempotent
  • open-world

Ask Instantly whether this domain's MX, SPF, DKIM and DMARC records are set up correctly. Use it after the customer edits their DNS: records take minutes to hours to spread, so calling this again IS the retry — a failing answer now is not a permanent one. The domain must be one this workspace already sends from.

Parameters: email_domain_check
NameTypeWhat it is for
domainrequiredstringA sending domain this workspace already has — read them from email_setup_get. Not any domain: this must be one Kairon sends from for this org.

email_domain_quote

What would this sending domain cost?

  • read-only
  • idempotent
  • open-world

Price a sending domain and its mailboxes. The figures come from Instantly simulating the real order, so they are exact — and nothing is bought, no card is touched. Show the operator dueNow and pass that same number back as agreedTotal if they say yes; the purchase re-prices and refuses if it moved. One to five mailboxes.

Parameters: email_domain_quote
NameTypeWhat it is for
domainrequiredstringThe domain name to buy and send from, like acme-outreach.com. Only .com and .org can be registered, and a name carrying a well-known trademark is refused. Not the customer's real website — a sending domain is a separate name, so a problem with it never touches their main domain's reputation.
mailboxesrequiredobject[]The mailboxes to create on the domain — one to five, each a real person on the team, since a recipient who replies is replying to that name. More mailboxes means more sending capacity and more monthly cost.
forwardingDomainstringThe customer's real website, where a visitor to the sending domain is sent — acme.com. Worth setting: a sending domain that resolves to nothing is exactly what a suspicious recipient checks. Any extension, not just .com/.org. Ask the operator; never guess it from their email address.

email_mailbox_add

Add a mailbox the customer already owns

  • write
  • idempotent
  • open-world

Add a mailbox the customer ALREADY owns — on their own domain, in their own Google Workspace or Microsoft account — so Kairon can send from it. Needs an APP PASSWORD, not the account password: for Google that is an app password created in their Google account, and 2-step verification must be on. Ask the operator for it directly; never guess one. Adding the same address twice is safe and creates nothing. The reply says whether the domain's DNS records (MX, SPF, DKIM, DMARC) pass yet — they often do not immediately, which is normal and does not undo the mailbox. Then call email_warmup_start.

Parameters: email_mailbox_add
NameTypeWhat it is for
emailrequiredstringThe full address of a mailbox the customer already owns, like [email protected].
firstNamerequiredstringThe mailbox owner's first name — it appears in the From line of every send.
lastNamerequiredstringThe mailbox owner's last name.
providerrequired"google" | "microsoft" | "custom"Who hosts the mailbox. google for Google Workspace or Gmail, microsoft for Microsoft 365 or Outlook — both fill in their own server settings. custom for anything else, and then all four host and port fields are required.
passwordrequiredstringAn APP PASSWORD for the mailbox, not the account password. For Google the operator creates one in their Google account with 2-step verification on. Ask them for it directly and never invent one; it is passed straight to the sending provider and stored nowhere in Kairon.
smtpHoststringOutgoing mail server, custom provider only (Google and Microsoft are known).
smtpPortintegerOutgoing mail port, custom provider only — usually 465 or 587.
imapHoststringIncoming mail server, custom provider only.
imapPortintegerIncoming mail port, custom provider only — usually 993.

email_plan_request

Ask Kairon for email sending or enrichment credits

  • write
  • idempotent

Ask Kairon to set this workspace up for email sending (plan), or to add enrichment credits (credits). Owner or admin only. Neither is charged automatically: a person at Kairon completes the purchase, and you will see it done when the workspace appears or the credit balance rises — so tell the operator it is being set up, not that it is ready. Asking twice for the same thing is refused; ask email_setup_get first to see what is already open.

Parameters: email_plan_request
NameTypeWhat it is for
kindrequired"plan" | "credits"plan to have Kairon set this workspace up for email sending at all; credits to add enrichment credits, which are what finds a prospect's email address. Ask for plan first — credits are useless without somewhere to send from.
quantityinteger= 1How many credit packs are wanted. Ignored for plan — there is one workspace.

email_setup_get

Read the email sending setup

  • read-only
  • idempotent
  • open-world

How this workspace sends email: whether it is on Kairon's sending plan, which sending domains it has and what each mailbox is doing (still being created, warming up, sending, or in trouble), how much of the plan is used, how many enrichment credits are left, and anything already requested from Kairon. Read this FIRST before adding a domain or a mailbox — it says whether the shop is even open for this org.

Takes no parameters.

email_warmup_start

Start warmup on a domain’s mailboxes

  • write
  • idempotent
  • open-world

Start warming up every mailbox on this sending domain. Warmup is what builds the reputation that keeps mail out of spam, and it takes about three weeks — Kairon attaches a mailbox to campaigns by itself once it is ready, so nothing else is needed after this. Safe to call twice: a mailbox already warming is left exactly as it is. If the mailboxes do not exist yet the reply says pending, which means a domain order is still being provisioned — wait and call again.

Parameters: email_warmup_start
NameTypeWhat it is for
domainrequiredstringA sending domain this workspace already has — read them from email_setup_get. Not any domain: this must be one Kairon sends from for this org.

feedback_report

Report a problem back to Kairon

  • write
  • open-world

Tell Kairon that something went wrong for this user. Call it the FIRST time a Kairon tool errors, returns something that clearly is not what was asked for, or refuses in a way you cannot explain, and also with no error at all when the user says Kairon is broken or asks again for something a Kairon tool has already answered. Kairon cannot see your conversation, so a problem you do not report here is a problem nobody at Kairon will ever learn about. It is free, spends no action and needs no seat. Do not report the same problem twice in a row; a second, different problem in the same conversation IS worth a second call. Never send credentials. Quote the user only where their own words explain the problem better than your summary does, and only the sentence that does it.

Parameters: feedback_report
NameTypeWhat it is for
problemrequiredstringWhat the user was trying to do, and what went wrong, in your own words. Concrete beats polite: name the entity, the filter, the campaign, whatever they were actually working on.
severityrequired"blocked" | "friction"blocked — the user cannot get what they came for and there is no way around it. friction — they got there, but it cost extra turns, a workaround, or visible annoyance. When unsure, say friction; a wrong blocked spends someone's attention on the wrong hour.
toolsobject[]The Kairon tools involved, in the order you called them. Include a tool that returned no error but the wrong answer — that case is the reason this field is not just an error string.
quotestringThe user's own words, when they explain the problem better than your summary. Their exact phrasing, no more than a sentence or two. Omit it rather than paraphrase — a paraphrase here is just your summary twice.

framework_get

Read one go-to-market axiom

  • read-only
  • idempotent

One axiom in full, as markdown, by a slug from framework_list. It is the argument and what to do about it, in the author's own words. Use it to ground advice and to explain WHY a playbook step exists; quote its reasoning to the operator in plain language, never the slug. Pair with skill_get for the how and doc_get for what Kairon itself does.

Parameters: framework_get
NameTypeWhat it is for
slugrequiredstringWhich axiom to read — a slug from framework_list.

framework_list

List the go-to-market axioms

  • read-only
  • idempotent

Kairon's go-to-market theory: the standalone truths every playbook rests on, each a slug, an area (belief-system, targeting, outreach, content, loop) and a title that says exactly what is inside. framework_get returns one in full. Read one when you need the WHY behind a step a playbook prescribes, when the operator questions the approach, or when you are about to advise on strategy rather than run a job. Titles only — cheap to list once per session.

Takes no parameters.

skill_get

Read one skill

  • read-only
  • idempotent

One playbook in full, as markdown, by a name from skill_list — call that first; an invented name is a 404. Read it BEFORE doing the job it covers, never after drafting: it encodes what Kairon measured to work, which is usually NOT what a general model would choose, so a draft written first is already wrong in the ways the playbook exists to prevent. Pair a writing skill with writing_check_list for the rules the draft is held to, and config_get for the operator's offer. Every playbook says WHAT to do; talking-to-the-operator says how to say it back to a founder who is not an engineer — fetch that one too if you have not already.

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

skill_list

List the available skills

  • read-only
  • idempotent

Kairon's playbooks — how to do each job well: onboarding, Sales Navigator, list building, campaigns, writing messages and posts, what to do next. Each row is a TEASER, not the craft: it hints at what a playbook covers so you can choose, and the instructions live only in the body. Never act on a row alone — including its own "pair with config_get" advice, which applies AFTER you skill_get that name, not instead of it. The catalogue grows without a release; never assume you know it.

Takes no parameters.

workspace_rename

Rename the workspace

  • write
  • idempotent

Change the organization's own display name, exactly as it appears across the Kairon app. Owner/admin only — a caller on any other role is refused. Separate from config_set because it carries its own, stricter permission rather than the open one every config section shares.

Parameters: workspace_rename
NameTypeWhat it is for
namerequiredstringThe workspace's new display name.

writing_check_judge

Judge a draft against the rules, without sending it

  • write

Run one draft past the SAME judge a real send goes through, and get a pass/fail plus a reason for every rule that governs it. Nothing is sent, drafted or saved. Use it to try a rule the operator just wrote before they rely on it, and to show them WHY a draft fails — a rule that reads well and cannot be satisfied is the failure this exists to catch. Pass context (the post being answered, the thread) or rules about their words cannot be judged at all. Costs one model call.

Parameters: writing_check_judge
NameTypeWhat it is for
draftrequiredstringThe exact text to judge, as it would go out.
surfacerequired"post" | "comment" | "dm"What kind of writing this is. It decides which rules apply: general plus this section and nothing else. dm covers every one-to-one message — a campaign send, an inbox reply, an auto-reply.
contextstringWhat the writer was working from: the post a comment answers, the thread a reply continues, who the message is to. Rules like "does not echo their words back" or "reacts to something specific in the post" CANNOT be judged without it — omit it and they are judged on the draft alone, which reads as a pass.
templatestringThe operator's own template seed, when this draft was written from one. Its fixed text is then exempt from the style rules (the writer may only fill the slots, so failing it for the operator's own words asks it to fix what it may not touch), and template fidelity is measured against it.

writing_check_list

Read the rules every draft is held to

  • read-only
  • idempotent

The rules Kairon holds every draft to, in four sections: general (everything it writes), post, comment, dm (one-to-one messages — campaign sends, inbox replies, auto-replies). Each returns Kairon's own shared rules, with the key you switch them by and whether they are on, then the Author's own rules with their ids. recentFailures counts drafts each rule stopped in the last 7 days — a high one is a rule nothing can satisfy. Read this before drafting anything.

Parameters: writing_check_list
NameTypeWhat it is for
sections"general" | "post" | "comment" | "dm"[]Read only these sections. Omit for all four. general applies to everything Kairon writes; the other three govern that surface alone.

writing_check_set

Save the Author's own rules for one section

  • write
  • idempotent

Replace the Author's OWN rules for ONE section, whole. Kairon's shared rules are untouched — switch those with writing_check_shared_set. Send the COMPLETE list for that section: keep a rule by including it with its id from writing_check_list (an id we do not recognise is replaced with a fresh one, which resets what that rule has stopped), add one by omitting the id, delete one by leaving it out. Other sections are untouched. Phrase a rule so a draft either passes it or does not.

Parameters: writing_check_set
NameTypeWhat it is for
sectionrequired"general" | "post" | "comment" | "dm"Which section to replace. A writer is judged against general plus its own section and nothing else, so a rule about comments belongs in comment, never in general.
checksrequiredobject[]The COMPLETE desired list for that section — it REPLACES what is stored. Include every rule to keep, each with its id from writing_check_list so its history survives, plus any new ones without an id. Omit a rule to delete it. An empty array clears the section. At most 20; Kairon's own rules do not count against it.

writing_check_shared_set

Switch one of Kairon's own rules on or off

  • write
  • idempotent

Switch one of Kairon's SHARED rules on or off for this Author, by a key from writing_check_list. Kairon's rules are on by default and are not editable — an Author only chooses whether one applies — so this is the only thing to do to one. Use it when the operator's own style genuinely conflicts with a built-in rule, not to quiet a rule that keeps stopping drafts: that one is usually telling the truth about the writing.

Parameters: writing_check_shared_set
NameTypeWhat it is for
keyrequiredstringA shared rule's key from writing_check_list (e.g. comment.no_first_name_opener). A key that is not a real shared rule is refused rather than stored.
enabledrequiredbooleanfalse switches Kairon's rule off for this Author; true switches it back on.

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.

Read the full privacy policy