Documentation

Connect your AI assistant to LinkedIn

Kairon is a remote LinkedIn MCP server for Claude, ChatGPT, Cursor and custom agents. Search, qualify, build lists, run campaigns and handle replies from the assistant you already use. Setup takes about a minute.

Server URL https://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 Customize, 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/mcp
    npm 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?

audience_preview

Count who an audience rule would select

  • read-only
  • idempotent

How many of the org's OWN leads a pipeline-audience rule selects, before any list is created or anyone is contacted. Returns matched (the total), byStage (that total split across the Pipeline columns) and recentlyContacted7d — how many of the matches heard from you in the last seven days, which is the number that tells you whether the rule is about to write to people mid-conversation. Costs nothing and changes nothing; run it, read the numbers, then pass the SAME rule to list_create as audience.

Parameters: audience_preview
NameTypeWhat it is for
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
audience requiredobject The rule to count, exactly as list_create would take it. Nothing is created and no one is contacted.

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
target requiredobject The 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.
cursor string Continue from a previous page. Omit for the first page.
count integer Followers per page (default 50, the most LinkedIn allows for a page).
intoList string Append this page’s followers to one of your lead lists, by name or id. Combine with exhaust to walk the whole audience into it.
exhaust boolean Page 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.
maxResults integer How 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.
readReceipts string[] | string Required with intoList: the readReceipt of skill_get({ name: "list-building" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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
target requiredobject The company page whose posts to read — named by URL, universal name, or provider id.
cursor string

crm_push

Send leads to the organization's CRM

  • write
  • idempotent
  • open-world

Sends these leads to the CRM the organization connected (HubSpot or Salesforce), whatever their Pipeline stage. In HubSpot each lands as a contact with its company; in Salesforce as a Salesforce Lead or as a Contact under its Account, as the organization chose (a person Salesforce already has as a Contact stays that Contact). Its LinkedIn messages, emails, invites, likes and comments land on the record's timeline (HubSpot) or as completed Tasks (Salesforce). Queued, not instant — it lands within a minute or two. Pushing a lead already in the CRM is safe: only what the CRM does not have yet is sent. Answers CRM_NOT_CONNECTED when the organization has no CRM, and CRM_CONNECTION_BROKEN when an admin must reconnect it in Settings. Leads of other organizations, or deleted ones, are skipped and not counted in queued.

Parameters: crm_push
NameTypeWhat it is for
leadIds requiredstring[] The leads to send (lead ids, lead_…), at most 1000.

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
facet required"people" | "companies" What these items are. people suppresses each named individual. companies suppresses the company itself AND everyone who works there.
items requiredobject[] The entries to add, up to 20000. 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
facet required"people" | "companies" Which facet the entry sits on. It decides the id prefix: people → exper_…, companies → excom_….
id requiredstring The 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
type required"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.
query requiredstring The 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. Always runs on YOUR OWN seat: a public group needs no Sales Navigator and no membership; a members-only group works only if your seat has joined it, and is otherwise refused with that reason. When your seat’s read budget is spent it refuses with a try-later. 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.

Parameters: group_members
NameTypeWhat it is for
group requiredstring The group, as its URL (https://www.linkedin.com/groups/<id>/) or its bare numeric id. Read on your own seat: a public group needs no membership and no Sales Navigator; a members-only group needs your seat to have joined it.
cursor string Continue from a previous page. Omit for the first page.
count integer Members per page (default 50, max 100). LinkedIn refuses anything higher.
intoList string Append 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.
exhaust boolean Page 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.
maxResults integer How 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.
readReceipts string[] | string Required with intoList: the readReceipt of skill_get({ name: "list-building" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

icp_create

Create an ICP

  • write

Create an ICP — a target thesis spanning the companies to reach and the people inside them — as an UNPUBLISHED DRAFT: nothing can qualify against it, and no list or campaign can pick it, until icp_publish makes it 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. Each AI check is ONE yes/no question about ONE thing public data shows, at most 250 characters — write several short checks, never one long one; a longer check is refused.

Parameters: icp_create
NameTypeWhat it is for
name requiredstring A short, specific name — the segment, not the pitch.
definition requiredobject The ICP definition. On icp_update a field you omit is CARRIED FORWARD — send only what you are changing. A field you send replaces that field WHOLE. To clear: null for the thesis or a side, [] for a check list. AI CHECKS: several short ones, each ONE yes/no question public data answers, ≤250 chars. One nobody can answer comes back unconfirmed and disqualifies, emptying the ICP. Read the icp-definition skill.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "icp-definition" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

icp_criteria_list_edit

Add or remove values in one ICP filter list

  • write

Add or remove values inside ONE filter list of one criteria side of the ICP's DRAFT, carrying the whole rest of the ICP forward. Like icp_update, nothing goes live until icp_publish. Use this instead of icp_update whenever you are changing a few values in a long list — icp_update replaces a criteria side WHOLE, so adding one excluded title there means resending every other title beside it. Lists — people: role.include, role.exclude, seniority.include, seniority.exclude, companyHeadcount, filters; company: headcount, filters. Values use the same names and shapes as search_sales_navigator. Adding a value already there, or removing one that is not, is reported back rather than refused — and if nothing changed, the draft is not written. Read the result change to see what actually happened.

Parameters: icp_criteria_list_edit
NameTypeWhat it is for
icpId requiredstring
side required"company" | "people" Which criteria side to edit: the company search or the people search.
list required"role.include" | "role.exclude" | "seniority.include" | "seniority.exclude" | "companyHeadcount" | "filters" | "headcount" Which list inside that side. People: role.include, role.exclude, seniority.include, seniority.exclude, companyHeadcount, filters. Company: headcount, filters. A list the side does not have is refused by name.
add string | object[] Values to append. Strings for every list but filters, which takes { type, query, mode? }. A value already there is left alone, never duplicated.
remove string | object[] Values to drop, matched exactly (a filters item on type + query + mode). A value that is not there is reported back, not an error.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "icp-definition" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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 — and referencedBySources — how many signal-based lists (paused included) targeted it. The delete always proceeds, so tell the operator when either is not zero: any of those campaigns that is running just widened its audience, and each of those lists is now paused until source_set gives it another ICP or none (which turns it back on).

Parameters: icp_delete
NameTypeWhat it is for
icpId requiredstring

icp_get

Get an ICP

  • read-only
  • idempotent

Read one ICP by icpId: its status, its live version (what everything qualifies against; null until the first publish), its draft (unpublished edits; null when there are none) and changes — what publishing the draft would change. Show the operator changes before you call icp_publish.

Parameters: icp_get
NameTypeWhat it is for
icpId requiredstring

icp_list

List ICPs

  • read-only
  • idempotent

List this org's ICPs, unpublished ones included. Each has a status — draft (never published: nothing can use it yet), published, or published_with_draft (live, with unpublished edits) — its live version, its draft, and changes: what the draft changes against the live version.

Takes no parameters.

icp_publish

Publish an ICP's draft

  • write
  • destructive

Make the ICP's draft its next live version. From the next qualification on, every list and campaign that uses this ICP judges against it. Answers to criteria the new version kept word for word are reused: a verdict they settle moves to the new version at publish for free, and the rest are asked only the criteria that were added or changed. Search keywords are never criteria, so a keywords-only edit keeps every verdict. Publish changes who campaigns enroll and spends allowance on those re-checks, so FIRST show the operator what changes (icp_get → changes, and how many campaigns use it) and publish only once they say yes. Returns the new version, the changes it made, usedByCampaigns, and verdicts: per side, how many were kept and how many still need judging (toJudge) — tell the operator both. Refused when there is no draft, and (ICP_FILTER_UNRESOLVED) when LinkedIn no longer knows an industry or place name in it: rename it via filter_resolve, or remove it.

Parameters: icp_publish
NameTypeWhat it is for
icpId requiredstring
ifDraftUpdatedAt string The draft.updatedAt you showed the operator. Publish then refuses (ICP_DRAFT_CHANGED) if the draft changed after, instead of publishing edits they did not see.

icp_update

Update an ICP

  • write

Edit an ICP's DRAFT. Nothing changes who qualifies until icp_publish — so after editing, show the operator the changes in the result and publish only once they say yes. Send ONLY the fields you are changing — anything you leave out is carried forward from the draft (or the live version when there is no draft), including name. A field you DO send replaces that field whole, so a criteria side or a check list must arrive complete. To clear: null for the thesis or a side, [] for a check list. definition itself is always required — to rename and nothing else, send definition: {} (a rename applies at once; it is not part of the draft). Read the icp-definition skill before you change the AI checks — it decides whether the new version still qualifies anyone. A new or edited AI check over 250 characters is refused: rewrite or split it into short one-thing checks, never append.

Parameters: icp_update
NameTypeWhat it is for
icpId requiredstring
name string A short, specific name — the segment, not the pitch.
definition requiredobject The ICP definition. On icp_update a field you omit is CARRIED FORWARD — send only what you are changing. A field you send replaces that field WHOLE. To clear: null for the thesis or a side, [] for a check list. AI CHECKS: several short ones, each ONE yes/no question public data answers, ≤250 chars. One nobody can answer comes back unconfirmed and disqualifies, emptying the ICP. Read the icp-definition skill.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "icp-definition" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

icp_version_restore

Bring an earlier ICP definition back into the draft

  • write
  • destructive

Copy an earlier published version into the ICP's DRAFT — exactly like a campaign restore. Nothing changes who qualifies until icp_publish. It REPLACES any draft already there, so if icp_get shows a draft, confirm with the operator first. Read icp_versions to pick the right one.

Parameters: icp_version_restore
NameTypeWhat it is for
icpId requiredstring
version requiredinteger The version to bring back, as icp_versions listed it.

icp_versions

Read an ICP's earlier definitions

  • read-only
  • idempotent

Every PUBLISHED version of this ICP, newest first, with the date, the check counts and changes against the version before — and which one is in use. Versions are minted only by icp_publish and never rewritten; unpublished edits live in the draft (icp_get). Pass version to get that one in full: its thesis, both filter sets, both check lists, as they were published.

Parameters: icp_versions
NameTypeWhat it is for
icpId requiredstring
version integer Omit 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
cursor string Continue from a previous page. Omit for the first page.
count integer Invitations per page (default 50, max 100).
intoList string Append 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.
exhaust boolean Page 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.
maxResults integer How 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.
readReceipts string[] | string Required with intoList: the readReceipt of skill_get({ name: "list-building" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

lead_email_set

Record what a finder learned about people's email addresses

  • write
  • destructive
  • idempotent

Store the work email a finder returned, on the PEOPLE themselves — which is what an email campaign reads, unlike a custom list column. { items, source }, ≤500 items. Name each person by memberId, url, or providerId, then give either email or notFound: true. source names the provider (clay, apollo, contactout, hunter, instantly), recorded per person so a bounce can be traced to it.

Read the full description

Send notFound: true for misses too, not just hits: it marks the person already-asked, the only thing that stops the next pass paying to ask again. list_members { emailState: 'unasked' } then returns exactly who is left. Per item: set, missed (asked, no address), refused, not_found (nobody by that name here — nothing is created), invalid. refused needs no retry: a human-typed address is never overwritten, and a notFound is ignored for anyone who already has one.

Parameters: lead_email_set
NameTypeWhat it is for
items requiredobject[] What was learned about each person, each naming its subject by ONE of memberId, url, or providerId, plus either an email or notFound: true.
source required"clay" | "apollo" | "contactout" | "hunter" | "instantly" Which finder produced these answers. Recorded per lead, so a bounce can later be traced to the provider that supplied the address. A human typing an address in does not use this tool.

lead_enrich

Find people's work emails and mobile phones

  • write
  • open-world

Find the work email and/or mobile of up to 100 people and save each find on the person, where campaigns read it. Name each by ONE of memberId, url or providerId; ask for email, phone or both.

Read the full description

Paid from balance: $0.02 per email found, $0.20 per mobile found; a search that finds nothing is free. Nobody is searched twice: one who has the field or was searched before is skipped free (has_value, already_searched), as is one known too little (not_enough_to_search — load their profile). Per person: searched with an outcome per field (found with the value, missed — searched, nothing there, skipped with a reason, searching — still running: call again in a minute, free, failed — this search did not finish, or unavailable — searching is down; none is a miss), not_found (nobody by that name) or not_reached: the call stopped first (stoppedReason) — the balance ran out, searching is down, or time_limit: call again for the rest. chargedUsd: the most it cost.

Parameters: lead_enrich
NameTypeWhat it is for
items requiredobject[] The people to search for, each named by ONE of memberId, url, or providerId.
email boolean Find their work email.
phone boolean Find their mobile phone.

lead_phone_set

Record what a finder learned about people's phone numbers

  • write
  • destructive
  • idempotent

Store the phone a finder returned, on the PEOPLE themselves — where the operator sees it, in the list's Phone column and the person's card — unlike a custom list column, which is per list. { items, source }, ≤500 items. Name each person by memberId, url, or providerId, then give either phone or notFound: true. One number per person: when the finder returns several, send the best (mobile first). Stored as given. source names the provider (clay, apollo, contactout).

Read the full description

Send notFound: true for misses too: it marks the person already-asked, the only thing that stops the next pass paying to ask again. list_members { phoneState: 'unasked' } then returns exactly who is left. Per item: set, missed (asked, no number), refused, not_found (nobody by that name here — nothing is created), invalid (not shaped like a phone). refused needs no retry: a human-typed number is never overwritten, and a notFound is ignored for anyone who already has one.

Parameters: lead_phone_set
NameTypeWhat it is for
items requiredobject[] What was learned about each person, each naming its subject by ONE of memberId, url, or providerId, plus either a phone or notFound: true.
source required"clay" | "apollo" | "contactout" Which finder produced these answers. Recorded per lead, so a wrong number can later be traced to the provider that supplied it. A human typing a number in does not use this tool.

linkedin_fetch

Fetch a LinkedIn profile or company

  • read-only
  • idempotent
  • open-world

Fetch a LinkedIn person OR company — 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, across every limit. 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 — Kairon still serves the next call. For a person, withRecommendations: true also returns who vouched for them — each recommender named and addressable, the strongest warm-intro path LinkedIn publishes. Reads run on shared capacity first, so a batch can go in parallel. relative (network distance, a 1st-degree contact panel) needs relationship: true.

Parameters: linkedin_fetch
NameTypeWhat it is for
target requiredobject Who 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.
refresh boolean True bypasses the cache and forces a live read from LinkedIn. Slower, and it spends a lookup against your headroom.
withRecommendations boolean True 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.
relationship boolean Profile-only. True reads LIVE on YOUR own account, the only way to get the person's network distance and, for a 1st-degree connection, their contact panel. Without it the read may run on Kairon's shared capacity, where those come back empty.

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. A PERSON added this way is loaded in the background, through the shared pool first and inside the daily limits. A COMPANY is not: it shows only what Kairon already knew about it until something loads it — linkedin_fetch on its URL does), 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
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring
items requiredobject[] The rows to add, each naming its subject by ONE of memberId, url, providerId, or snapshot.

list_archive

Archive or restore a list

  • write
  • idempotent

Archive (archived: true) or restore (archived: false) a list by asset + id. Archiving HIDES a list without losing it: it leaves list_list's default view (read it back with archived: true), keeps its members, columns and name (a new list still cannot take that name), and can still be read by id and edited. Use it to tidy up the small working lists you built along the way — test batches, one list per search — once their people are merged into the list you will actually use. Prefer it over list_delete whenever the list might be wanted again. An archived lead list is never filled by its audience rule and cannot be given to a campaign. Archiving a lead list that a campaign (other than an archived one) still uses fails with LEAD_LIST_IN_USE, naming those campaigns; one whose signal Source is still active fails with LEAD_LIST_SOURCE_ACTIVE (pause it with source_set first). Idempotent both ways.

Parameters: list_archive
NameTypeWhat it is for
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring
archived requiredboolean true archives the list; false restores it.

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. Two ways to fill it. Name the people yourself with list_add. Or pass an audience rule and the list fills itself from the org's OWN leads — Pipeline stage, tag, owner, ICP verdict, last contact, when they arrived — and keeps filling every night as more people match. Leads only. Run audience_preview on the rule first: it costs nothing and tells you how many it selects and how many of those you contacted this week.

Parameters: list_create
NameTypeWhat it is for
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
name requiredstring The list's name, for the operator to recognise it.
audience object Make this list build itself from a rule over the org's OWN leads, instead of adding people by hand. include selects; exclude (optional) removes from that selection; inside one condition the fields AND together and the values in a field OR together; a group's match is all (AND) or any (OR) across its conditions. The rule only ever ADDS: whoever matches becomes an ordinary member and stays one, so a campaign's own first message — which moves the person off on_hold — cannot cancel their sequence. It re-runs nightly, so people who reach the stage later join by themselves. TWO THINGS TO GET RIGHT. on_hold fills the instant a campaign's last message goes out, so pair it with exclude: { match: 'all', conditions: [{ lastContacted: '30d' }] } unless you really mean to write to someone who heard from you yesterday — in the orgs we measured, 17-45% of the Nurture pile had been contacted inside 30 days. And there is no pause: clearing the rule with audience: null is the only way to stop it, and the members it already added stay. Use audience_preview first to see how many people the rule selects.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "list-building" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring

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. A list that fills itself from a rule also returns that audience rule and the last run's trail — when it ran, how many it added, and the error if it failed. 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
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring

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. Archived lists are left out unless you pass archived: true, which lists ONLY the archived ones. Deleted lists never appear.

Parameters: list_list
NameTypeWhat it is for
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
q string Case-insensitive substring of the list's name.
cursor string
limit integer = 25
archived boolean = falsefalse (default) lists the active lists only. true lists ONLY the archived ones — archived lists never appear in the default view.

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, phone, 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
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring
q string Case-insensitive substring over the member's display fields.
cursor string
limit integer = 25
tags string[] 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" | "phone" | "industry" | "qualification" | "owner" | "added" | "source" Which column to order by. Leads: person, title, company, email, phone, 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.
qualificationStatus string | string[] Keep members whose ICP verdict is ANY OF these: qualified, disqualified, unevaluated. unevaluated means nobody has judged them — never that they failed.
ownerId string | string[] Keep members owned by ANY OF these user ids. Ownership rides the entity, not the list.
tagId string | string[] Keep members carrying ANY OF these tag ids — an OR, and the counterpart to tags above, which is an AND over NAMES.
addedAfter string Keep members added to THIS list at or after this ISO timestamp.
addedBefore string Keep members added to THIS list strictly before this ISO timestamp.
hydrated boolean true 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).
column object[] 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'.
includeQualification boolean Attach 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.
signalKey string | 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.
emailState "has" | "missing" | "unasked" Leads only. Narrow by what is known about the person's work email: has (an address is on file), missing (a finder was already asked and came back empty — asking again costs money and learns nothing), or unasked (nobody has looked). unasked is the one an enrichment pass wants: it IS the remaining work, so a pass can stop, resume tomorrow, and never pay twice for the same person.
phoneState "has" | "missing" | "unasked" Leads only. The same three states for the person's phone: has, missing (a finder already came back empty), unasked (nobody has looked — the worklist for a lead_phone_set pass).
employeeMin integer Companies only. Keep companies with AT LEAST this many employees.
employeeMax integer Companies only. Keep companies with AT MOST this many employees.
industry string[] Companies only. Keep companies carrying ANY OF these industries, matched case-insensitively against the whole industry list, not just the one shown as subtitle.
hqCountry string[] 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, in batches Kairon sizes: 20, then 40, then 80, then the rest of the list; an ICP edit starts again at 20. Omit subjectIds for the next batch (members never tried with this version come first, at random); named ids past the batch come back in dropped as batch_limit. Waits up to a minute; follow a batch still going with list_qualify_runs. While a batch is still going, list_qualify hands it back; once it has ended, a call starts the next batch.

Read the full description

Every answer carries progress — the whole list against the ICP's current version; trust it over list_get and list_members, which show older verdicts as current — and next: the state in one sentence and what to do now. Do what next says. After the first batch with a version, tell the operator what you found and ask before judging the rest; after that, keep going unless the results change. The list-building skill has the order to qualify in.

Parameters: list_qualify
NameTypeWhat it is for
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring
icpId requiredstring The ICP to judge against. Its ACTIVE version is pinned when the run starts.
subjectIds string[] The members to judge. Omit for the next batch of the list. Ids that are no longer members, or past the batch limit, come back in dropped; total counts the rest.
sample integer Make the next batch smaller than the limit — Kairon picks members with no verdict against this ICP version, at random. Ignored when subjectIds is given.
ignoreIcpMismatch boolean = falseHas no effect today: a run no longer stops because the list fits the ICP badly — it judges every member. Kept so older callers that pass it are not refused.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "list-building" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring
runId requiredstring The run to stop.

list_qualify_runs

Read a list's qualification run

  • read-only
  • idempotent

How a run is going, or how it ended. Without runId, this list's latest run; with one, that run. null: never qualified — an answer, not a failure. Carries the same progress and next as list_qualify: read next first.

Read the full description

judged = paid to decide (qualified/disqualified); alreadyCurrent skipped free; failed could not be READ (failedReasons says why) and stays unevaluated. criterionBreakdown (every finished run), per criterion: failed, unconfirmed (the fact was not there), examples, and a suggestion to act on first — missing_on_linkedin / enable_web_or_rewrite / rewrite_or_remove fix the ICP; narrow_the_search fixes the search; check_examples: show the operator the examples, ask if they wanted them. stopReason: allowance_exhausted, platform_outage (Kairon's own failure — never tell them to top up) or icp_mismatch (older runs only: the ICP or the search may be wrong — see the list-building skill); next says what to do.

Parameters: list_qualify_runs
NameTypeWhat it is for
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring
runId string A specific run. Omit for this list's most recent one.

list_remove

Remove members from a list

  • write
  • destructive

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
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring
items requiredobject[] 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
  • destructive

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
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring
items requiredobject[] The rows to write, each naming its subject plus the custom-column values to set on it.

list_show

Show a list in the chat

  • read-only
  • idempotent

Show the operator a list's members as a table inside the chat — the same table Kairon's list page shows. Call it when they want to SEE a list ('show me my Fintech CTOs list'); to read the members yourself, use list_members. Takes list_members' filters and sort, so 'show me the qualified ones' is qualificationStatus: ['qualified']. Returns a short note, never the rows: the table is for the operator, so do not repeat it. The table appears only in apps that show MCP Apps (Claude.ai, Claude Desktop); elsewhere read the rows with list_members.

Parameters: list_show
NameTypeWhat it is for
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring
q string Case-insensitive substring over the member's display fields.
tags string[] 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" | "phone" | "industry" | "qualification" | "owner" | "added" | "source" Which column to order by. Leads: person, title, company, email, phone, 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.
qualificationStatus string | string[] Keep members whose ICP verdict is ANY OF these: qualified, disqualified, unevaluated. unevaluated means nobody has judged them — never that they failed.
ownerId string | string[] Keep members owned by ANY OF these user ids. Ownership rides the entity, not the list.
tagId string | string[] Keep members carrying ANY OF these tag ids — an OR, and the counterpart to tags above, which is an AND over NAMES.
addedAfter string Keep members added to THIS list at or after this ISO timestamp.
addedBefore string Keep members added to THIS list strictly before this ISO timestamp.
hydrated boolean true 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).
column object[] 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'.
signalKey string | 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.
emailState "has" | "missing" | "unasked" Leads only. Narrow by what is known about the person's work email: has (an address is on file), missing (a finder was already asked and came back empty — asking again costs money and learns nothing), or unasked (nobody has looked). unasked is the one an enrichment pass wants: it IS the remaining work, so a pass can stop, resume tomorrow, and never pay twice for the same person.
phoneState "has" | "missing" | "unasked" Leads only. The same three states for the person's phone: has, missing (a finder already came back empty), unasked (nobody has looked — the worklist for a lead_phone_set pass).
employeeMin integer Companies only. Keep companies with AT LEAST this many employees.
employeeMax integer Companies only. Keep companies with AT MOST this many employees.
industry string[] Companies only. Keep companies carrying ANY OF these industries, matched case-insensitively against the whole industry list, not just the one shown as subtitle.
hqCountry string[] Companies only. Keep companies whose HQ is in ANY OF these ISO-2 country codes.

list_update

Rename a list, or set its custom columns

  • write
  • destructive

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. audience sets or clears the rule a list fills itself from (leads only). Sending one stores it AND runs it, so the reply carries what that run did. Omit the field to leave the current rule alone; send null to clear it — the members it already added stay, because clearing is the only way to stop it.

Parameters: list_update
NameTypeWhat it is for
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring
name requiredstring The list's name. ALWAYS required here, even when you only mean to change columns — send the current name back to keep it.
columns object[] The WHOLE ordered custom-column set, replacing what is there. Omit to leave the columns alone; [] removes them all. Carry renameFrom to rename a column and keep every member's values under the new name.
audience object | null Make this list build itself from a rule over the org's OWN leads, instead of adding people by hand. include selects; exclude (optional) removes from that selection; inside one condition the fields AND together and the values in a field OR together; a group's match is all (AND) or any (OR) across its conditions. The rule only ever ADDS: whoever matches becomes an ordinary member and stays one, so a campaign's own first message — which moves the person off on_hold — cannot cancel their sequence. It re-runs nightly, so people who reach the stage later join by themselves. TWO THINGS TO GET RIGHT. on_hold fills the instant a campaign's last message goes out, so pair it with exclude: { match: 'all', conditions: [{ lastContacted: '30d' }] } unless you really mean to write to someone who heard from you yesterday — in the orgs we measured, 17-45% of the Nurture pile had been contacted inside 30 days. And there is no pause: clearing the rule with audience: null is the only way to stop it, and the members it already added stay. Use audience_preview first to see how many people the rule selects. Omit this field to leave the current rule untouched; send null to clear it (the members it already added stay).

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
newsletter requiredstring The 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.
cursor string Continue from a previous page. Omit for the first page.
count integer Subscribers per page (default 50, max 100). LinkedIn refuses anything higher — asking for 250 fails the whole call, it does not clamp.
intoList string Append 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.
exhaust boolean Page 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.
maxResults integer How 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.
readReceipts string[] | string Required with intoList: the readReceipt of skill_get({ name: "list-building" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

owner_set

Assign an owner to leads or companies

  • write
  • destructive
  • 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
asset required"leads" | "companies" Which kind of row these items are — people or companies.
items requiredobject[] The rows to assign, each naming its subject by ONE of memberId, url, or providerId.
owner requiredobject | null Who 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.
lastContacted string Narrow 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.
cursor string Resume the named column from a previous call — pass the cursor it returned.
limit integer = 25How many people to return when stage is named.

pipeline_stage_set

Move people on the pipeline board

  • write
  • destructive
  • 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, or when their reply is a "not now" — 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
items requiredobject[] The people to move, each naming its subject by ONE of memberId, url, or providerId.
stage required"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
url requiredstring The 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.
intoList string Where 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.
cursor string
exhaust boolean Page 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.
maxResults integer How 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.
readReceipts string[] | string Required with intoList: the readReceipt of skill_get({ name: "list-building" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

profile_comments

The comments a person has written

  • read-only
  • idempotent
  • open-world

A page of the comments this person has WRITTEN, on anyone's posts, newest first — the mirror of post_interactions, which answers who engaged with ONE post. Address them with target — { type, value } (url, public_identifier, 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 profile.fetch to resolve first. Each row carries the comment's own text, its date and the post it sits under. Two jobs it is the only read for: how this person WRITES, in their own words at length (read 30 of an operator's own comments before drafting anything in their voice — it is evidence, where a style rule is a guess), and what they have been paying attention to LATELY, which their own posts do not say. For the posts they wrote use profile_posts; for what they merely reacted to use profile_reactions. Transient — nothing here is persisted.

Parameters: profile_comments
NameTypeWhat it is for
target requiredobject Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id.
cursor string

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
target requiredobject Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id.
cursor string

profile_reactions

The posts a person has reacted to

  • read-only
  • idempotent
  • open-world

A page of the posts this person has REACTED to, newest first — the same attention feed profile_comments reads, with no words in it. Address them with target — { type, value } (url, public_identifier, or provider id) — and page with cursor; one page is one metered action, plus one profile.fetch to resolve a target that is not already a provider id. Reach for it when the question is BREADTH — what topics and whose posts keep pulling this person in — since most people react far more often than they comment. It says nothing about how they write: a reaction has no text, so use profile_comments for voice. Transient — nothing here is persisted.

Parameters: profile_reactions
NameTypeWhat it is for
target requiredobject Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id.
cursor string

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. 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. status: met, not_met (a fact says no) or unconfirmed (the fact is not there, disqualifies unless waived). result: null is NOT CHECKED: no failure, still disqualifies. On a lead or a company, manualQualification (with manualQualificationBy) means a PERSON set status by hand; the tallies still show what the criteria found. Null means Kairon decided. icpVersionId is the version pinned when it was judged, not today's active one. An unevaluated member has no criteria — an answer, not a failure; next is list_qualify.

Parameters: qualification_get
NameTypeWhat it is for
asset required"leads" | "companies" Which kind of list: 'leads' (people) or 'companies'.
id requiredstring The 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 or companies yourself

  • write
  • destructive
  • idempotent

Say whether these people — or, with asset: 'companies', these companies — are worth reaching out to, in your own judgement, instead of waiting for Kairon to decide. qualified lets one through even if your ICP would reject it; disqualified keeps one out even if it would accept it; null takes 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, so taking it back restores the ICP's verdict with nothing to re-run or pay for. Use it for the calls Kairon cannot make: someone you know personally, a profile that does not say what you know, a company whose global or empty LinkedIn page fails the ICP's filters, or a lead a campaign needs today. Never creates a subject: an unknown url comes back not_found.

Parameters: qualification_set
NameTypeWhat it is for
asset "leads" | "companies" = "leads"Who you are deciding about: 'leads' (people, the default) or 'companies'. Companies are named by memberId (from list_members), url or providerId, like people.
items requiredobject[] The people (or companies) to decide about, each naming its subject by ONE of memberId, url, or providerId.
qualification required"qualified" | "disqualified" | null Your 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.

recruiter_saved_searches

List Recruiter saved searches

  • read-only
  • idempotent
  • open-world

The searches this seat has SAVED in LinkedIn Recruiter, by name. Call this first: it returns no people, so it costs a call and none of the daily result budget, and it is refused before any LinkedIn call at all when the seat has no Recruiter — which makes it the cheap way to prove the Recruiter door is open. It NAMES searches, it does not run them: to run one, ask the operator to open it in LinkedIn Recruiter and paste the URL from the address bar into search_recruiter. There is no id to pass; LinkedIn documents none that would work. Recruiter needs a seat connected with the Kairon Chrome extension; a seat connected with an email and password cannot hold it.

Parameters: recruiter_saved_searches
NameTypeWhat it is for
keywords string Narrow the list by name. Omit to list everything the seat has saved.
limit integer How many to return (default 10, max 100).

search_people

Search people (classic)

  • read-only
  • idempotent
  • open-world

Classic LinkedIn people search — the door for TWO asks: something measured from the operator themselves, and event attendees. networkDistance, connectionsOf and followersOf are measured from the operator here, and search_sales_navigator sometimes 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. Event attendees: url = https://www.linkedin.com/search/results/people/?eventAttending=%5B%22<id>%22%5D; no need to attend; no total: exhaust. Pass intoList to append the page.

Parameters: search_people
NameTypeWhat it is for
url string
keywords string
cursor string
limit integer Page 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.
filters object[]
networkDistance 1 | 2 | 3[] Connection degree: 1 (1st), 2 (2nd), 3 (3rd+). LinkedIn native filter CONNECTIONS.
profileLanguage string[] Profile languages as 2-letter ISO-639-1 codes (e.g. ["en","es"]).
openTo "proBono" | "boardMember"[]
connectionsOf string[] 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.
followersOf string[] LinkedIn provider ids (ACoAA…, from people.suggest — NOT numeric member ids) whose followers to search within.
advancedKeywords object
intoList string
exhaust boolean Page 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.
maxResults integer How 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.
readReceipts string[] | string Required with intoList: the readReceipt of skill_get({ name: "list-building" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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.
firstDegreeOnly boolean Only posts by the seat's 1st-degree connections.
postedByMe boolean Only the seat's own posts.
postedByFollowed boolean Only posts by people the seat follows.
fromMember string[] Author NAMES — only posts written by these people.
fromCompany string[] Company NAMES — only posts published by these pages.
mentioningMember string[] Only posts that @-mention these people, by NAME.
mentioningCompany string[] Only posts that @-mention these companies, by NAME.
authorIndustry string[] Industry NAMES — only posts whose AUTHOR works in one of them.
authorCompany string[] Company NAMES — only posts whose AUTHOR works at one of them.
authorKeywords string Free text matched against the AUTHOR's profile, e.g. a job title.
keywords requiredstring Text matched against the post body. Boolean OR/AND/NOT allowed.
cursor string

search_recruiter

Search (LinkedIn Recruiter)

  • read-only
  • idempotent
  • open-world

LinkedIn Recruiter's own candidate search — a DIFFERENT product from Sales Navigator, separately licensed, and people only. Two ways in. url: a Recruiter search URL the operator copied from their browser, INCLUDING the URL of a saved search — it runs verbatim, needs no ids resolved, and is the surest path there is. This is how a saved search runs: recruiter_saved_searches names them, the operator opens one and copies the address bar. Ask for a URL whenever they can paste one. keywords: free text with boolean modifiers (developers AND "product owner" NOT manager), for when no saved search fits. Those two and nothing else, on purpose: Recruiter's other filters take ids this tool cannot verify, and LinkedIn DISCARDS a filter it does not understand rather than refusing it, so an unverified filter would silently widen the search while charging for the page. Pass intoList to append the page to a leads list. One page is one metered action.

Parameters: search_recruiter
NameTypeWhat it is for
url string A Recruiter search URL copied from the browser, INCLUDING the URL of a saved search — open the saved search in Recruiter and copy the address bar. Unipile runs it verbatim and it overrides everything else here, so nothing has to be resolved to an id first. This is how a saved search runs, and it is the best way to use this tool.
keywords string Free text over Recruiter's index. Boolean modifiers work: developers AND "product owner" NOT manager. The fallback for when no saved search fits — ignored when url is set, which overrides everything.
cursor string
limit integer Page size. Recruiter allows up to 100.
intoList string Where this page's people are appended — always a LEADS list, because Recruiter answers with people and nothing else.
exhaust boolean Page 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.
maxResults integer How 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.
readReceipts string[] | string Required with intoList: the readReceipt of skill_get({ name: "list-building" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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. 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 structured filters over noisy keywords. Pass human-readable names ({ type, query }); the server resolves ids. Same-dimension OR, different dimensions AND. Pass icpId (or withoutIcp with a reason). 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.
keywords string LAST resort — matches noisily across all text. Prefer a structured filter for any title/industry/geography/size intent; use keywords only for a concept no filter captures. This is a BOOLEAN query, not a phrase: bare space-separated words are AND-ed (ALL must appear), so "fintech payments neobank" requires all three together and usually returns ZERO. For ALTERNATIVES, OR them, quote each, and ALWAYS wrap the OR group in parentheses: ("fintech" OR "payments" OR "neobank"). The parentheses are MANDATORY — LinkedIn silently mis-parses an unparenthesized OR of 4+ terms and returns ~0. Quote multi-word phrases; combine groups with AND/NOT (e.g. ("fintech" OR "neobank") AND "payments"). Operators must be UPPERCASE.
filters object[] Id-resolved dimensions (location, industry, company, …). Same dimension repeated = OR; different dimensions = AND (each narrows the results).
cursor string Pass the cursor from a prior result to page. It continues only the exact search that made it: send it with the SAME category and filters as the first page, or it is refused.
limit integer Page 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.
firstName string
lastName string
role object Current job-TITLE keywords (include/exclude) — the primary people filter. Use it for ALL title intent, NOT keywords and NOT seniority. Each include entry is a separate Boolean title clause LinkedIn ORs. Two kinds: (1) single-title pills — exact exec titles + founder, one entry per variant (e.g. "CEO","Chief Executive Officer","Founder","Fundador"); (2) ONE seniority-block pill for broad leadership roles: a marker AND a parenthesized OR-list of role synonyms, e.g. <<SENIORITY_BLOCK_EN>> AND ("growth" OR "demand generation"). The marker (one of <<SENIORITY_BLOCK_EN>> / <<SENIORITY_BLOCK_ES_EN>> / <<SENIORITY_BLOCK_PT_EN>>, by language) expands server-side to a broad leadership OR-block so you recall heads/VPs/directors/managers without enumerating titles. See the playbook for the full recipe. 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.
seniority object LinkedIn's coarse seniority ladder (cxo, vice_president, director, senior, …) — NOT job titles. Use only when you mean a seniority level; pair with role for "VP-level whose title contains X".
tenure object[] Years at the current company in their current role, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
tenureAtCompany object[] Total years at the current company, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
tenureAtRole object[] Years in the current role, as {min,max} ranges. Years MUST be LinkedIn bucket breakpoints — valid min: 0, 1, 3, 6, 10; valid max: 1, 2, 5, 10 (buckets <1, 1-2, 3-5, 6-10, 10+). Any other value (e.g. min:5 or min:2) is rejected. So "5+ years" → {min:6}; "3+ years" → {min:3}; "3 to 5 years" → {min:3,max:5}.
companyHeadcount "1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001-5000" | "5001-10000" | "10001+"[] Size band of the person's current employer (people search). For a company's OWN size use headcount. Approximate "startup" with small buckets (["1-10","11-50","51-200"]).
companyType "public_company" | "privately_held" | "non_profit" | "educational_institution" | "partnership" | "self_employed" | "self_owned" | "government_agency"[] Ownership type of the person's current employer (people search; e.g. privately_held, public_company). A companies search has no company-type facet.
profileLanguage string[] Profile languages as 2-letter ISO codes (e.g. ["en","es"]).
networkDistance 1 | 2 | 3 | "GROUP"[] Connection degree from the operator: 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. Answered only on the operator's OWN Sales Navigator seat. 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.
connectionsOf string[] 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.
changedJobs boolean Spotlight: only people who recently changed jobs.
postedOnLinkedin boolean
mentionedInNews boolean
followingYourCompany boolean
pastColleague boolean Spotlight, 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.
sharedExperiences boolean Spotlight, 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.
viewedYourProfileRecently boolean
viewedProfileRecently boolean
messagedRecently boolean
includeSavedLeads boolean
includeSavedAccounts boolean
hasJobOffers boolean Company search: only companies currently hiring.
headcount "1-10" | "11-50" | "51-200" | "201-500" | "501-1000" | "1001-5000" | "5001-10000" | "10001+"[] The company's OWN size band (companies search; the analog of people-search companyHeadcount). Approximate "startup" with small buckets (["1-10","11-50","51-200"]).
headcountGrowth object Company headcount growth rate as a {min,max} percentage range.
annualRevenue object Company annual revenue {currency (ISO 4217), min, max} in millions. Both min AND max are required and MUST be breakpoints: 0, 0.2, 1, 2.5, 5, 10, 20, 50, 100, 500, 1000, 1001.
followersCount object[] COMPANY followers only — a companies search. Search has no filter for a PERSON's followers; check that with the ICP. {min,max} ranges. Breakpoints — min: 1, 51, 101, 1001, 5001; max: 50, 100, 1000, 5000.
fortune object[] Fortune ranking {min,max} ranges (e.g. Fortune 500). Breakpoints — min: 0, 51, 101, 251; max: 50, 100, 250, 500.
recentActivities "senior_leadership_changes" | "funding_events"[] Company signals: senior_leadership_changes and/or funding_events.
departmentHeadcount object
departmentHeadcountGrowth object
savedAccounts string[]
savedSearchId string
recentSearchId string
lastViewedAt number
withinArea number Radius around each postal_code filter; ignored without one. Omit it (or 0) to search only the postal codes themselves — the default. Under an ICP that names postal codes, the ICP radius applies to every code and cannot be changed (any other value is refused). A radius widens each code into a circle: LinkedIn snaps it to 5, 10, 25, 35 or 50 (its own unit), and even 5 around one central Buenos Aires code reaches millions. Use several exact postal codes rather than a radius to cover a city.
icpId string The ICP the results will be judged against. Its search criteria are applied for you: you may add filters, exclusions, or a subset of its values, never a value outside them. Pass it on every search except while creating or editing an ICP, exploring, or finding people at companies the operator named (then pass withoutIcp). An ICP with no search filters yet still takes its icpId: the search then runs on your filters, and the result tells you to propose saving them to the ICP.
withoutIcp object Only when no ICP applies yet. Cannot be combined with icpId. With intoList it stores the first page only, as a sample: no exhaust, no next page. Filling a list needs an ICP behind it.
intoList string
exhaust boolean Page 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.
maxResults integer How 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.
companyLists object[] Scope a PEOPLE search to the companies in your own Kairon company lists (list_list, asset "companies"). The server expands each list to its companies' LinkedIn ids as a current_company filter — no per-company resolution, no 40-filter ceiling. NOT account_lists, which means Sales Navigator's own saved lists and will not match a Kairon list. People category only.
readReceipts string[] | string Required with intoList: the readReceipt of skill_get({ name: "list-building" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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
listId requiredstring The 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
listId requiredstring The 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_requalify

Re-qualify a source run's candidates against the current ICP

  • write
  • idempotent
  • open-world

Re-judge people a source run already decided on, against the source's CURRENT ICP — the fix after the ICP was loosened and the people it rejected now show as "seen before". A run only judges people it finds, so without this a rejected person is only looked at again if they surface again. Pass a runId and up to 200 leadIds from source_runs with that runId. Those who now qualify join the list exactly as a run would add them — with their Signal note, through the exclusion list and the contact filter — and the run's record is rewritten to the new verdict. It never removes anyone. Each stale candidate costs one AI judgment; anyone already judged against this ICP version is skipped for free and counted as alreadyCurrent. Re-judge a few first and read the result before sending a full batch.

Parameters: source_requalify
NameTypeWhat it is for
leadIds requiredstring[] The candidates to re-judge — each row's leadId from source_runs with this runId. An id that is not a candidate of this run counts as failed.
listId requiredstring The 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.
runId requiredstring The run whose candidates to re-judge, from source_runs.

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 runs real LinkedIn searches, on Kairon’s own accounts and, for its seat signals, on the list’s seat — 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
listId requiredstring The 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
listId requiredstring The 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.
runId string A 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.
limit integer Rows per page, 1-100. Defaults to 25.
cursor string The 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
  • destructive
  • idempotent

Make a lead list SIGNAL-BASED, or change the one it already is: the signals that should feed it, and optionally an ICP every new person must pass (omit it and everyone the signals find is added; lookalike, changed_jobs, posted_recently and the two company signals need one). 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. Changing the ICP restarts only the signals that search with it. 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
listId requiredstring The 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.
icpId string | null The ICP every new person must pass before joining the list, from icp_list. Its ACTIVE version is the check, so revising the ICP changes future runs. Omit it or pass null for no ICP: then everyone the signals find is added. This is a full replacement, so on a list that HAS an ICP, omitting it removes the check — send back the icp.id source_get gave you to keep it. Six signals also SEARCH with the ICP and need one: lookalike, changed_jobs, posted_recently, company_raised_funds, company_leadership_change, company_hiring. Changing the ICP restarts those six from the top of their feed; the others keep their place.
signals requiredobject[] 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.
contactFilter object Who 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.
dailyTarget integer How many qualified people this list should gain per day (UTC), 1-1000. A floor, not a cap: every event signal still runs in full first, and the lookalike signal then keeps searching until the day's total reaches this number, the search runs out, 30 people in a row fail the ICP (when the ICP describes companies: 10 companies in a row where nobody fits), or 20 companies (or result pages) in a row add nobody (it picks up from there the next day). Only the lookalike fills the gap, so on a source without one this only reports progress. Omitted leaves the current setting; a new source starts at 30.
peoplePerCompany integer How many people a walk that goes company by company takes at each company, 1-100. Applies to the lookalike (when the ICP describes companies) and to the recently-raised-funds and new-senior-leadership signals. Higher means fewer companies and less variety, but more of a big employer. Omitted leaves the current setting; a new source starts at 3.
seatId string Which workspace seat (a channel account id from seat_list) reads this list's SEAT signals: viewed_my_profile, recently_connected and company_followers, and any signal whose own search is measured from a seat (firstDegreeOnly, postedByMe, postedByFollowed or inYourNetwork in its config, or a networkDistance-style filter in the ICP). Every other signal reads on Kairon's own accounts and needs no seat. Any member may pick any connected seat. Omitted: the list keeps its seat, else takes yours if it can run them; if neither works the call is refused with SOURCE_SEAT_REQUIRED, which lists the seats that can. Refused with SOURCE_SEAT_CANNOT_RUN when the seat lacks what a signal needs (a paid licence for viewed_my_profile, page admin for company_followers, Sales Navigator for a viewer-relative ICP). Ignored when no signal needs a seat.
acceptExtraListCharge boolean The workspace's plan includes a number of growing lists (lists whose source is active). Turning a list on past that number costs the plan's price for an extra list, every month, billed to the organization; on a plan that sells none it is refused. A call that turns a list on past the included number is REFUSED unless this is true, and the refusal carries the price: ask the user first, then retry with it. Ignored when the call turns no list on, or the list fits. A list carries at most 3 signals.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "list-building" }) and of framework_get({ slug: "targeting" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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
id requiredstring

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
id string
name string The 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
  • destructive
  • 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
asset required"leads" | "companies" Which kind of row these items are — people or companies.
items requiredobject[] 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
query requiredstring What data you are after, in plain words. Up to 1000 characters.
limit integer How 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
provider requiredstring The provider slug from webdata_find, e.g. apify.
endpoint requiredstring The 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. Quote it against allowance_get: what they hold, what this takes, what it leaves.

Parameters: webdata_quote
NameTypeWhat it is for
provider requiredstring The provider slug from webdata_find, e.g. apify.
endpoint requiredstring The endpoint path from webdata_find, e.g. /apidojo/tweet-scraper.
input object The 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.
expectedResults integer For 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
runId requiredstring The 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
provider requiredstring The provider slug from webdata_find, e.g. apify.
endpoint requiredstring The endpoint path from webdata_find, e.g. /apidojo/tweet-scraper.
input object The 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.
quoteId requiredstring The 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 every precondition holds — 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_get → leadListIds); 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
startNow boolean = trueDefault true: enroll today's first batch immediately instead of waiting for the next send window to open.
id requiredstring The campaign id, as campaign_list or campaign_create returned it.

campaign_check_accepts

Check LinkedIn for new accepts

  • write
  • idempotent
  • open-world

Ask LinkedIn now which invited leads of a campaign accepted, and move each one on to its next step. Use it when the operator says someone accepted on LinkedIn but Kairon does not show it: Kairon hears of an accept from Unipile, which can be up to ~8 hours late. The same as the refresh on the campaign's Invite Accepted card. It reads each sender seat's connections, so an accept found for ANOTHER campaign on that seat moves on too. kind: checked — found leads moved on (0 means LinkedIn shows no new accept yet), seatsFailed seats could not be read (reconnect them); nobody_waiting — no lead of this campaign is waiting on an accept; cooldown — each seat is checked at most once per 10 minutes, so tell the operator nextCheckAt and do not retry before it.

Parameters: campaign_check_accepts
NameTypeWhat it is for
id requiredstring The 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
id requiredstring
missingOnly boolean = 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.
cursor string
limit integer = 50

campaign_create

Create a draft campaign

  • write

Create a draft campaign: a name and the sending seats (channelAccountIds, one or more from seat_list), and the ICP to gate enrollment on (icpId). Nothing is selected for you: omitting icpId creates an UNGATED campaign. ASK the operator, every time — show them their ICPs (icp_list; only a published or published_with_draft one can gate — a draft one is refused until icp_publish) and say in one line what each answer means: gated, and only people who qualify are contacted while everyone else on their lists is never messaged; ungated, and nobody is checked against an ICP. "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. A new campaign skips anyone your seats are already in touch with; widen it with enrollmentFilter on campaign_update. The campaign-building skill has the full order.

Parameters: campaign_create
NameTypeWhat it is for
name requiredstring The campaign's name, for the operator to recognise it in a list. Not seen by any lead.
channelAccountIds requiredstring[] The sending seats — connected LinkedIn accounts in this org, one or more. Each new lead goes to one of them and hears from that seat for the whole sequence; every seat enrolls up to the campaign's dailyEnrollmentLimit a day, so the campaign's daily volume grows with each seat. Duplicates collapse. Get the ids 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.)
icpId string The ICP to gate enrollment on. There is no default and nothing is selected for you: omitting this creates an UNGATED campaign. 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, except the people its Enrollment Filter skips (a new campaign skips anyone your seats are already in touch with: a conversation, or an invitation, message or comment a campaign sent). "No gate" is a fine answer, but it has to be one they gave.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "campaign-building" }) and of framework_get({ slug: "outreach" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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
id requiredstring The 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
id requiredstring The campaign id, as campaign_list or campaign_create returned it.
leadId requiredstring The 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
q string Case-insensitive substring of the campaign's name.
status string | 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.
initiative string | "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.
cursor string Opaque keyset token from the previous page's nextCursor. Omit for the first page.
limit integer = 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 pick up there when the campaign is resumed. Nothing already sent is recalled. Idempotent — pausing a paused campaign returns it unchanged, with one exception: a campaign Kairon paused itself (its LinkedIn account disconnected, or the plan went unpaid) resumes on its own within the hour once the cause is fixed, and pausing it makes the pause yours, so it stays paused. Refused (400) if the campaign is draft or archived: only an active one can be paused.

Parameters: campaign_pause
NameTypeWhat it is for
id requiredstring The 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, and an error where no message could be produced — for a step with a Post Condition, no_eligible_post, no_matching_post or condition_check_failed; each lead with at least one eligible post spends one check, judged as on a first lap (no watermark). 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
id requiredstring The campaign id, as campaign_list or campaign_create returned it.
nodeKey requiredstring Which 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.
leadIds string[] 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.
senderUserId string Whose text to preview on a send_message step with per-sender versions (bySender): a sender's ownerUserId from seat_list. OMIT IT to preview what the campaign's first sender sends — their own version if they have one, else the step's text. A sender with no version previews the step's text.

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
id requiredstring The campaign id, as campaign_list or campaign_create returned it.
migrateInFlight boolean Default 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 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. autopilot means BOOKED, not unreviewed: approvedAt is set when a person approved it.

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.
campaignId string Only 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.
limit integer Rows 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
runId requiredstring One 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
runIds requiredstring[] 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
runId requiredstring One run's id, as campaign_runs returned it — a run is one enrolled lead.

campaign_run_skip_step

Skip the step this person is on — their sequence goes on

  • write
  • destructive

Pass over the step a run is on WITHOUT ending the run: the drafted or booked message, comment, email, like, visit, follow, research or removal never happens, and the person carries straight on to the next step (or finishes, when it was the last — next: 'completed'). This is the answer to "not this one" that campaign_run_cancel is not: cancel ends their whole sequence for good. Refused with CAMPAIGN_RUN_NOT_ACTIONABLE on an invite (the accept wait after it would wait on nothing), on a wait or a delay (a delay is campaign_run_skip_wait), on an A/B split or an agent conversation, on a step already firing, on a message still being WRITTEN (skip it once it is booked or held for review), on an email not held for review, and on a paused campaign. NOT idempotent: it skips the step the run is on NOW, so calling it twice skips TWO steps. Read campaign_run_get first if you are unsure where the run stands, and never retry a success.

Parameters: campaign_run_skip_step
NameTypeWhat it is for
runId requiredstring One 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_skip_wait

Collapse the delay this run is waiting on

  • write
  • destructive

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
runId requiredstring One 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.
stage string | string[] Activity states to include; several read as "any of these".
nodeKey string Only 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.
q string Find a person by name, headline, company or LinkedIn handle.
dateFrom string Earliest 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.
dateTo string Latest 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.
listId string | string[] Only people enrolled from these source lists.
qualificationStatus string | string[] Only people with these ICP verdicts; several read as "any of these".
funnel "invite_sent" | "invite_accepted" | "message_sent" | "replied" | "interested" Only the people one Performance funnel card counts: invite_sent, invite_accepted, message_sent, replied or interested.
cursor string
limit integer = 50
id requiredstring
includeQualification boolean Attach 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_seat_add

Add a sender to a campaign

  • write
  • idempotent

Add a LinkedIn seat as a sender of a campaign (draft, active or paused). Each new lead goes to one of the campaign's senders and hears from that seat for its whole sequence and every reply; every sender enrolls up to its own daily limit, so the campaign's daily volume grows with each one. The added seat takes new leads from the next enrollment — leads already running keep their seat. Re-adding a removed sender makes it live again. Refused with CAMPAIGN_SEAT_LACKS_CAPABILITY naming the step when the seat cannot run a step of the graph.

Parameters: campaign_seat_add
NameTypeWhat it is for
channelAccountId requiredstring The seat to add as a sender — a connected LinkedIn account in this org (seat_list). It takes new leads from the next enrollment; leads already running keep their own seat. Re-adding a removed sender makes it live again. Refused, naming the step, when the seat cannot run a step of the graph (an InMail step or an invite note needs Premium or Sales Navigator).
dailyEnrollmentLimit integer This sender's own new leads per day, 1-200. Omit to use the campaign's default (dailyEnrollmentLimit).
id requiredstring The campaign id, as campaign_list or campaign_create returned it.

campaign_seat_remove

Remove a sender from a campaign

  • write
  • destructive
  • idempotent

Remove a sender from a campaign. It takes no new leads from now on. leads is REQUIRED and is the operator's choice — ask them: finish lets the leads it is mid-sequence with keep getting their follow-ups from it until their sequence ends; stop cancels those runs now and nothing more is sent to them from this campaign. Either way its leads with nothing sent go back to the pool for the other senders (returnedToPool). The last live sender cannot be removed (CAMPAIGN_NEEDS_A_SENDER) — add another first.

Parameters: campaign_seat_remove
NameTypeWhat it is for
id requiredstring The campaign id, as campaign_list or campaign_create returned it.
channelAccountId requiredstring The sender seat — a channel account id from the campaign view’s seats.
leads required"finish" | "stop" What happens to the leads this sender is mid-sequence with. finish: they keep getting their follow-ups from this sender until their sequence ends. stop: their runs are canceled now and nothing more is sent to them from this campaign. Either way, its leads with nothing sent yet go back to the pool for the other senders. There is no default — ask the operator.

campaign_seat_update

Set a sender's daily limit

  • write
  • idempotent

Set how many new leads one sender enrolls per day for this campaign, 1-200, or null to return it to the campaign's default (dailyEnrollmentLimit on the campaign). The seat's own LinkedIn action limits still apply on top.

Parameters: campaign_seat_update
NameTypeWhat it is for
dailyEnrollmentLimit requiredinteger | null This sender's own new leads per day, 1-200, or null to go back to the campaign's default.
id requiredstring The campaign id, as campaign_list or campaign_create returned it.
channelAccountId requiredstring The sender seat — a channel account id from the campaign view’s seats.

campaign_send_edit

Rewrite a booked send before it fires

  • write
  • destructive
  • 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
nodeRunId requiredstring The 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.
body requiredstring The 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.
subject string An 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.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "write-messages" }) and of framework_get({ slug: "outreach" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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. A draft still held (an unfilled {{…}}, or over its length limit) is refused with COPILOT_DRAFT_NOT_SHIPPABLE, as campaign_run_approve refuses it — fix the draft with campaign_send_edit first. 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
runId requiredstring One 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
  • destructive
  • idempotent

Replace the campaign's DRAFT sequence graph IN FULL — it overwrites, it is not a patch. Nodes are { key, type, config } with a stable key you choose; edges are { fromKey, toKey, condition }. Layout is automatic. An edge's condition is an outcome its source type emits: send_invite→sent, wait_connection_accepted→timeout/accepted*, delay→default*, send_message→sent, like_last_post→done, visit_profile→done, follow_profile→done, remove_relation→done, comment_on_post→commented, send_inmail→sent, send_email→sent, audience_split→branch_a*/branch_b*, wait_reply→timeout/replied, agentic_reply→no_reply/done (* must be wired). Which types may follow which is on the edges field; a violation is refused (400). The response is the saved graph, in the shape this tool accepts — read it back. The campaign-building skill has the build order. Saving is not publishing.

Parameters: campaign_set_graph
NameTypeWhat it is for
id requiredstring The campaign id, as campaign_list or campaign_create returned it.
graph requiredobject The 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.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "campaign-building" }) and of framework_get({ slug: "outreach" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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
id requiredstring The campaign id, as campaign_list or campaign_create returned it.
days 7 | 30 Window 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_template_get

Read one campaign template in full

  • read-only
  • idempotent

One template, in the shapes the build tools take. graph goes to campaign_set_graph as is. settings go to campaign_update (sendWindow takes the sending seat's own timeZone, which you add). list, when not null, is the lead list's signal set for source_set: fill each config: null per source_set's schema (inbound-engagers starts engaged_with_profile on the operator's own profile, { target: { kind: 'person', url } }; a signal with configFrom takes the same page url that signal's target names); with combine: true put every signal on one list, otherwise pick the one that fits. Ask the operator which ICP the list checks its finds against (source_set's icpId), as the app does. In the graph's send_message bodies, {{…}} is written by the AI for each lead and stays as is; [[…]] is a blank only the operator can fill, and campaign_publish refuses while one is left, so ask them. Reactivation sends from every connected seat (seat_list).

Parameters: campaign_template_get
NameTypeWhat it is for
id required"reactivation" | "nurturing" | "inbound-engagers" | "competitor-engagers" | "company-page-engagers" | "keyword" | "travel" | "third-party-event" | "own-event" | "webinar" | "networking" | "lead-magnet" Which template — an id from campaign_template_list.
locale "en" | "es" The operator's language for names and messages: en (default) or es. Pick the one the operator writes in; Kairon translates a message per lead's language when it drafts.

campaign_template_list

List the campaign templates

  • read-only
  • idempotent

The campaign templates the app's New campaign page offers: each an id, a name, what it is for, its kind and the signals its lead list needs. reactivation brings back leads parked in Nurture when they have news (an org has at most ONE Reactivation campaign and ONE Reactivation list, the list whose source carries parked_lead_news: look for it with list_list + source_get first, reuse that list when no campaign uses it yet, and do not offer Reactivation when one already does). A signal template builds a new list from those signals. An outreach template works on any list the operator picks (signals is empty). nurturing (kind outreach) is a weekly loop with no message: a person leaves it by replying, leaving the list, an exclusion, a stop, or a cancelled comment; a pause or an unapproved one holds it. Campaigns named Nurturing before 2026-10 are Reactivation. Recommend one that fits the goal, then read it with campaign_template_get and build it.

Parameters: campaign_template_list
NameTypeWhat it is for
locale "en" | "es" The operator's language for names and messages: en (default) or es. Pick the one the operator writes in; Kairon translates a message per lead's language when it drafts.

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. An archived list cannot be bound (LEAD_LIST_ARCHIVED) — restore it with list_archive first. It is also how a campaign is pointed at the initiative it serves, via initiativeId (null detaches) — do that for every campaign you build under an initiative, or the initiative 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
name string The campaign's name, for the operator to recognise it. Not seen by any lead.
sendWindow object When 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.
allowsReEntry boolean May 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.
sendFromOwner boolean With several sending seats: send each lead from its OWNER's seat. true means a lead whose owner has a working seat in this campaign is contacted from that seat only (it waits for that seat's daily room); a lead with no owner, or whose owner's seat is disconnected, removed or gone, goes to whichever seat has room. false (the default) ignores owners: every seat takes the oldest leads. No effect with one seat.
dailyEnrollmentLimit integer The DEFAULT new leads per day, per sender, 1-200 (default 30): every sender without a limit of its own enrolls up to this many a day, so the campaign's day is the sum over its senders. Caps the START of new runs, not sends — the org's own per-seat limits still apply on top. Senders are added, removed and given their own limit with campaign_seat_add / campaign_seat_remove / campaign_seat_update.
priority integer | null Which 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.
endsOn string | null The day this campaign stops, YYYY-MM-DD, in its send window's time zone. On that day it pauses itself (reason ended) and nothing more goes out — no new leads, no follow-ups. Today counts: setting today or an earlier day on a running campaign pauses it AT ONCE, so to make today the last sending day, pass tomorrow. Use it for a campaign tied to a dated event. null clears it; omitted leaves it unchanged. To run it again after it ended, move the date later or clear it, then resume.
enrollmentFilter object Which prior-contact history may enroll, and whose. A new campaign starts on new across the org: it skips anyone who already has a conversation with any seat of this org, whoever wrote first, and anyone a Kairon campaign already sent a message, a comment or an invitation WITH a note to (a note-less invitation said nothing, so someone who accepts one stays new). Widen it only for a campaign meant for people already contacted: add unanswered for a re-engagement; add in_conversation only when every message is written for someone already talking to you, never under a fixed opening line (that greets them as a stranger). A narrowed filter SKIPS the people it excludes rather than queueing them, so it is a common reason a big audience enrolls almost nobody.
icpId string | null The ICP that gates enrollment. null clears it (no ICP check); omitted leaves it unchanged. Must be an ICP in this org. Leads that fail it are marked Skipped, never messaged.
stopOnReply boolean Default 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.
id requiredstring The campaign id, as campaign_list or campaign_create returned it.
leadListIds string[] 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.
initiativeId string | null The initiative this campaign serves. null detaches it; omitted leaves it unchanged. A campaign serves at most ONE initiative, so a different id MOVES it. Detaching changes nothing else — a campaign that outlives its initiative 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
id requiredstring The campaign id, as campaign_list or campaign_create returned it.
versionNumber requiredinteger The 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
id requiredstring The campaign id, as campaign_list or campaign_create returned it.
versionNumber integer Omit 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 an initiative’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 initiative or by accident, never to tidy away a verdict somebody did not like.

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

checkpoint_edit

Correct a checkpoint

  • write
  • destructive
  • 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
body string What 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](#kairon-ana@co.com) — 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.
initiativeId requiredstring The initiative the checkpoint belongs to.
checkpointId requiredstring The checkpoint id (ckpt_…), as initiative_get returned it in the history.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "gtm-checkpoint" }) and of framework_get({ slug: "loop" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

checkpoint_publish

Publish a checkpoint on an initiative

  • 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
body requiredstring What 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](#kairon-ana@co.com) — 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 required"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.
initiativeId requiredstring The initiative being judged (init_…).
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "gtm-checkpoint" }) and of framework_get({ slug: "loop" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

initiative_delete

Withdraw a go-to-market initiative

  • 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 an initiative created by mistake — most often a duplicate left behind by an initiative_set that omitted initiativeId. Never use it to retire an initiative 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
initiativeId requiredstring The initiative to remove, by id (init_…).

initiative_get

Read one go-to-market initiative

  • 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
initiativeId requiredstring The initiative to read, by id (init_…).

initiative_list

List the go-to-market initiatives

  • read-only
  • idempotent

Every initiative in the organization: what it tries, 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 initiative 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 initiative

  • write
  • destructive

Write down an initiative: 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 initiative 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
name string Short, 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.
body string | null The thinking, in markdown: what it tries, 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](#kairon-ana@co.com), 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.
priority integer | null 0 is highest. Lower sorts first, matching every other priority in Kairon.
startDate string | null YYYY-MM-DD, this century. An initiative starts on a date, not at a moment.
endDate string | null YYYY-MM-DD, this century. When the initiative should have paid off, or be called. Must not be before startDate.
initiativeId string Omit 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.
owner object | null Who 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.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "gtm-initiative" }) and of framework_get({ slug: "belief-system" }) and of framework_get({ slug: "loop" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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
from requiredstring First day of the window, YYYY-MM-DD, inclusive.
to requiredstring Last day of the window, YYYY-MM-DD, inclusive. Must be on or after from, and the window must not exceed 366 days.
userId string Narrow 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, answered, classification (what their last inbound message meant — interested / objection / neutral / not_interested, NOT a Pipeline stage), leads-only, or q, which matches the counterpart's NAME and does not search message text. Combine answered: "no" with classification: "interested" for replies that said yes and you have not answered. 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
seatId string One 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.
unread boolean True 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.
leadsOnly boolean Only chats already linked to a lead.
needsHuman boolean Only chats where Kairon handed the conversation back and is waiting on you.
classification "interested" | "objection" | "neutral" | "not_interested" What their last inbound message meant — not a Pipeline stage. Combine with answered: "no" for interested replies you have not answered.
seen boolean True returns LinkedIn chats where they saw your last message and have not replied. Checks LinkedIn live for up to 50 chats first, so the first call can take up to a minute; calling again works through the rest. On normal (non-Sales Navigator) chats many people hide read receipts, so a chat missing here may still have been read. It already means your message is the last one: do not combine it with answered.
q string Case-insensitive match on the counterpart's NAME.
cursor string
limit integer

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 — and cc, the addresses a reply of yours copied. 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
chatId requiredstring
cursor string
limit integer

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. Write it in the Author's voice: read message_style_get first.

Parameters: inmail_send
NameTypeWhat it is for
target requiredobject Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id.
subject string The InMail subject line — the one thing that decides whether it is opened.
text requiredstring The InMail body, as it will be sent. Over MCP, em dashes and curly quotes become plain punctuation; nothing else rewrites it.
attachments object[]
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "write-messages" }) and of framework_get({ slug: "outreach" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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. Write it in the Author's voice: read message_style_get first.

Parameters: invite_send
NameTypeWhat it is for
target requiredobject Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id.
note string The connection-request note, up to 300 characters. Omit it entirely for a note-less invite, which is valid and often accepts better.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "write-messages" }) and of framework_get({ slug: "outreach" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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. LinkedIn allows this only within 60 minutes of sending; Kairon refuses a later attempt (INBOX_MESSAGE_CHANGE_WINDOW_PASSED) without calling LinkedIn. Metered.

Parameters: message_delete
NameTypeWhat it is for
chatId requiredstring
messageId requiredstring The 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 only within 60 minutes of sending; Kairon refuses a later attempt (INBOX_MESSAGE_CHANGE_WINDOW_PASSED) without calling LinkedIn. Updates the conversation in Kairon as well as on LinkedIn. Metered.

Parameters: message_edit
NameTypeWhat it is for
chatId requiredstring
messageId requiredstring The message's id from chat_messages.
text requiredstring The replacement body. It overwrites the message for the recipient too.

message_send

Send a direct message

  • write
  • destructive
  • open-world

Send a 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; on an EMAIL conversation the reply leaves from the mailbox the thread lives on, carries no attachment, and can copy up to 5 addresses with cc (an email-only field, refused on LinkedIn) — or target to open a NEW LinkedIn 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. A video goes as a message of its own, after the text; if a file could not follow, filesFailed says why — the message itself went, so never send it again. This contacts a real person, with no approval step. Write it in the Author's voice: read message_style_get first.

Parameters: message_send
NameTypeWhat it is for
chatId string Reply in this existing conversation (from chat_list). Use this, or target.
target object Open a NEW conversation with this person. Use this, or chatId.
text requiredstring The message body, as it will be sent. Em dashes and curly quotes become plain punctuation; nothing else rewrites it.
attachments object[]
cc string[] EMAIL conversations only: up to 5 addresses to copy on the reply, e.g. your main address so the conversation continues there. Refused on a LinkedIn conversation.
clientToken string Your own idempotency key. Send the SAME token when retrying after a lost response and the message is not sent twice.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "write-messages" }) and of framework_get({ slug: "outreach" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

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
search string Filter by name, email address or organization name.

impersonate_start

Act as another user

  • write
  • destructive
  • 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
user requiredstring The 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 campaign wizard's sender picker), every one connected and ready. Call it BEFORE campaign_create: a seat's id is exactly what goes in that tool's channelAccountIds array, 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. canSendInviteNote false means its invites carry no note. authCountry is where it authenticates — open a send window on noon there. An empty list means no seat here is ready to send from. If you expected one, ask the operator to contact support at admin@heykairon.com. 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?

allowance_get

Read the organization's AI balance

  • read-only
  • idempotent

What is left of the organization's monthly AI balance: remainingUsd (can be negative), includedUsd, usedUsd (null when it cannot be derived), and periodEndsAt when it resets. Call this BEFORE quoting a webdata_run, so you can tell the operator what they hold now and what the run would leave them with. When it answers free, quote no money at all. Reading it spends nothing.

Takes no parameters.

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
  • destructive
  • 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
competitorUrls string[] Replaces every competitor URL. Each must be a LinkedIn URL.
sourceUrls string[] Replaces every source URL. Any http(s) URL.
valueProposition string Replaces the organization's value proposition. An empty string clears it.
leadMagnet string Replaces the organization's lead magnet — what it gives a prospect to earn a reply. An empty string clears it.
learnings string Replaces the organization's GTM learnings log WHOLE. To append an entry, read the log first and write back the joined text. An empty string clears it.

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
slug requiredstring Which 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. The operator pays: Kairon passes on exactly what Instantly charged, with no markup — agreedTotal (the dueNow you showed them) is billed to them, and the order refuses if that price moved. Get their explicit yes to that amount, and tell them 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. Only placed is billed; the other three charge the operator nothing. 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
domain requiredstring The 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.
mailboxes requiredobject[] 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.
forwardingDomain string The 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.
agreedTotal requirednumber The 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
domain requiredstring A 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, nothing is charged. Only dueNow is billed to the operator, once, at purchase, at cost with no markup (billedForInfrastructure from email_setup_get says whether on their plan invoice or on one Kairon sends by hand); the monthly and yearly totals are what the order costs Kairon to keep running, not further charges to the operator. 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
domain requiredstring The 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.
mailboxes requiredobject[] 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.
forwardingDomain string The 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_format_set

Send email as plain text or HTML

  • write
  • destructive
  • idempotent
  • open-world

Switch how the managed email campaign sends: text (plain text, the default) or html (formatted). It applies to every email, not only the signature — html is what lets a mailbox signature carry links, bold or a logo. The email text Kairon writes keeps its line breaks either way. email_setup_get names the current emailFormat. Going back to text is refused while any mailbox signature still has HTML; remove it first with email_mailbox_signature_set. Plain text is the safer choice for cold email deliverability — switch only when the operator asks for a formatted signature.

Parameters: email_format_set
NameTypeWhat it is for
format required"text" | "html" text sends plain text (the default). html sends formatted email, so mailbox signatures may use HTML — links, bold, a logo. It applies to every email the managed campaign sends. Switching back to text is refused while a mailbox signature still has HTML in it.

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
email requiredstring The full address of a mailbox the customer already owns, like ada@acme.com.
firstName requiredstring The mailbox owner's first name — it appears in the From line of every send.
lastName requiredstring The mailbox owner's last name.
provider required"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.
password requiredstring An 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.
smtpHost string Outgoing mail server, custom provider only (Google and Microsoft are known).
smtpPort integer Outgoing mail port, custom provider only — usually 465 or 587.
imapHost string Incoming mail server, custom provider only.
imapPort integer Incoming mail port, custom provider only — usually 993.

email_mailbox_limit_set

Set a mailbox’s daily sending limit

  • write
  • destructive
  • idempotent
  • open-world

Set how many campaign emails one mailbox may send per day, from 1 to 30 (Instantly’s own recommendation for a cold mailbox). Some mailboxes allow fewer — email_setup_get lists each one’s dailyLimit and dailyLimitMax; a number above dailyLimitMax is refused. The sending campaign’s daily total is the sum of its mailboxes’ limits and follows by itself, so to send more in a day, add mailboxes rather than asking for more than 30 from one. Warmup email does not count. A new mailbox ramps up to its limit over about two weeks.

Parameters: email_mailbox_limit_set
NameTypeWhat it is for
email requiredstring The mailbox, as email_setup_get lists it under its domain.
dailyLimit requiredinteger Campaign emails this mailbox may send per day, 1 to 30. Some mailboxes allow fewer — email_setup_get names each one's dailyLimitMax. Warmup email does not count against it.

email_mailbox_signature_set

Set a mailbox’s email signature

  • write
  • destructive
  • idempotent
  • open-world

Set the signature under every campaign email one mailbox sends — up to 1000 characters, line breaks kept; an empty string removes it. Plain text, unless email_setup_get says emailFormat: "html": only then may it carry HTML (links, bold, a logo), and HTML in plain-text sending is refused. email_setup_get lists each mailbox’s current signature. Each email carries the signature of the mailbox that sends it, so give every mailbox a person sends from the same one. Do not also write the signature inside a step’s message, or it shows twice.

Parameters: email_mailbox_signature_set
NameTypeWhat it is for
email requiredstring The mailbox, as email_setup_get lists it under its domain.
signature requiredstring The text Instantly puts under every campaign email this mailbox sends — a name, a role, a phone. Line breaks are kept. Empty removes it. No {{ }} variables. HTML only when email_setup_get says emailFormat: "html" (switch it with email_format_set); in plain-text sending it is refused.

email_plan_request

Ask Kairon to set up email sending

  • write
  • idempotent

Ask Kairon to set this workspace up for email sending through Instantly — the sending plan. Owner or admin only. It is not charged automatically: a person at Kairon completes the purchase, and you will see it done when the workspace appears — so tell the operator it is being set up, not that it is ready. Asking twice is refused; ask email_setup_get first to see whether it is already open. Finding prospects' email addresses needs nothing from here: that is lead_enrich.

Takes no parameters.

email_setup_get

Read the email sending setup

  • read-only
  • idempotent
  • open-world

How this workspace sends email: emailProvider first — instantly, unipile (each person sends from their own Gmail/Outlook mailbox, connected by that person in Settings > Integrations > Email; there is nothing to buy or warm up), or null (no email yet). For an Instantly org it then says 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, and whether the plan was 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; the domain, mailbox and plan tools are Instantly-only.

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
domain requiredstring A 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
problem requiredstring What 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.
priority required"urgent" | "high" | "medium" | "low" How far up the queue this goes. Read these in order and stop at the FIRST one that fits — do not weigh them against each other. urgent — something wrong already left Kairon and cannot be taken back: a message sent to the wrong person or with broken text, a person contacted despite an exclusion, a wrong value written that the user has not seen. high — the user cannot finish what they came to do, and you found no way around it. medium — they finished, but only through a workaround, a retry, or repairing something by hand. low — they finished as intended; it cost extra turns, a confusing answer, or a limit nobody documented. Two tie-breakers: when two rungs both fit, send the LOWER one, and a mistake the user never saw is never urgent.
tools object[] 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.
quote string The 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 go-to-market axioms

  • read-only
  • idempotent

Axioms in full, as markdown — ONE by slug from framework_list, or a whole area at once (every axiom in it, in order). 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. An area read returns a readReceipt: building a list asks for targeting's, a campaign for outreach's. Pair with skill_get for the how and doc_get for what Kairon itself does.

Parameters: framework_get
NameTypeWhat it is for
slug string Which axiom to read — a slug from framework_list. An area name reads the whole area, like area.
area "belief-system" | "targeting" | "outreach" | "content" | "loop" Read every axiom of one area at once instead of one by slug.

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, or a whole area at once. 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.

message_style_get

Read how the Author writes messages, in their own words

  • read-only
  • idempotent

The Author's message style: free text about how they write invites, messages and InMails — their real messages as examples, how they open, how long and how formal they are. Kairon's own writers never read it and nothing judges it; it is context for YOU. Read it before you write or edit any message in their voice (message_send, invite_send, inmail_send, campaign copy). Empty text means none is set.

Takes no parameters.

message_style_set

Save how the Author writes messages

  • write
  • destructive
  • idempotent

Replace the Author's message style, whole. Kairon's own writers never read it: it changes only what an agent reads with message_style_get before writing a message for them. Best built from their REAL sent messages (chat_messages): paste 10-20 as examples, then note how they open, how long they write and how formal they are. An empty string removes it.

Parameters: message_style_set
NameTypeWhat it is for
text requiredstring The COMPLETE message style — it REPLACES what is stored. Put the Author's real messages in as examples, then how they open, how long they write, how formal they are and what their language is like. Kairon's own writers never read it; it is for you, before you write a message for them. An empty string removes it. At most 8000 characters.

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. The readReceipt it returns is what the tools of that job ask for in readReceipts.

Parameters: skill_get
NameTypeWhat it is for
name requiredstring Which 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.

webhook_delete

Take a webhook endpoint down

  • write
  • destructive

Kairon stops delivering to it at once. Its delivery history stays, so what it already received can still be read. To pause one instead, use webhook_set { id, status: "disabled_by_user" }.

Parameters: webhook_delete
NameTypeWhat it is for
id requiredstring

webhook_deliveries

Read what an endpoint was sent, and what it answered

  • read-only
  • idempotent

The recent deliveries to one endpoint, newest first: which event, when, whether it landed, how many attempts it took, and the status code the endpoint answered with. This is what answers 'my integration is not working' — a succeeded row means we delivered and their server accepted it.

Parameters: webhook_deliveries
NameTypeWhat it is for
endpointId requiredstring The endpoint whose recent deliveries to read.

webhook_list

Read the organization's webhook endpoints

  • read-only
  • idempotent

Every URL this organization has asked Kairon to POST events to, with the event types it wants, whether it is on, and how its last delivery went. The signing secret is NOT here — it is shown once, when the endpoint is created. An endpoint reading disabled_by_kairon was switched off by us after five days of total failure; turning it back on is webhook_set { id, status: 'enabled' }.

Takes no parameters.

webhook_set

Register or change a webhook endpoint

  • write

Without id, registers a new endpoint and returns its signing secret — the only time that secret is ever shown, so hand it to the operator immediately and tell them we cannot show it again. Before you write the code that RECEIVES these requests, read doc_get { slug: "webhooks" }: the signature check, the raw-body rule and what to answer are not guessable. With id, changes that endpoint. The URL must be https and must resolve to a public address: localhost, a private range and a cloud metadata address are all refused, and the refusal says which rule it broke. Use status to turn an endpoint off without losing its history.

Parameters: webhook_set
NameTypeWhat it is for
id string Omit to register a new endpoint. Present to change that one.
url string Where to POST. Must be https and must resolve to a public address. Required when creating.
description string The operator's own label for it, e.g. 'Our HubSpot sync'.
enabledEvents "lead.stage.changed"[] Which event types to send. Defaults to all of them when creating.
status "enabled" | "disabled_by_user" Turn it off without deleting it, or back on. Re-enabling clears its failure streak.

webhook_test

Send a test event to a webhook endpoint

  • write
  • open-world

Sends one sample lead.stage.changed (an invented person) to the endpoint right now, signed like a real event, and returns what the endpoint answered: attempt.statusCode (null when nothing answered) and attempt.error. Use it after writing or changing the receiver; if it does not answer 2xx, fix the receiver and test again, but stop after a few tries and report the status code. The sample lead id is always lead_00000000000000000000000000, so a receiver can accept it without storing it. One try, never retried, and it never counts toward the endpoint being switched off. The body it sent is in doc_get { slug: "webhooks" } as the example.

Parameters: webhook_test
NameTypeWhat it is for
id requiredstring The endpoint to send the sample event to.

workspace_rename

Rename the workspace

  • write
  • destructive
  • 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
name requiredstring The 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
draft requiredstring The exact text to judge, as it would go out.
surface required"post" | "comment" What kind of writing this is. It decides which rules apply: general plus this section and nothing else. Messages are not checked, so there is no dm.
context string What 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.
template string The 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 posts and comments to, in three sections: general (both), post, comment. Messages, invites and replies are not checked, so there is no dm. 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"[] Read only these sections. Omit for all three. general applies to posts and comments; the other two govern that surface alone. Messages have no section: they are not checked.

writing_check_set

Save the Author's own rules for one section

  • write
  • destructive
  • 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
section required"general" | "post" | "comment" 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.
checks requiredobject[] 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.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "writing-checks" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

writing_check_shared_set

Switch one of Kairon's own rules on or off

  • write
  • destructive
  • 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
key requiredstring A 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.
enabled requiredboolean false switches Kairon's rule off for this Author; true switches it back on.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "writing-checks" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

writing_style_get

Read how the Author comments, in their own words

  • read-only
  • idempotent

The Author's comment style: free text Kairon follows every time it writes a comment for them — their real comments as examples, how they use emoji, their kind of English. It guides the writer and is never judged; the comment rules from writing_check_list are what is enforced. Empty text means none is set. Read it before writing a comment in their voice.

Takes no parameters.

writing_style_set

Save how the Author comments

  • write
  • destructive
  • idempotent

Replace the Author's comment style, whole. Every comment Kairon drafts for them follows it, in every campaign, with no step to edit. Best built from their REAL comments (profile_comments on their own profile): paste 15-20 as examples, then note how they use emoji, how they put themselves in and what their English is like. A rule that must hold every time goes in writing_check_set instead. An empty string removes it.

Parameters: writing_style_set
NameTypeWhat it is for
text requiredstring The COMPLETE comment style — it REPLACES what is stored. Put the Author's real comments in as examples, then how they use emoji, how they bring themselves in and what their English is like. Guidance, never judged: anything that must hold every time belongs in a comment rule instead. An empty string removes it. At most 8000 characters.
readReceipts string[] | string Required: the readReceipt of skill_get({ name: "comment-voice" }). Read them BEFORE you start — they hold what Kairon measured to work for this job. A missing or expired receipt is refused and the refusal names what to read.

Presence

How do you show up on LinkedIn?

post_media_upload

Upload files for a LinkedIn post

  • read-only
  • idempotent

Show the operator an upload box in the chat, to add photos, a video or a PDF from their own computer to a LinkedIn post. Call it when they want to post a file they have locally — a file they attached to the chat cannot reach Kairon any other way. When they finish, a chat message names each file and its id; pass those to post_publish as media: [{ id }]. The box appears only in apps that show MCP Apps (Claude.ai, Claude Desktop); elsewhere nothing appears, so say so rather than wait.

Takes no parameters.

post_publish

Publish a LinkedIn post

  • write
  • destructive
  • open-world

Publish a LinkedIn post NOW from the CALLER'S OWN account — public at once. text up to 3,000 characters. media: up to 20 images, or one video, or one PDF, never mixed; 100 MB each and in total. A file on the operator's computer comes from post_media_upload as { id }; one online is { fileName, mimeType, url }; one you made is { fileName, mimeType, dataBase64 } (one, 3 MB). To tag, write {{0}} in text and pass mentions: [{ name, profileId }] (the provider id linkedin_fetch returns; isCompany: true for a page). Show the operator the exact text and files, and call only after a clear yes: no draft, no undo. Kairon turns em dashes into commas and curly quotes into straight ones, so show the text that way. Read writing_style_get first. Returns url, or url: null when LinkedIn gave no id (it is out). After a timeout or provider error the post may be out: have them check their profile before you retry. 3 posts a day.

Parameters: post_publish
NameTypeWhat it is for
text requiredstring The post as it will read on LinkedIn, at most 3000 characters.
media object[] The files to attach, in order. Each is { id } from the upload box, { fileName, mimeType, url } (https, fetched server-side), or { fileName, mimeType, dataBase64 } (one per post, at most 3 MB). Omit for a text-only post.
mentions object[] People or companies to tag. Write {{0}}, {{1}}… in text where each goes.

profile_edit

Edit your own LinkedIn profile

  • write
  • destructive
  • open-world

Change how the CALLER'S OWN LinkedIn profile looks — headline (max 220 chars), about (the About section, max 2600), picture, and banner (the wide image behind the top of the profile). This edits the seat you are acting as, never anyone else; to read a stranger's profile use linkedin_fetch. A PATCH: send only what should change, and anything you omit keeps its current value. Each image is one source or the other — inline dataBase64, or an https url we fetch server-side — and must be a jpeg, png, gif or webp. All four fields move in ONE call, so change them together rather than one at a time. There is no undo — Kairon does not read the old values first and keeps no copy, so the only way back is the operator restoring them on LinkedIn themselves. Show the operator exactly what you propose and get their agreement before calling. Capped at 20 edits a day.

Parameters: profile_edit
NameTypeWhat it is for
headline string The line under your name on LinkedIn. Omit to leave it as it is; there is no way to clear it from here.
about string The whole About section, replaced outright — send the full text you want, not an addition to it. Omit to leave it as it is.
picture object A new profile picture. Pass the bytes as dataBase64, or an https url we fetch — one or the other, never both. Must be a jpeg, png, gif or webp.
banner object A new background banner, the wide image behind the top of the profile. Same two sources and same image types as picture.

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.

ICP creation or qualification fails

The MCP can create and update ICPs with icp_create and icp_update. Judging is list_qualify, the one tool that spends your AI balance — tell the operator roughly what a run costs before you start it. Read the tool error first: missing criteria, stale versions and exhausted qualification allowance each return a specific reason.

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