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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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.
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.
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.
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.
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.
Is this company hiring? A published vacancy PROVES a need, dated and linked — the strongest buying signal for anyone selling staffing, recruitment or talent. Sweep a market with region + what you are looking for (this DISCOVERS companies, it does not filter yours), or check one with company, where zero postings is a real answer and not an error. region is required for a sweep: without it LinkedIn silently returns your own country only. Prefer roles over keywords — a job-title id matches the ROLE while a keyword matches anywhere in the ad, and on the same sweep that was 17 postings against 1241. Seniority is seniority, NEVER the word "Senior" in keywords: LinkedIn ORs keywords, so that widens the search (1241 → 2480) while looking like it narrows it. Hits are NOT deduped — one company often posts one need many times.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Remove members from a list ({ asset, id, items }). Each item names ONE member however you hold it: { memberId }, { url }, { providerId }, or { snapshot } (a hit passed back unedited — only its providerId is read). Deletes only the membership; the leads/companies stay in the org pool and their other lists. Per item: removed, not_member, or not_found.
Write custom-column values on members ALREADY in a list ({ asset, id, items }, ≤500). Each item names one member — { memberId }, { url }, { providerId } or { snapshot } — plus values, keyed by COLUMN NAME.
Read the full description
Merges: names you send are written, names you omit keep what they had, the empty string clears. Create columns first with list_update; a name this list lacks is skipped and echoed in ignored. Per item: set, not_member, not_found. Read them back with list_members.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
Find people by what they DID, not by what their profile says — the behavioural counterpart to search_people and search_sales_navigator. Pick a signal (who engaged with a profile or a keyword, who posted one, the seat's newest connections, who viewed it); each takes its own config. Results are RAW — real people who really did this, with no ICP screening, so screen them yourself. Pass intoList to append the page. The list-building skill covers which signal answers which question.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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.
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).
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.
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).
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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".
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.
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.
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.
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.
Check whether Kairon can buy this domain to send from, and what is available instead when it is taken. Only .com and .org can be bought, and a name carrying a well-known trademark is refused outright. Free, and buys nothing. Use it for a domain the customer does NOT own yet — a domain they already run needs email_mailbox_add instead.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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" }.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.