{
  "server": "https://app.heykairon.com/mcp",
  "clusters": [
    {
      "id": "list-building",
      "title": "List building",
      "question": "Who should we talk to, and which list are they in?"
    },
    {
      "id": "campaigns",
      "title": "Campaigns",
      "question": "What motion is running against them, and how is it doing?"
    },
    {
      "id": "inbox-outreach",
      "title": "Inbox & outreach",
      "question": "What did they say, and what do we say back?"
    },
    {
      "id": "acting-as",
      "title": "Acting as someone else",
      "question": "Whose seat is this running on?"
    },
    {
      "id": "craft",
      "title": "Craft & config",
      "question": "How does this work, and what have you told me?"
    },
    {
      "id": "presence",
      "title": "Presence",
      "question": "How do you show up on LinkedIn?"
    }
  ],
  "tools": [
    {
      "name": "audience_preview",
      "title": "Count who an audience rule would select",
      "description": "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`.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "audience",
          "type": "object",
          "required": true,
          "description": "The rule to count, exactly as `list_create` would take it. Nothing is created and no one is contacted."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "audience": {
            "type": "object",
            "properties": {
              "include": {
                "type": "object",
                "properties": {
                  "match": {
                    "type": "string",
                    "enum": [
                      "all",
                      "any"
                    ],
                    "description": "How this group's conditions join. `all` means a lead must satisfy EVERY condition; `any` means one is enough. Inside a single condition the fields always AND and the values in a field always OR, whatever `match` says — so `{ crmStage: ['on_hold','lost'], tagId: ['t1'] }` reads as (on_hold OR lost) AND t1."
                  },
                  "conditions": {
                    "minItems": 1,
                    "maxItems": 10,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "crmStage": {
                          "description": "Keep leads standing at ANY of these Pipeline columns. `on_hold` is the parked pile — people a campaign ran out of things to say to, or whose conversation went quiet for two weeks. `new` is NOT 'people arriving': it is every lead nobody has worked yet, including every CSV row ever imported, so a rule over it can select tens of thousands.",
                          "minItems": 1,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "new",
                              "invite_sent",
                              "connected",
                              "contacted",
                              "replied",
                              "interested",
                              "meeting_booked",
                              "won",
                              "on_hold",
                              "lost",
                              "discarded"
                            ]
                          }
                        },
                        "tagId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "ownerId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "qualificationStatus": {
                          "description": "Keep leads whose ICP verdict is ANY of these. A lead the audience adds can still be qualified afterwards like any other member — `list_qualify` works on an audience list exactly as it does on a hand-made one.",
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "lastContacted": {
                          "description": "Keep leads last contacted inside this window — `7d`, `30d`, `90d`, `never`, or `YYYY-MM-DD..YYYY-MM-DD`. Counts BOTH a campaign send and a message in the chat. This is the field that makes an `exclude` group safe: `exclude` + `lastContacted: '30d'` is how you say 'nobody who heard from me this month'. Resolved against the clock at every evaluation, so a stored `30d` always means the 30 days before tonight's run, never the 30 days before the day you saved it.",
                          "type": "string"
                        },
                        "companyId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "createdAfter": {
                          "description": "Keep only leads that ARRIVED in the org after this instant (ISO-8601, e.g. `2026-09-21T00:00:00.000Z`). This is what 'the people coming in' means: `crmStage: ['new']` on its own is the whole untouched backlog — every CSV row ever imported — which in the largest orgs is tens of thousands of people who have been sitting there for a year. Pair the two: `{ crmStage: ['new'], createdAfter: '<now>' }` is a flow for people who arrive from here on. An ABSOLUTE instant, not a rolling window, so the audience does not quietly forget the people it admitted last month.",
                          "type": "string",
                          "format": "date-time",
                          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
                        }
                      },
                      "additionalProperties": false
                    },
                    "description": "The conditions this group joins, 1 to 10. Each must name at least one field; an empty condition is refused rather than read as 'everyone'."
                  }
                },
                "required": [
                  "match",
                  "conditions"
                ],
                "description": "Who the rule selects. Required — a rule with only an exclude would select everyone and then take some away, which is never what an operator means."
              },
              "exclude": {
                "description": "Who to take back OUT of that selection. Optional, and evaluated as `include AND NOT exclude`, so both apply at once. A lead the exclude cannot decide about — no owner on file when you exclude by owner, no employer when you exclude by company — is KEPT, not dropped.",
                "type": "object",
                "properties": {
                  "match": {
                    "type": "string",
                    "enum": [
                      "all",
                      "any"
                    ],
                    "description": "How this group's conditions join. `all` means a lead must satisfy EVERY condition; `any` means one is enough. Inside a single condition the fields always AND and the values in a field always OR, whatever `match` says — so `{ crmStage: ['on_hold','lost'], tagId: ['t1'] }` reads as (on_hold OR lost) AND t1."
                  },
                  "conditions": {
                    "minItems": 1,
                    "maxItems": 10,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "crmStage": {
                          "description": "Keep leads standing at ANY of these Pipeline columns. `on_hold` is the parked pile — people a campaign ran out of things to say to, or whose conversation went quiet for two weeks. `new` is NOT 'people arriving': it is every lead nobody has worked yet, including every CSV row ever imported, so a rule over it can select tens of thousands.",
                          "minItems": 1,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "new",
                              "invite_sent",
                              "connected",
                              "contacted",
                              "replied",
                              "interested",
                              "meeting_booked",
                              "won",
                              "on_hold",
                              "lost",
                              "discarded"
                            ]
                          }
                        },
                        "tagId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "ownerId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "qualificationStatus": {
                          "description": "Keep leads whose ICP verdict is ANY of these. A lead the audience adds can still be qualified afterwards like any other member — `list_qualify` works on an audience list exactly as it does on a hand-made one.",
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "lastContacted": {
                          "description": "Keep leads last contacted inside this window — `7d`, `30d`, `90d`, `never`, or `YYYY-MM-DD..YYYY-MM-DD`. Counts BOTH a campaign send and a message in the chat. This is the field that makes an `exclude` group safe: `exclude` + `lastContacted: '30d'` is how you say 'nobody who heard from me this month'. Resolved against the clock at every evaluation, so a stored `30d` always means the 30 days before tonight's run, never the 30 days before the day you saved it.",
                          "type": "string"
                        },
                        "companyId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "createdAfter": {
                          "description": "Keep only leads that ARRIVED in the org after this instant (ISO-8601, e.g. `2026-09-21T00:00:00.000Z`). This is what 'the people coming in' means: `crmStage: ['new']` on its own is the whole untouched backlog — every CSV row ever imported — which in the largest orgs is tens of thousands of people who have been sitting there for a year. Pair the two: `{ crmStage: ['new'], createdAfter: '<now>' }` is a flow for people who arrive from here on. An ABSOLUTE instant, not a rolling window, so the audience does not quietly forget the people it admitted last month.",
                          "type": "string",
                          "format": "date-time",
                          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
                        }
                      },
                      "additionalProperties": false
                    },
                    "description": "The conditions this group joins, 1 to 10. Each must name at least one field; an empty condition is refused rather than read as 'everyone'."
                  }
                },
                "required": [
                  "match",
                  "conditions"
                ]
              }
            },
            "required": [
              "include"
            ],
            "description": "The rule to count, exactly as `list_create` would take it. Nothing is created and no one is contacted."
          }
        },
        "required": [
          "asset",
          "audience"
        ]
      }
    },
    {
      "name": "company_followers",
      "title": "List a company page’s followers",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "target",
          "type": "object",
          "required": true,
          "description": "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."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false,
          "description": "Continue from a previous page. Omit for the first page."
        },
        {
          "name": "count",
          "type": "integer",
          "required": false,
          "description": "Followers per page (default 50, the most LinkedIn allows for a page)."
        },
        {
          "name": "intoList",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "exhaust",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "maxResults",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "target": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "url",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "A LinkedIn profile or company URL."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "public_identifier",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The vanity slug (e.g. \"jane-doe\") or company universal name."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "provider_id",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "Unipile/LinkedIn internal id (e.g. \"ACoAAB…\" or the Sales-Nav \"ACwAAB…\")."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              }
            ],
            "description": "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": {
            "description": "Continue from a previous page. Omit for the first page.",
            "type": "string",
            "maxLength": 65536
          },
          "count": {
            "description": "Followers per page (default 50, the most LinkedIn allows for a page).",
            "type": "integer",
            "minimum": 1,
            "maximum": 50
          },
          "intoList": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "exhaust": {
            "description": "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.",
            "type": "boolean"
          },
          "maxResults": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "target"
        ]
      }
    },
    {
      "name": "company_posts",
      "title": "A company page's recent posts",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "target",
          "type": "object",
          "required": true,
          "description": "The company page whose posts to read — named by URL, universal name, or provider id."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "target": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "url",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "A LinkedIn profile or company URL."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "public_identifier",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The vanity slug (e.g. \"jane-doe\") or company universal name."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "provider_id",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "Unipile/LinkedIn internal id (e.g. \"ACoAAB…\" or the Sales-Nav \"ACwAAB…\")."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              }
            ],
            "description": "The company page whose posts to read — named by URL, universal name, or provider id."
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          }
        },
        "required": [
          "target"
        ]
      }
    },
    {
      "name": "crm_push",
      "title": "Send leads to the organization's CRM",
      "description": "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`.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "leadIds",
          "type": "string[]",
          "required": true,
          "description": "The leads to send (lead ids, `lead_…`), at most 1000."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "leadIds": {
            "minItems": 1,
            "maxItems": 1000,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            },
            "description": "The leads to send (lead ids, `lead_…`), at most 1000."
          }
        },
        "required": [
          "leadIds"
        ]
      }
    },
    {
      "name": "exclusion_add",
      "title": "Forbid contact with people or companies",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "facet",
          "type": "\"people\" | \"companies\"",
          "required": true,
          "description": "What these items are. `people` suppresses each named individual. `companies` suppresses the company itself AND everyone who works there."
        },
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "The entries to add, up to 20000. Each is judged on its own, so one invalid item never fails the rest."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "facet": {
            "type": "string",
            "enum": [
              "people",
              "companies"
            ],
            "description": "What these items are. `people` suppresses each named individual. `companies` suppresses the company itself AND everyone who works there."
          },
          "items": {
            "minItems": 1,
            "maxItems": 20000,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "linkedinUrl": {
                  "description": "On `people`: their profile URL, `https://www.linkedin.com/in/…`. On `companies`: the company page URL, `https://www.linkedin.com/company/…`. Either identifies the entry on its own.",
                  "type": "string",
                  "maxLength": 2048
                },
                "fullName": {
                  "description": "`people` only. Their whole name. Enough only WITH a company field beside it — a name alone is refused.",
                  "type": "string",
                  "maxLength": 200
                },
                "firstName": {
                  "description": "`people` only. Use with `lastName` when you hold no single full name. A first name alone is refused.",
                  "type": "string",
                  "maxLength": 200
                },
                "lastName": {
                  "description": "`people` only. The other half of `firstName`.",
                  "type": "string",
                  "maxLength": 200
                },
                "companyName": {
                  "description": "On `people`: where they work — one of the three company qualifiers a name needs. On `companies`: the company being excluded. Matched by name, which is also what makes `matchPastEmployees` able to catch anybody.",
                  "type": "string",
                  "maxLength": 200
                },
                "companyUrl": {
                  "description": "`people` only. Their employer's LinkedIn page URL — a company qualifier, an alternative to `companyName`.",
                  "type": "string",
                  "maxLength": 2048
                },
                "companyDomain": {
                  "description": "`people` only. Their employer's website domain (`acme.com`) — the third form of company qualifier.",
                  "type": "string",
                  "maxLength": 253
                },
                "domain": {
                  "description": "`companies` only. The company's own website domain, `acme.com`. On the `people` facet use `companyDomain` instead.",
                  "type": "string",
                  "maxLength": 253
                },
                "matchPastEmployees": {
                  "description": "`companies` only. Also suppress people who USED to work there, not only today's staff. Off by default. It matches on `companyName` alone, so an entry added as a bare URL or domain catches nobody this way. Settable only HERE, when the entry is created — changing it afterwards is done in the app.",
                  "type": "boolean"
                }
              }
            },
            "description": "The entries to add, up to 20000. Each is judged on its own, so one invalid item never fails the rest."
          }
        },
        "required": [
          "facet",
          "items"
        ]
      }
    },
    {
      "name": "exclusion_list",
      "title": "Read the organization's exclusion list",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "facet",
          "type": "\"people\" | \"companies\"",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "facet": {
            "description": "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.",
            "type": "string",
            "enum": [
              "people",
              "companies"
            ]
          }
        }
      }
    },
    {
      "name": "exclusion_remove",
      "title": "Remove one entry from the exclusion list",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "facet",
          "type": "\"people\" | \"companies\"",
          "required": true,
          "description": "Which facet the entry sits on. It decides the id prefix: `people` → `exper_…`, `companies` → `excom_…`."
        },
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The entry id, exactly as `exclusion_list` returned it."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "facet": {
            "type": "string",
            "enum": [
              "people",
              "companies"
            ],
            "description": "Which facet the entry sits on. It decides the id prefix: `people` → `exper_…`, `companies` → `excom_…`."
          },
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The entry id, exactly as `exclusion_list` returned it."
          }
        },
        "required": [
          "facet",
          "id"
        ]
      }
    },
    {
      "name": "filter_resolve",
      "title": "Resolve a Sales Navigator filter",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "type",
          "type": "\"location\" | \"industry\" | \"function\" | \"current_company\" | \"past_company\" | \"company_location\" | \"school\" | \"past_role\" | \"groups\" | \"persona\" | \"technologies\" | \"account_lists\" | \"lead_lists\" | \"postal_code\"",
          "required": true,
          "description": "Which Sales Navigator dimension the name belongs to."
        },
        {
          "name": "query",
          "type": "string",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "location",
              "industry",
              "function",
              "current_company",
              "past_company",
              "company_location",
              "school",
              "past_role",
              "groups",
              "persona",
              "technologies",
              "account_lists",
              "lead_lists",
              "postal_code"
            ],
            "description": "Which Sales Navigator dimension the name belongs to."
          },
          "query": {
            "type": "string",
            "minLength": 1,
            "maxLength": 256,
            "description": "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."
          }
        },
        "required": [
          "type",
          "query"
        ]
      }
    },
    {
      "name": "group_members",
      "title": "List a LinkedIn group’s members",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "group",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false,
          "description": "Continue from a previous page. Omit for the first page."
        },
        {
          "name": "count",
          "type": "integer",
          "required": false,
          "description": "Members per page (default 50, max 100). LinkedIn refuses anything higher."
        },
        {
          "name": "intoList",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "exhaust",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "maxResults",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "group": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "description": "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": {
            "description": "Continue from a previous page. Omit for the first page.",
            "type": "string",
            "maxLength": 65536
          },
          "count": {
            "description": "Members per page (default 50, max 100). LinkedIn refuses anything higher.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "intoList": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "exhaust": {
            "description": "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.",
            "type": "boolean"
          },
          "maxResults": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "group"
        ]
      }
    },
    {
      "name": "icp_create",
      "title": "Create an ICP",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "name",
          "type": "string",
          "required": true,
          "description": "A short, specific name — the segment, not the pitch."
        },
        {
          "name": "definition",
          "type": "object",
          "required": true,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "A short, specific name — the segment, not the pitch."
          },
          "definition": {
            "type": "object",
            "properties": {
              "description": {
                "description": "The targeting thesis in prose: who these companies and people are, the pains they carry, and the observable signals that say one needs this NOW. Not sent to Sales Navigator and not judged as a check.",
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 5000
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "companySalesNavCriteria": {
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {},
                    "additionalProperties": {},
                    "description": "Sales Navigator criteria — the same filter object `search_sales_navigator` takes (its full schema is in this same tool list; reuse those filter names and value shapes). Kept open here to keep this tool small; it is validated strictly on the server, so a wrong filter name comes back as a per-field error rather than being stored. Use `null` for \"this side has no criteria\". Keep them BROAD; by default no title or geography."
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "peopleSalesNavCriteria": {
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {},
                    "additionalProperties": {},
                    "description": "Sales Navigator criteria — the same filter object `search_sales_navigator` takes (its full schema is in this same tool list; reuse those filter names and value shapes). Kept open here to keep this tool small; it is validated strictly on the server, so a wrong filter name comes back as a per-field error rather than being stored. Use `null` for \"this side has no criteria\". Keep them BROAD; by default no title or geography."
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "companyQualificationConditions": {
                "description": "Judges the COMPANY. Rules: see `definition`.",
                "maxItems": 200,
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 128
                    },
                    "description": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2000
                    },
                    "webSearch": {
                      "type": "boolean"
                    },
                    "siteKeywords": {
                      "type": "object",
                      "properties": {
                        "include": {
                          "maxItems": 32,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 120,
                            "pattern": "^[^\"]*$"
                          }
                        },
                        "exclude": {
                          "maxItems": 32,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 120,
                            "pattern": "^[^\"]*$"
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "description"
                  ]
                }
              },
              "strictFilters": {
                "type": "boolean"
              },
              "waiveMissingCompany": {
                "type": "boolean"
              },
              "peopleQualificationConditions": {
                "description": "Judges the PERSON. Rules: see `definition`.",
                "maxItems": 200,
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 128
                    },
                    "description": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2000
                    },
                    "webSearch": {
                      "type": "boolean"
                    },
                    "siteKeywords": {
                      "type": "object",
                      "properties": {
                        "include": {
                          "maxItems": 32,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 120,
                            "pattern": "^[^\"]*$"
                          }
                        },
                        "exclude": {
                          "maxItems": 32,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 120,
                            "pattern": "^[^\"]*$"
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "description"
                  ]
                }
              }
            },
            "additionalProperties": {},
            "description": "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": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "name",
          "definition"
        ]
      }
    },
    {
      "name": "icp_criteria_list_edit",
      "title": "Add or remove values in one ICP filter list",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "icpId",
          "type": "string",
          "required": true
        },
        {
          "name": "side",
          "type": "\"company\" | \"people\"",
          "required": true,
          "description": "Which criteria side to edit: the company search or the people search."
        },
        {
          "name": "list",
          "type": "\"role.include\" | \"role.exclude\" | \"seniority.include\" | \"seniority.exclude\" | \"companyHeadcount\" | \"filters\" | \"headcount\"",
          "required": true,
          "description": "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."
        },
        {
          "name": "add",
          "type": "string | object[]",
          "required": false,
          "description": "Values to append. Strings for every list but `filters`, which takes `{ type, query, mode? }`. A value already there is left alone, never duplicated."
        },
        {
          "name": "remove",
          "type": "string | object[]",
          "required": false,
          "description": "Values to drop, matched exactly (a `filters` item on type + query + mode). A value that is not there is reported back, not an error."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "icpId": {
            "type": "string",
            "minLength": 1
          },
          "side": {
            "type": "string",
            "enum": [
              "company",
              "people"
            ],
            "description": "Which criteria side to edit: the company search or the people search."
          },
          "list": {
            "type": "string",
            "enum": [
              "role.include",
              "role.exclude",
              "seniority.include",
              "seniority.exclude",
              "companyHeadcount",
              "filters",
              "headcount"
            ],
            "description": "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": {
            "description": "Values to append. Strings for every list but `filters`, which takes `{ type, query, mode? }`. A value already there is left alone, never duplicated.",
            "maxItems": 200,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                },
                {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "enum": [
                        "location",
                        "industry",
                        "function",
                        "current_company",
                        "past_company",
                        "company_location",
                        "school",
                        "past_role",
                        "groups",
                        "persona",
                        "technologies",
                        "account_lists",
                        "lead_lists",
                        "postal_code"
                      ],
                      "description": "Which id-resolved dimension. People: location, industry, function, current_company, past_company, company_location, school, past_role, groups, persona, account_lists, lead_lists, postal_code. Companies: location, industry, technologies, account_lists, postal_code — the rest are people-only and are REFUSED here rather than quietly ignored. On a companies search the company IS the subject, so its location is plain `location` (`company_location` is accepted as a synonym for it, and means the EMPLOYER’s location only on a people search). `groups` is measured from YOU, like the saved lists: LinkedIn only knows the groups your own account has JOINED, so it works only on your OWN Sales Navigator seat and is otherwise refused, rather than answered about a different audience. To reach the people in a group, use `group_members` instead — it reads the group’s own member list on your own seat: any public group, or a members-only one your seat has joined."
                    },
                    "query": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 256,
                      "description": "The value as a human-readable NAME (never an id). Phrase it the way LinkedIn names things — canonical industry names (\"Financial Services\", not \"fintech\"), real place names (\"Greater Madrid Metropolitan Area\" or the country). A `postal_code` is the FULL code as the country writes it — Argentina's \"C1425ECB\", not \"1425\", which resolves to a code abroad; check the echoed label names the right country. Resolution takes the best single typeahead match and echoes the resolved label back."
                    },
                    "mode": {
                      "description": "\"include\" (default) requires the value; \"exclude\" filters it out.",
                      "type": "string",
                      "enum": [
                        "include",
                        "exclude"
                      ]
                    }
                  },
                  "required": [
                    "type",
                    "query"
                  ]
                }
              ]
            }
          },
          "remove": {
            "description": "Values to drop, matched exactly (a `filters` item on type + query + mode). A value that is not there is reported back, not an error.",
            "maxItems": 200,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                },
                {
                  "type": "object",
                  "properties": {
                    "type": {
                      "type": "string",
                      "enum": [
                        "location",
                        "industry",
                        "function",
                        "current_company",
                        "past_company",
                        "company_location",
                        "school",
                        "past_role",
                        "groups",
                        "persona",
                        "technologies",
                        "account_lists",
                        "lead_lists",
                        "postal_code"
                      ],
                      "description": "Which id-resolved dimension. People: location, industry, function, current_company, past_company, company_location, school, past_role, groups, persona, account_lists, lead_lists, postal_code. Companies: location, industry, technologies, account_lists, postal_code — the rest are people-only and are REFUSED here rather than quietly ignored. On a companies search the company IS the subject, so its location is plain `location` (`company_location` is accepted as a synonym for it, and means the EMPLOYER’s location only on a people search). `groups` is measured from YOU, like the saved lists: LinkedIn only knows the groups your own account has JOINED, so it works only on your OWN Sales Navigator seat and is otherwise refused, rather than answered about a different audience. To reach the people in a group, use `group_members` instead — it reads the group’s own member list on your own seat: any public group, or a members-only one your seat has joined."
                    },
                    "query": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 256,
                      "description": "The value as a human-readable NAME (never an id). Phrase it the way LinkedIn names things — canonical industry names (\"Financial Services\", not \"fintech\"), real place names (\"Greater Madrid Metropolitan Area\" or the country). A `postal_code` is the FULL code as the country writes it — Argentina's \"C1425ECB\", not \"1425\", which resolves to a code abroad; check the echoed label names the right country. Resolution takes the best single typeahead match and echoes the resolved label back."
                    },
                    "mode": {
                      "description": "\"include\" (default) requires the value; \"exclude\" filters it out.",
                      "type": "string",
                      "enum": [
                        "include",
                        "exclude"
                      ]
                    }
                  },
                  "required": [
                    "type",
                    "query"
                  ]
                }
              ]
            }
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "icpId",
          "side",
          "list"
        ]
      }
    },
    {
      "name": "icp_delete",
      "title": "Delete an ICP",
      "description": "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).",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "icpId",
          "type": "string",
          "required": true
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "icpId": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "icpId"
        ]
      }
    },
    {
      "name": "icp_get",
      "title": "Get an ICP",
      "description": "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`.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "icpId",
          "type": "string",
          "required": true
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "icpId": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "icpId"
        ]
      }
    },
    {
      "name": "icp_list",
      "title": "List ICPs",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "icp_publish",
      "title": "Publish an ICP's draft",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "icpId",
          "type": "string",
          "required": true
        },
        {
          "name": "ifDraftUpdatedAt",
          "type": "string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "icpId": {
            "type": "string",
            "minLength": 1
          },
          "ifDraftUpdatedAt": {
            "description": "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.",
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
          }
        },
        "required": [
          "icpId"
        ]
      }
    },
    {
      "name": "icp_update",
      "title": "Update an ICP",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "icpId",
          "type": "string",
          "required": true
        },
        {
          "name": "name",
          "type": "string",
          "required": false,
          "description": "A short, specific name — the segment, not the pitch."
        },
        {
          "name": "definition",
          "type": "object",
          "required": true,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "icpId": {
            "type": "string",
            "minLength": 1
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "A short, specific name — the segment, not the pitch."
          },
          "definition": {
            "type": "object",
            "properties": {
              "description": {
                "description": "The targeting thesis in prose: who these companies and people are, the pains they carry, and the observable signals that say one needs this NOW. Not sent to Sales Navigator and not judged as a check.",
                "anyOf": [
                  {
                    "type": "string",
                    "maxLength": 5000
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "companySalesNavCriteria": {
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {},
                    "additionalProperties": {},
                    "description": "Sales Navigator criteria — the same filter object `search_sales_navigator` takes (its full schema is in this same tool list; reuse those filter names and value shapes). Kept open here to keep this tool small; it is validated strictly on the server, so a wrong filter name comes back as a per-field error rather than being stored. Use `null` for \"this side has no criteria\". Keep them BROAD; by default no title or geography."
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "peopleSalesNavCriteria": {
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {},
                    "additionalProperties": {},
                    "description": "Sales Navigator criteria — the same filter object `search_sales_navigator` takes (its full schema is in this same tool list; reuse those filter names and value shapes). Kept open here to keep this tool small; it is validated strictly on the server, so a wrong filter name comes back as a per-field error rather than being stored. Use `null` for \"this side has no criteria\". Keep them BROAD; by default no title or geography."
                  },
                  {
                    "type": "null"
                  }
                ]
              },
              "companyQualificationConditions": {
                "description": "Judges the COMPANY. Rules: see `definition`.",
                "maxItems": 200,
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 128
                    },
                    "description": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2000
                    },
                    "webSearch": {
                      "type": "boolean"
                    },
                    "siteKeywords": {
                      "type": "object",
                      "properties": {
                        "include": {
                          "maxItems": 32,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 120,
                            "pattern": "^[^\"]*$"
                          }
                        },
                        "exclude": {
                          "maxItems": 32,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 120,
                            "pattern": "^[^\"]*$"
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "description"
                  ]
                }
              },
              "strictFilters": {
                "type": "boolean"
              },
              "waiveMissingCompany": {
                "type": "boolean"
              },
              "peopleQualificationConditions": {
                "description": "Judges the PERSON. Rules: see `definition`.",
                "maxItems": 200,
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 128
                    },
                    "description": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2000
                    },
                    "webSearch": {
                      "type": "boolean"
                    },
                    "siteKeywords": {
                      "type": "object",
                      "properties": {
                        "include": {
                          "maxItems": 32,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 120,
                            "pattern": "^[^\"]*$"
                          }
                        },
                        "exclude": {
                          "maxItems": 32,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 120,
                            "pattern": "^[^\"]*$"
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "description"
                  ]
                }
              }
            },
            "additionalProperties": {},
            "description": "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": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "icpId",
          "definition"
        ]
      }
    },
    {
      "name": "icp_version_restore",
      "title": "Bring an earlier ICP definition back into the draft",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "icpId",
          "type": "string",
          "required": true
        },
        {
          "name": "version",
          "type": "integer",
          "required": true,
          "description": "The version to bring back, as `icp_versions` listed it."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "icpId": {
            "type": "string",
            "minLength": 1
          },
          "version": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "The version to bring back, as `icp_versions` listed it."
          }
        },
        "required": [
          "icpId",
          "version"
        ]
      }
    },
    {
      "name": "icp_versions",
      "title": "Read an ICP's earlier definitions",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "icpId",
          "type": "string",
          "required": true
        },
        {
          "name": "version",
          "type": "integer",
          "required": false,
          "description": "Omit for the list of versions. Give one to get THAT version in full."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "icpId": {
            "type": "string",
            "minLength": 1
          },
          "version": {
            "description": "Omit for the list of versions. Give one to get THAT version in full.",
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "icpId"
        ]
      }
    },
    {
      "name": "invitations_received",
      "title": "Pending connection requests",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "cursor",
          "type": "string",
          "required": false,
          "description": "Continue from a previous page. Omit for the first page."
        },
        {
          "name": "count",
          "type": "integer",
          "required": false,
          "description": "Invitations per page (default 50, max 100)."
        },
        {
          "name": "intoList",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "exhaust",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "maxResults",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "cursor": {
            "description": "Continue from a previous page. Omit for the first page.",
            "type": "string",
            "maxLength": 65536
          },
          "count": {
            "description": "Invitations per page (default 50, max 100).",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "intoList": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "exhaust": {
            "description": "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.",
            "type": "boolean"
          },
          "maxResults": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        }
      }
    },
    {
      "name": "job_search",
      "title": "Search LinkedIn job postings",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "keywords",
          "type": "string",
          "required": false,
          "description": "What the company is hiring FOR — a product, stack or role (\"Oracle\", \"NetSuite\", \"SAP HCM\"). This is the market sweep: it returns every company hiring that profile, so it DISCOVERS companies rather than filtering ones you already hold. Requires `region` unless `company` is also set. A product name usually needs no translation — \"Oracle\" catches Portuguese and Spanish postings alike."
        },
        {
          "name": "region",
          "type": "string",
          "required": false,
          "description": "The country or region to sweep, in plain language (\"Brazil\", \"Mexico\") — resolved to LinkedIn's id for you, and echoed back so a wrong match is visible. REQUIRED for a keyword sweep: without it LinkedIn silently returns only the seat's own country, which measured 33 results against 151 for the same search. Optional in company mode, where the question is not geographic. A bare numeric id is accepted and passes through."
        },
        {
          "name": "company",
          "type": "object",
          "required": false,
          "description": "Check ONE company instead of sweeping a market: \"is this company hiring, anywhere?\". A `provider_id` is free; a url or slug costs one cache-first company lookup and the resolved id is echoed back, so hold it for next time. NO postings is a real answer here, not an error — it means this company is not hiring, which disqualifies a lead with a reason."
        },
        {
          "name": "datePosted",
          "type": "integer",
          "required": false,
          "description": "How many days back to look. A sweep defaults to 30 days, which is almost always right: LinkedIn removes a posting when the role closes, so 90 days measured 170 postings against 151 at 30 — to find MORE companies repeat the sweep in a few weeks rather than widening it. A COMPANY CHECK has no default window: \"is this company hiring\" is asked of everything still open, so an empty answer means \"not hiring\" rather than \"nothing lately\"."
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "description": "Page size, default 50 (the provider maximum). One page costs one metered action whatever its size, so a small page buys nothing. The response carries `total`: compare it to the postings you hold and keep calling with the returned `cursor` until `cursor` is null."
        },
        {
          "name": "roles",
          "type": "string[]",
          "required": false,
          "description": "Job TITLES from LinkedIn's own taxonomy, in plain language (\"Product Analyst\", \"Head of Growth\") — resolved to title ids for you and echoed back. This is the sharpest filter this tool has and usually beats `keywords`: measured on the same Argentina sweep, `keywords: \"Product Analyst\"` returned 1241 postings while `roles: [\"Product Analyst\"]` returned 17, because keywords match anywhere in a posting while a title id matches the ROLE. LinkedIn treats several titles as OR — pass the variants you mean (\"Product Analyst\", \"Senior Product Analyst\")."
        },
        {
          "name": "functions",
          "type": "string[]",
          "required": false,
          "description": "Job FUNCTIONS — the department a role belongs to (\"Product Management\", \"Engineering\", \"Marketing\"), resolved to LinkedIn ids and echoed back. Much wider than `roles`: Product Management alone returned 207 postings where the \"Product Analyst\" title returned 17. Reach for it to size a department across a market; reach for `roles` to find one job."
        },
        {
          "name": "industries",
          "type": "string[]",
          "required": false,
          "description": "The HIRING COMPANY's industry, in plain language (\"Financial Services\", \"Software Development\") — resolved to LinkedIn ids and echoed back. This filters by the employer, not by the role, so it is how a sweep is aimed at the market you actually sell to."
        },
        {
          "name": "seniority",
          "type": "\"executive\" | \"director\" | \"mid_senior\" | \"associate\" | \"entry\" | \"intern\"[]",
          "required": false,
          "description": "LinkedIn's EXPERIENCE LEVEL on the posting. This is the ONLY way to ask for seniority — putting \"Senior\" in `keywords` does the opposite of what it looks like, because LinkedIn ORs the words: the same sweep went from 1241 hits to 2480, wider, not narrower. Several levels are OR (\"senior and above\" is `[\"mid_senior\",\"director\",\"executive\"]`, measured 675 against a 1241 baseline)."
        },
        {
          "name": "jobType",
          "type": "\"full_time\" | \"part_time\" | \"contract\" | \"temporary\" | \"volunteer\" | \"internship\" | \"other\"[]",
          "required": false,
          "description": "LinkedIn's JOB TYPE — the employment arrangement. Several are OR."
        },
        {
          "name": "presence",
          "type": "\"on_site\" | \"hybrid\" | \"remote\"[]",
          "required": false,
          "description": "On-site / hybrid / remote, as the posting declares it. Several are OR."
        },
        {
          "name": "underTenApplicants",
          "type": "boolean",
          "required": false,
          "description": "LinkedIn's UNDER 10 APPLICANTS. The sharpest recency filter there is — it says the posting is not just recent but still UNWORKED, and it cut the same 1241-hit sweep to 127. Use it when the value of the signal is getting there first."
        },
        {
          "name": "inYourNetwork",
          "type": "boolean",
          "required": false,
          "description": "LinkedIn's IN YOUR NETWORK — only companies where the seat already has a connection. Warm ground: it cut the same sweep from 1241 to 265, and every hit comes with a path in."
        },
        {
          "name": "easyApply",
          "type": "boolean",
          "required": false,
          "description": "LinkedIn's EASY APPLY."
        },
        {
          "name": "hasVerifications",
          "type": "boolean",
          "required": false,
          "description": "LinkedIn's HAS VERIFICATIONS — the poster verified their identity."
        },
        {
          "name": "fairChanceEmployer",
          "type": "boolean",
          "required": false,
          "description": "LinkedIn's FAIR CHANCE EMPLOYER."
        },
        {
          "name": "benefits",
          "type": "\"medical_insurance\" | \"vision_insurance\" | \"dental_insurance\" | \"disability_insurance\" | \"401(k)\" | \"pension_plan\" | \"paid_maternity_leave\" | \"paid_paternity_leave\" | \"commuter_benefits\" | \"student_loan_assistance\" | \"tuition_assistance\"[]",
          "required": false,
          "description": "LinkedIn's BENEFITS, as the posting declares them. Several are OR."
        },
        {
          "name": "commitments",
          "type": "\"career_growth_and_learning\" | \"diversity_equity_and_inclusion\" | \"environmental_sustainability\" | \"social_impact\" | \"work_life_balance\"[]",
          "required": false,
          "description": "LinkedIn's COMMITMENTS, as the company declares them. Several are OR."
        },
        {
          "name": "minimumSalary",
          "type": "object",
          "required": false,
          "description": "LinkedIn's SALARY floor. Both fields are required together, and `value` is in THOUSANDS from a fixed ladder LinkedIn will not interpolate — dollars (USD/AUD/CAD) step 40…200, pounds (GBP) step 20…100. Off-ladder numbers are refused rather than rounded."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false,
          "description": "The opaque forward cursor from the previous page. `null` came back = no more. It carries the whole search, so send it alone: the filters need not be repeated."
        },
        {
          "name": "intoList",
          "type": "string",
          "required": false,
          "description": "Where this page's HIRING COMPANIES are appended — always a companies list, because a job posting surfaces a company, never a person. Free: the postings already carry each company's identity, so nothing is re-fetched. A company hiring for several roles is appended once."
        },
        {
          "name": "exhaust",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "maxResults",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "keywords": {
            "description": "What the company is hiring FOR — a product, stack or role (\"Oracle\", \"NetSuite\", \"SAP HCM\"). This is the market sweep: it returns every company hiring that profile, so it DISCOVERS companies rather than filtering ones you already hold. Requires `region` unless `company` is also set. A product name usually needs no translation — \"Oracle\" catches Portuguese and Spanish postings alike.",
            "type": "string",
            "minLength": 1,
            "maxLength": 512
          },
          "region": {
            "description": "The country or region to sweep, in plain language (\"Brazil\", \"Mexico\") — resolved to LinkedIn's id for you, and echoed back so a wrong match is visible. REQUIRED for a keyword sweep: without it LinkedIn silently returns only the seat's own country, which measured 33 results against 151 for the same search. Optional in company mode, where the question is not geographic. A bare numeric id is accepted and passes through.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "company": {
            "description": "Check ONE company instead of sweeping a market: \"is this company hiring, anywhere?\". A `provider_id` is free; a url or slug costs one cache-first company lookup and the resolved id is echoed back, so hold it for next time. NO postings is a real answer here, not an error — it means this company is not hiring, which disqualifies a lead with a reason.",
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "url",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "A LinkedIn profile or company URL."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "public_identifier",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The vanity slug (e.g. \"jane-doe\") or company universal name."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "provider_id",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "Unipile/LinkedIn internal id (e.g. \"ACoAAB…\" or the Sales-Nav \"ACwAAB…\")."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              }
            ]
          },
          "datePosted": {
            "description": "How many days back to look. A sweep defaults to 30 days, which is almost always right: LinkedIn removes a posting when the role closes, so 90 days measured 170 postings against 151 at 30 — to find MORE companies repeat the sweep in a few weeks rather than widening it. A COMPANY CHECK has no default window: \"is this company hiring\" is asked of everything still open, so an empty answer means \"not hiring\" rather than \"nothing lately\".",
            "type": "integer",
            "minimum": 1,
            "maximum": 365
          },
          "limit": {
            "description": "Page size, default 50 (the provider maximum). One page costs one metered action whatever its size, so a small page buys nothing. The response carries `total`: compare it to the postings you hold and keep calling with the returned `cursor` until `cursor` is null.",
            "type": "integer",
            "minimum": 1,
            "maximum": 50
          },
          "roles": {
            "description": "Job TITLES from LinkedIn's own taxonomy, in plain language (\"Product Analyst\", \"Head of Growth\") — resolved to title ids for you and echoed back. This is the sharpest filter this tool has and usually beats `keywords`: measured on the same Argentina sweep, `keywords: \"Product Analyst\"` returned 1241 postings while `roles: [\"Product Analyst\"]` returned 17, because keywords match anywhere in a posting while a title id matches the ROLE. LinkedIn treats several titles as OR — pass the variants you mean (\"Product Analyst\", \"Senior Product Analyst\").",
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          "functions": {
            "description": "Job FUNCTIONS — the department a role belongs to (\"Product Management\", \"Engineering\", \"Marketing\"), resolved to LinkedIn ids and echoed back. Much wider than `roles`: Product Management alone returned 207 postings where the \"Product Analyst\" title returned 17. Reach for it to size a department across a market; reach for `roles` to find one job.",
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          "industries": {
            "description": "The HIRING COMPANY's industry, in plain language (\"Financial Services\", \"Software Development\") — resolved to LinkedIn ids and echoed back. This filters by the employer, not by the role, so it is how a sweep is aimed at the market you actually sell to.",
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          "seniority": {
            "description": "LinkedIn's EXPERIENCE LEVEL on the posting. This is the ONLY way to ask for seniority — putting \"Senior\" in `keywords` does the opposite of what it looks like, because LinkedIn ORs the words: the same sweep went from 1241 hits to 2480, wider, not narrower. Several levels are OR (\"senior and above\" is `[\"mid_senior\",\"director\",\"executive\"]`, measured 675 against a 1241 baseline).",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "executive",
                "director",
                "mid_senior",
                "associate",
                "entry",
                "intern"
              ]
            }
          },
          "jobType": {
            "description": "LinkedIn's JOB TYPE — the employment arrangement. Several are OR.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "full_time",
                "part_time",
                "contract",
                "temporary",
                "volunteer",
                "internship",
                "other"
              ]
            }
          },
          "presence": {
            "description": "On-site / hybrid / remote, as the posting declares it. Several are OR.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "on_site",
                "hybrid",
                "remote"
              ]
            }
          },
          "underTenApplicants": {
            "description": "LinkedIn's UNDER 10 APPLICANTS. The sharpest recency filter there is — it says the posting is not just recent but still UNWORKED, and it cut the same 1241-hit sweep to 127. Use it when the value of the signal is getting there first.",
            "type": "boolean"
          },
          "inYourNetwork": {
            "description": "LinkedIn's IN YOUR NETWORK — only companies where the seat already has a connection. Warm ground: it cut the same sweep from 1241 to 265, and every hit comes with a path in.",
            "type": "boolean"
          },
          "easyApply": {
            "description": "LinkedIn's EASY APPLY.",
            "type": "boolean"
          },
          "hasVerifications": {
            "description": "LinkedIn's HAS VERIFICATIONS — the poster verified their identity.",
            "type": "boolean"
          },
          "fairChanceEmployer": {
            "description": "LinkedIn's FAIR CHANCE EMPLOYER.",
            "type": "boolean"
          },
          "benefits": {
            "description": "LinkedIn's BENEFITS, as the posting declares them. Several are OR.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "medical_insurance",
                "vision_insurance",
                "dental_insurance",
                "disability_insurance",
                "401(k)",
                "pension_plan",
                "paid_maternity_leave",
                "paid_paternity_leave",
                "commuter_benefits",
                "student_loan_assistance",
                "tuition_assistance"
              ]
            }
          },
          "commitments": {
            "description": "LinkedIn's COMMITMENTS, as the company declares them. Several are OR.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "career_growth_and_learning",
                "diversity_equity_and_inclusion",
                "environmental_sustainability",
                "social_impact",
                "work_life_balance"
              ]
            }
          },
          "minimumSalary": {
            "description": "LinkedIn's SALARY floor. Both fields are required together, and `value` is in THOUSANDS from a fixed ladder LinkedIn will not interpolate — dollars (USD/AUD/CAD) step 40…200, pounds (GBP) step 20…100. Off-ladder numbers are refused rather than rounded.",
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "currency": {
                    "type": "string",
                    "enum": [
                      "USD",
                      "AUD",
                      "CAD"
                    ],
                    "description": "A dollar currency — these three share one ladder (40…200, in 20s)."
                  },
                  "value": {
                    "anyOf": [
                      {
                        "type": "number",
                        "const": 40
                      },
                      {
                        "type": "number",
                        "const": 60
                      },
                      {
                        "type": "number",
                        "const": 80
                      },
                      {
                        "type": "number",
                        "const": 100
                      },
                      {
                        "type": "number",
                        "const": 120
                      },
                      {
                        "type": "number",
                        "const": 140
                      },
                      {
                        "type": "number",
                        "const": 160
                      },
                      {
                        "type": "number",
                        "const": 180
                      },
                      {
                        "type": "number",
                        "const": 200
                      }
                    ],
                    "description": "In THOUSANDS of dollars, from LinkedIn's fixed ladder (40 = \"$40,000+\")."
                  }
                },
                "required": [
                  "currency",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "currency": {
                    "type": "string",
                    "const": "GBP",
                    "description": "Pounds — a different ladder (20…100, in 10s)."
                  },
                  "value": {
                    "anyOf": [
                      {
                        "type": "number",
                        "const": 20
                      },
                      {
                        "type": "number",
                        "const": 30
                      },
                      {
                        "type": "number",
                        "const": 40
                      },
                      {
                        "type": "number",
                        "const": 50
                      },
                      {
                        "type": "number",
                        "const": 60
                      },
                      {
                        "type": "number",
                        "const": 70
                      },
                      {
                        "type": "number",
                        "const": 80
                      },
                      {
                        "type": "number",
                        "const": 90
                      },
                      {
                        "type": "number",
                        "const": 100
                      }
                    ],
                    "description": "In THOUSANDS of pounds, from LinkedIn's fixed ladder (20 = \"£20,000+\")."
                  }
                },
                "required": [
                  "currency",
                  "value"
                ]
              }
            ]
          },
          "cursor": {
            "description": "The opaque forward cursor from the previous page. `null` came back = no more. It carries the whole search, so send it alone: the filters need not be repeated.",
            "type": "string",
            "maxLength": 65536
          },
          "intoList": {
            "description": "Where this page's HIRING COMPANIES are appended — always a companies list, because a job posting surfaces a company, never a person. Free: the postings already carry each company's identity, so nothing is re-fetched. A company hiring for several roles is appended once.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "exhaust": {
            "description": "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.",
            "type": "boolean"
          },
          "maxResults": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        }
      }
    },
    {
      "name": "lead_email_set",
      "title": "Record what a finder learned about people's email addresses",
      "description": "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.\n\nSend `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.\n\nPer 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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "What was learned about each person, each naming its subject by ONE of `memberId`, `url`, or `providerId`, plus either an `email` or `notFound: true`."
        },
        {
          "name": "source",
          "type": "\"clay\" | \"apollo\" | \"contactout\" | \"hunter\" | \"instantly\"",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "items": {
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "allOf": [
                {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "memberId": {
                          "type": "string",
                          "minLength": 1
                        }
                      },
                      "required": [
                        "memberId"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 2048,
                          "description": "The person's LinkedIn profile URL, used when you do not have their `memberId`. Name each item exactly one way."
                        }
                      },
                      "required": [
                        "url"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "providerId": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 256
                        }
                      },
                      "required": [
                        "providerId"
                      ]
                    }
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "email": {
                      "description": "The address the finder returned. Pass `notFound: true` instead when it came back empty — that is a different fact, and the one that stops the next run paying to ask again. Clearing an address is a human correction and belongs on the lead itself, not here.",
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "notFound": {
                      "description": "The finder was asked about this person and returned nothing. Stamps `emailNotFoundAt` so a later pass skips them instead of re-paying. Never send this together with an `email`.",
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              ]
            },
            "description": "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": {
            "type": "string",
            "enum": [
              "clay",
              "apollo",
              "contactout",
              "hunter",
              "instantly"
            ],
            "description": "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."
          }
        },
        "required": [
          "items",
          "source"
        ]
      }
    },
    {
      "name": "lead_enrich",
      "title": "Find people's work emails and mobile phones",
      "description": "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.\n\nPaid 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).\n\nPer 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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "The people to search for, each named by ONE of `memberId`, `url`, or `providerId`."
        },
        {
          "name": "email",
          "type": "boolean",
          "required": false,
          "description": "Find their work email."
        },
        {
          "name": "phone",
          "type": "boolean",
          "required": false,
          "description": "Find their mobile phone."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "items": {
            "minItems": 1,
            "maxItems": 100,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "memberId": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "required": [
                    "memberId"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2048,
                      "description": "The person's LinkedIn profile URL, used when you do not have their `memberId`. Name each item exactly one way."
                    }
                  },
                  "required": [
                    "url"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "providerId": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 256
                    }
                  },
                  "required": [
                    "providerId"
                  ]
                }
              ]
            },
            "description": "The people to search for, each named by ONE of `memberId`, `url`, or `providerId`."
          },
          "email": {
            "description": "Find their work email.",
            "type": "boolean"
          },
          "phone": {
            "description": "Find their mobile phone.",
            "type": "boolean"
          }
        },
        "required": [
          "items"
        ]
      }
    },
    {
      "name": "lead_phone_set",
      "title": "Record what a finder learned about people's phone numbers",
      "description": "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).\n\nSend `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.\n\nPer 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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "What was learned about each person, each naming its subject by ONE of `memberId`, `url`, or `providerId`, plus either a `phone` or `notFound: true`."
        },
        {
          "name": "source",
          "type": "\"clay\" | \"apollo\" | \"contactout\"",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "items": {
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "allOf": [
                {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "memberId": {
                          "type": "string",
                          "minLength": 1
                        }
                      },
                      "required": [
                        "memberId"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 2048,
                          "description": "The person's LinkedIn profile URL, used when you do not have their `memberId`. Name each item exactly one way."
                        }
                      },
                      "required": [
                        "url"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "providerId": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 256
                        }
                      },
                      "required": [
                        "providerId"
                      ]
                    }
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "phone": {
                      "description": "The number the finder returned, as it returned it (`+54 9 11 5555-1234`, `(415) 555-0100`) — Kairon stores it as given. When the finder returned several, send the best one: mobile first. Pass `notFound: true` instead when it came back empty. Clearing a number is a human correction and belongs on the lead itself, not here.",
                      "anyOf": [
                        {
                          "type": "string"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "notFound": {
                      "description": "The finder was asked about this person and returned no phone. Stamps `phoneNotFoundAt` so a later pass skips them instead of re-paying. Never send this together with a `phone`.",
                      "type": "boolean",
                      "const": true
                    }
                  }
                }
              ]
            },
            "description": "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": {
            "type": "string",
            "enum": [
              "clay",
              "apollo",
              "contactout"
            ],
            "description": "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."
          }
        },
        "required": [
          "items",
          "source"
        ]
      }
    },
    {
      "name": "linkedin_fetch",
      "title": "Fetch a LinkedIn profile or company",
      "description": "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`.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "target",
          "type": "object",
          "required": true,
          "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
        },
        {
          "name": "kind",
          "type": "\"profile\" | \"company\"",
          "required": false,
          "description": "Override auto-detection. Required for a company addressed by universal name or numeric id."
        },
        {
          "name": "refresh",
          "type": "boolean",
          "required": false,
          "description": "True bypasses the cache and forces a live read from LinkedIn. Slower, and it spends a lookup against your headroom."
        },
        {
          "name": "withRecommendations",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "relationship",
          "type": "boolean",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "target": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "url",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "A LinkedIn profile or company URL."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "public_identifier",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The vanity slug (e.g. \"jane-doe\") or company universal name."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "provider_id",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "Unipile/LinkedIn internal id (e.g. \"ACoAAB…\" or the Sales-Nav \"ACwAAB…\")."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              }
            ],
            "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
          },
          "kind": {
            "description": "Override auto-detection. Required for a company addressed by universal name or numeric id.",
            "type": "string",
            "enum": [
              "profile",
              "company"
            ]
          },
          "refresh": {
            "description": "True bypasses the cache and forces a live read from LinkedIn. Slower, and it spends a lookup against your headroom.",
            "type": "boolean"
          },
          "withRecommendations": {
            "description": "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.",
            "type": "boolean"
          },
          "relationship": {
            "description": "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.",
            "type": "boolean"
          }
        },
        "required": [
          "target"
        ]
      }
    },
    {
      "name": "list_add",
      "title": "Add members to a list",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "The rows to add, each naming its subject by ONE of `memberId`, `url`, `providerId`, or `snapshot`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "items": {
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "memberId": {
                      "type": "string",
                      "minLength": 1
                    },
                    "values": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "string",
                        "maxLength": 20000
                      },
                      "description": "This member's custom-column values, keyed by column NAME. Only names the list already has; the empty string clears one."
                    }
                  },
                  "required": [
                    "memberId"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2048,
                      "description": "The person's LinkedIn profile URL, used when you do not have their `memberId`. Name each item exactly one way."
                    },
                    "values": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "string",
                        "maxLength": 20000
                      },
                      "description": "This member's custom-column values, keyed by column NAME. Only names the list already has; the empty string clears one."
                    }
                  },
                  "required": [
                    "url"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "snapshot": {
                      "type": "object",
                      "properties": {
                        "providerId": {
                          "type": "string",
                          "minLength": 1
                        },
                        "publicIdentifier": {
                          "type": "string"
                        },
                        "profileUrl": {
                          "type": "string"
                        },
                        "name": {
                          "type": "string"
                        },
                        "headline": {
                          "type": "string"
                        },
                        "company": {
                          "type": "string"
                        },
                        "companyId": {
                          "type": "string"
                        },
                        "title": {
                          "type": "string"
                        },
                        "industry": {
                          "type": "string"
                        },
                        "location": {
                          "type": "string"
                        },
                        "avatarUrl": {
                          "type": "string"
                        },
                        "description": {
                          "type": "string"
                        },
                        "website": {
                          "type": "string"
                        },
                        "industries": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "employeeCount": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        "followerCount": {
                          "type": "integer",
                          "minimum": -9007199254740991,
                          "maximum": 9007199254740991
                        },
                        "hqCity": {
                          "type": "string"
                        },
                        "hqCountry": {
                          "type": "string"
                        },
                        "logoUrl": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "providerId"
                      ]
                    },
                    "values": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "string",
                        "maxLength": 20000
                      },
                      "description": "This member's custom-column values, keyed by column NAME. Only names the list already has; the empty string clears one."
                    }
                  },
                  "required": [
                    "snapshot"
                  ]
                }
              ]
            },
            "description": "The rows to add, each naming its subject by ONE of `memberId`, `url`, `providerId`, or `snapshot`."
          }
        },
        "required": [
          "asset",
          "id",
          "items"
        ]
      }
    },
    {
      "name": "list_archive",
      "title": "Archive or restore a list",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "archived",
          "type": "boolean",
          "required": true,
          "description": "true archives the list; false restores it."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "archived": {
            "type": "boolean",
            "description": "true archives the list; false restores it."
          }
        },
        "required": [
          "asset",
          "id",
          "archived"
        ]
      }
    },
    {
      "name": "list_create",
      "title": "Create a list",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "name",
          "type": "string",
          "required": true,
          "description": "The list's name, for the operator to recognise it."
        },
        {
          "name": "audience",
          "type": "object",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "The list's name, for the operator to recognise it."
          },
          "audience": {
            "description": "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.",
            "type": "object",
            "properties": {
              "include": {
                "type": "object",
                "properties": {
                  "match": {
                    "type": "string",
                    "enum": [
                      "all",
                      "any"
                    ],
                    "description": "How this group's conditions join. `all` means a lead must satisfy EVERY condition; `any` means one is enough. Inside a single condition the fields always AND and the values in a field always OR, whatever `match` says — so `{ crmStage: ['on_hold','lost'], tagId: ['t1'] }` reads as (on_hold OR lost) AND t1."
                  },
                  "conditions": {
                    "minItems": 1,
                    "maxItems": 10,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "crmStage": {
                          "description": "Keep leads standing at ANY of these Pipeline columns. `on_hold` is the parked pile — people a campaign ran out of things to say to, or whose conversation went quiet for two weeks. `new` is NOT 'people arriving': it is every lead nobody has worked yet, including every CSV row ever imported, so a rule over it can select tens of thousands.",
                          "minItems": 1,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "new",
                              "invite_sent",
                              "connected",
                              "contacted",
                              "replied",
                              "interested",
                              "meeting_booked",
                              "won",
                              "on_hold",
                              "lost",
                              "discarded"
                            ]
                          }
                        },
                        "tagId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "ownerId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "qualificationStatus": {
                          "description": "Keep leads whose ICP verdict is ANY of these. A lead the audience adds can still be qualified afterwards like any other member — `list_qualify` works on an audience list exactly as it does on a hand-made one.",
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "lastContacted": {
                          "description": "Keep leads last contacted inside this window — `7d`, `30d`, `90d`, `never`, or `YYYY-MM-DD..YYYY-MM-DD`. Counts BOTH a campaign send and a message in the chat. This is the field that makes an `exclude` group safe: `exclude` + `lastContacted: '30d'` is how you say 'nobody who heard from me this month'. Resolved against the clock at every evaluation, so a stored `30d` always means the 30 days before tonight's run, never the 30 days before the day you saved it.",
                          "type": "string"
                        },
                        "companyId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "createdAfter": {
                          "description": "Keep only leads that ARRIVED in the org after this instant (ISO-8601, e.g. `2026-09-21T00:00:00.000Z`). This is what 'the people coming in' means: `crmStage: ['new']` on its own is the whole untouched backlog — every CSV row ever imported — which in the largest orgs is tens of thousands of people who have been sitting there for a year. Pair the two: `{ crmStage: ['new'], createdAfter: '<now>' }` is a flow for people who arrive from here on. An ABSOLUTE instant, not a rolling window, so the audience does not quietly forget the people it admitted last month.",
                          "type": "string",
                          "format": "date-time",
                          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
                        }
                      },
                      "additionalProperties": false
                    },
                    "description": "The conditions this group joins, 1 to 10. Each must name at least one field; an empty condition is refused rather than read as 'everyone'."
                  }
                },
                "required": [
                  "match",
                  "conditions"
                ],
                "description": "Who the rule selects. Required — a rule with only an exclude would select everyone and then take some away, which is never what an operator means."
              },
              "exclude": {
                "description": "Who to take back OUT of that selection. Optional, and evaluated as `include AND NOT exclude`, so both apply at once. A lead the exclude cannot decide about — no owner on file when you exclude by owner, no employer when you exclude by company — is KEPT, not dropped.",
                "type": "object",
                "properties": {
                  "match": {
                    "type": "string",
                    "enum": [
                      "all",
                      "any"
                    ],
                    "description": "How this group's conditions join. `all` means a lead must satisfy EVERY condition; `any` means one is enough. Inside a single condition the fields always AND and the values in a field always OR, whatever `match` says — so `{ crmStage: ['on_hold','lost'], tagId: ['t1'] }` reads as (on_hold OR lost) AND t1."
                  },
                  "conditions": {
                    "minItems": 1,
                    "maxItems": 10,
                    "type": "array",
                    "items": {
                      "type": "object",
                      "properties": {
                        "crmStage": {
                          "description": "Keep leads standing at ANY of these Pipeline columns. `on_hold` is the parked pile — people a campaign ran out of things to say to, or whose conversation went quiet for two weeks. `new` is NOT 'people arriving': it is every lead nobody has worked yet, including every CSV row ever imported, so a rule over it can select tens of thousands.",
                          "minItems": 1,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "new",
                              "invite_sent",
                              "connected",
                              "contacted",
                              "replied",
                              "interested",
                              "meeting_booked",
                              "won",
                              "on_hold",
                              "lost",
                              "discarded"
                            ]
                          }
                        },
                        "tagId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "ownerId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "qualificationStatus": {
                          "description": "Keep leads whose ICP verdict is ANY of these. A lead the audience adds can still be qualified afterwards like any other member — `list_qualify` works on an audience list exactly as it does on a hand-made one.",
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "lastContacted": {
                          "description": "Keep leads last contacted inside this window — `7d`, `30d`, `90d`, `never`, or `YYYY-MM-DD..YYYY-MM-DD`. Counts BOTH a campaign send and a message in the chat. This is the field that makes an `exclude` group safe: `exclude` + `lastContacted: '30d'` is how you say 'nobody who heard from me this month'. Resolved against the clock at every evaluation, so a stored `30d` always means the 30 days before tonight's run, never the 30 days before the day you saved it.",
                          "type": "string"
                        },
                        "companyId": {
                          "anyOf": [
                            {
                              "type": "string"
                            },
                            {
                              "type": "array",
                              "items": {
                                "type": "string"
                              }
                            }
                          ]
                        },
                        "createdAfter": {
                          "description": "Keep only leads that ARRIVED in the org after this instant (ISO-8601, e.g. `2026-09-21T00:00:00.000Z`). This is what 'the people coming in' means: `crmStage: ['new']` on its own is the whole untouched backlog — every CSV row ever imported — which in the largest orgs is tens of thousands of people who have been sitting there for a year. Pair the two: `{ crmStage: ['new'], createdAfter: '<now>' }` is a flow for people who arrive from here on. An ABSOLUTE instant, not a rolling window, so the audience does not quietly forget the people it admitted last month.",
                          "type": "string",
                          "format": "date-time",
                          "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
                        }
                      },
                      "additionalProperties": false
                    },
                    "description": "The conditions this group joins, 1 to 10. Each must name at least one field; an empty condition is refused rather than read as 'everyone'."
                  }
                },
                "required": [
                  "match",
                  "conditions"
                ]
              }
            },
            "required": [
              "include"
            ]
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "asset",
          "name"
        ]
      }
    },
    {
      "name": "list_delete",
      "title": "Delete a list",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "asset",
          "id"
        ]
      }
    },
    {
      "name": "list_get",
      "title": "Get one list",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "asset",
          "id"
        ]
      }
    },
    {
      "name": "list_list",
      "title": "List lists",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "q",
          "type": "string",
          "required": false,
          "description": "Case-insensitive substring of the list's name."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "default": "25"
        },
        {
          "name": "archived",
          "type": "boolean",
          "required": false,
          "description": "false (default) lists the active lists only. true lists ONLY the archived ones — archived lists never appear in the default view.",
          "default": "false"
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "q": {
            "description": "Case-insensitive substring of the list's name.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          },
          "limit": {
            "default": 25,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "archived": {
            "default": false,
            "description": "false (default) lists the active lists only. true lists ONLY the archived ones — archived lists never appear in the default view.",
            "type": "boolean"
          }
        },
        "required": [
          "asset"
        ]
      }
    },
    {
      "name": "list_members",
      "title": "Read a list's members",
      "description": "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.\n\nNARROW HERE — every filter below runs in SQL; never page a list and sift it yourself.\n\n`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.\n\nA 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`.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "q",
          "type": "string",
          "required": false,
          "description": "Case-insensitive substring over the member's display fields."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "default": "25"
        },
        {
          "name": "tags",
          "type": "string[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "sort",
          "type": "\"person\" | \"title\" | \"company\" | \"email\" | \"phone\" | \"industry\" | \"qualification\" | \"owner\" | \"added\" | \"source\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "direction",
          "type": "\"asc\" | \"desc\"",
          "required": false,
          "description": "Order direction; read as `asc` once a `sort` field is named."
        },
        {
          "name": "qualificationStatus",
          "type": "string | string[]",
          "required": false,
          "description": "Keep members whose ICP verdict is ANY OF these: qualified, disqualified, unevaluated. `unevaluated` means nobody has judged them — never that they failed."
        },
        {
          "name": "ownerId",
          "type": "string | string[]",
          "required": false,
          "description": "Keep members owned by ANY OF these user ids. Ownership rides the entity, not the list."
        },
        {
          "name": "tagId",
          "type": "string | string[]",
          "required": false,
          "description": "Keep members carrying ANY OF these tag ids — an OR, and the counterpart to `tags` above, which is an AND over NAMES."
        },
        {
          "name": "addedAfter",
          "type": "string",
          "required": false,
          "description": "Keep members added to THIS list at or after this ISO timestamp."
        },
        {
          "name": "addedBefore",
          "type": "string",
          "required": false,
          "description": "Keep members added to THIS list strictly before this ISO timestamp."
        },
        {
          "name": "hydrated",
          "type": "boolean",
          "required": false,
          "description": "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)."
        },
        {
          "name": "column",
          "type": "object[]",
          "required": false,
          "description": "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'."
        },
        {
          "name": "includeQualification",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "signalKey",
          "type": "string | string[]",
          "required": false,
          "description": "Leads only. Keep members that arrived from ANY OF these signals."
        },
        {
          "name": "crmStage",
          "type": "\"new\" | \"invite_sent\" | \"connected\" | \"contacted\" | \"replied\" | \"interested\" | \"meeting_booked\" | \"won\" | \"on_hold\" | \"lost\" | \"discarded\"[]",
          "required": false,
          "description": "Leads only. Keep members standing at ANY OF these pipeline stages."
        },
        {
          "name": "emailState",
          "type": "\"has\" | \"missing\" | \"unasked\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "phoneState",
          "type": "\"has\" | \"missing\" | \"unasked\"",
          "required": false,
          "description": "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)."
        },
        {
          "name": "employeeMin",
          "type": "integer",
          "required": false,
          "description": "Companies only. Keep companies with AT LEAST this many employees."
        },
        {
          "name": "employeeMax",
          "type": "integer",
          "required": false,
          "description": "Companies only. Keep companies with AT MOST this many employees."
        },
        {
          "name": "industry",
          "type": "string[]",
          "required": false,
          "description": "Companies only. Keep companies carrying ANY OF these industries, matched case-insensitively against the whole industry list, not just the one shown as `subtitle`."
        },
        {
          "name": "hqCountry",
          "type": "string[]",
          "required": false,
          "description": "Companies only. Keep companies whose HQ is in ANY OF these ISO-2 country codes."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "q": {
            "description": "Case-insensitive substring over the member's display fields.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          },
          "limit": {
            "default": 25,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "tags": {
            "description": "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.",
            "maxItems": 25,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 30
            }
          },
          "sort": {
            "description": "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.",
            "type": "string",
            "enum": [
              "person",
              "title",
              "company",
              "email",
              "phone",
              "industry",
              "qualification",
              "owner",
              "added",
              "source"
            ]
          },
          "direction": {
            "description": "Order direction; read as `asc` once a `sort` field is named.",
            "type": "string",
            "enum": [
              "asc",
              "desc"
            ]
          },
          "qualificationStatus": {
            "description": "Keep members whose ICP verdict is ANY OF these: qualified, disqualified, unevaluated. `unevaluated` means nobody has judged them — never that they failed.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "ownerId": {
            "description": "Keep members owned by ANY OF these user ids. Ownership rides the entity, not the list.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "tagId": {
            "description": "Keep members carrying ANY OF these tag ids — an OR, and the counterpart to `tags` above, which is an AND over NAMES.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "addedAfter": {
            "description": "Keep members added to THIS list at or after this ISO timestamp.",
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "addedBefore": {
            "description": "Keep members added to THIS list strictly before this ISO timestamp.",
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "hydrated": {
            "description": "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).",
            "type": "boolean"
          },
          "column": {
            "description": "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'.",
            "maxItems": 5,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 40,
                  "description": "The column name, as list_get reports it. A name this list lacks is refused."
                },
                "equals": {
                  "description": "Keep members whose WHOLE cell equals this, case-insensitively. The empty string asks for the cells nobody has filled in.",
                  "type": "string",
                  "maxLength": 2000
                },
                "contains": {
                  "description": "Keep members whose cell CONTAINS this, case-insensitively. Matched literally.",
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 2000
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "includeQualification": {
            "description": "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.",
            "type": "boolean"
          },
          "signalKey": {
            "description": "Leads only. Keep members that arrived from ANY OF these signals.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "crmStage": {
            "description": "Leads only. Keep members standing at ANY OF these pipeline stages.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "new",
                "invite_sent",
                "connected",
                "contacted",
                "replied",
                "interested",
                "meeting_booked",
                "won",
                "on_hold",
                "lost",
                "discarded"
              ]
            }
          },
          "emailState": {
            "description": "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.",
            "type": "string",
            "enum": [
              "has",
              "missing",
              "unasked"
            ]
          },
          "phoneState": {
            "description": "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).",
            "type": "string",
            "enum": [
              "has",
              "missing",
              "unasked"
            ]
          },
          "employeeMin": {
            "description": "Companies only. Keep companies with AT LEAST this many employees.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "employeeMax": {
            "description": "Companies only. Keep companies with AT MOST this many employees.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "industry": {
            "description": "Companies only. Keep companies carrying ANY OF these industries, matched case-insensitively against the whole industry list, not just the one shown as `subtitle`.",
            "minItems": 1,
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          "hqCountry": {
            "description": "Companies only. Keep companies whose HQ is in ANY OF these ISO-2 country codes.",
            "minItems": 1,
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2
            }
          }
        },
        "required": [
          "asset",
          "id"
        ]
      }
    },
    {
      "name": "list_qualify",
      "title": "Qualify a list against an ICP",
      "description": "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.\n\nEvery 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.\n\nThe `list-building` skill has the order to qualify in.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "icpId",
          "type": "string",
          "required": true,
          "description": "The ICP to judge against. Its ACTIVE version is pinned when the run starts."
        },
        {
          "name": "subjectIds",
          "type": "string[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "sample",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "ignoreIcpMismatch",
          "type": "boolean",
          "required": false,
          "description": "Has 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.",
          "default": "false"
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "icpId": {
            "type": "string",
            "minLength": 1,
            "description": "The ICP to judge against. Its ACTIVE version is pinned when the run starts."
          },
          "subjectIds": {
            "description": "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.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            }
          },
          "sample": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 20
          },
          "ignoreIcpMismatch": {
            "default": false,
            "description": "Has 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.",
            "type": "boolean"
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "asset",
          "id",
          "icpId"
        ]
      }
    },
    {
      "name": "list_qualify_cancel",
      "title": "Stop a qualification run",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "runId",
          "type": "string",
          "required": true,
          "description": "The run to stop."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "runId": {
            "type": "string",
            "minLength": 1,
            "description": "The run to stop."
          }
        },
        "required": [
          "asset",
          "id",
          "runId"
        ]
      }
    },
    {
      "name": "list_qualify_runs",
      "title": "Read a list's qualification run",
      "description": "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.\n\n`judged` = paid to decide (`qualified`/`disqualified`); `alreadyCurrent` skipped free; `failed` could not be READ (`failedReasons` says why) and stays unevaluated.\n\n`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.\n\n`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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "runId",
          "type": "string",
          "required": false,
          "description": "A specific run. Omit for this list's most recent one."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "runId": {
            "description": "A specific run. Omit for this list's most recent one.",
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "asset",
          "id"
        ]
      }
    },
    {
      "name": "list_remove",
      "title": "Remove members from a list",
      "description": "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`.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "The rows to remove, each named by any one identity the member could have been added with."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "items": {
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "memberId": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "required": [
                    "memberId"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2048,
                      "description": "The person's LinkedIn profile URL, used when you do not have their `memberId`. Name each item exactly one way."
                    }
                  },
                  "required": [
                    "url"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "providerId": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 256
                    }
                  },
                  "required": [
                    "providerId"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "snapshot": {
                      "type": "object",
                      "properties": {
                        "providerId": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 256
                        }
                      },
                      "required": [
                        "providerId"
                      ],
                      "additionalProperties": {}
                    }
                  },
                  "required": [
                    "snapshot"
                  ]
                }
              ]
            },
            "description": "The rows to remove, each named by any one identity the member could have been added with."
          }
        },
        "required": [
          "asset",
          "id",
          "items"
        ]
      }
    },
    {
      "name": "list_set",
      "title": "Set members' custom-column values",
      "description": "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.\n\nMerges: 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.\n\nWhen 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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "The rows to write, each naming its subject plus the custom-column `values` to set on it."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "items": {
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "allOf": [
                {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "memberId": {
                          "type": "string",
                          "minLength": 1
                        }
                      },
                      "required": [
                        "memberId"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 2048,
                          "description": "The person's LinkedIn profile URL, used when you do not have their `memberId`. Name each item exactly one way."
                        }
                      },
                      "required": [
                        "url"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "providerId": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 256
                        }
                      },
                      "required": [
                        "providerId"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "snapshot": {
                          "type": "object",
                          "properties": {
                            "providerId": {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 256
                            }
                          },
                          "required": [
                            "providerId"
                          ],
                          "additionalProperties": {}
                        }
                      },
                      "required": [
                        "snapshot"
                      ]
                    }
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "values": {
                      "type": "object",
                      "propertyNames": {
                        "type": "string"
                      },
                      "additionalProperties": {
                        "type": "string",
                        "maxLength": 20000
                      },
                      "description": "This member's custom-column values, keyed by column NAME. Only names the list already has; the empty string clears one."
                    }
                  },
                  "required": [
                    "values"
                  ]
                }
              ]
            },
            "description": "The rows to write, each naming its subject plus the custom-column `values` to set on it."
          }
        },
        "required": [
          "asset",
          "id",
          "items"
        ]
      }
    },
    {
      "name": "list_show",
      "title": "Show a list in the chat",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "q",
          "type": "string",
          "required": false,
          "description": "Case-insensitive substring over the member's display fields."
        },
        {
          "name": "tags",
          "type": "string[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "sort",
          "type": "\"person\" | \"title\" | \"company\" | \"email\" | \"phone\" | \"industry\" | \"qualification\" | \"owner\" | \"added\" | \"source\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "direction",
          "type": "\"asc\" | \"desc\"",
          "required": false,
          "description": "Order direction; read as `asc` once a `sort` field is named."
        },
        {
          "name": "qualificationStatus",
          "type": "string | string[]",
          "required": false,
          "description": "Keep members whose ICP verdict is ANY OF these: qualified, disqualified, unevaluated. `unevaluated` means nobody has judged them — never that they failed."
        },
        {
          "name": "ownerId",
          "type": "string | string[]",
          "required": false,
          "description": "Keep members owned by ANY OF these user ids. Ownership rides the entity, not the list."
        },
        {
          "name": "tagId",
          "type": "string | string[]",
          "required": false,
          "description": "Keep members carrying ANY OF these tag ids — an OR, and the counterpart to `tags` above, which is an AND over NAMES."
        },
        {
          "name": "addedAfter",
          "type": "string",
          "required": false,
          "description": "Keep members added to THIS list at or after this ISO timestamp."
        },
        {
          "name": "addedBefore",
          "type": "string",
          "required": false,
          "description": "Keep members added to THIS list strictly before this ISO timestamp."
        },
        {
          "name": "hydrated",
          "type": "boolean",
          "required": false,
          "description": "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)."
        },
        {
          "name": "column",
          "type": "object[]",
          "required": false,
          "description": "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'."
        },
        {
          "name": "signalKey",
          "type": "string | string[]",
          "required": false,
          "description": "Leads only. Keep members that arrived from ANY OF these signals."
        },
        {
          "name": "crmStage",
          "type": "\"new\" | \"invite_sent\" | \"connected\" | \"contacted\" | \"replied\" | \"interested\" | \"meeting_booked\" | \"won\" | \"on_hold\" | \"lost\" | \"discarded\"[]",
          "required": false,
          "description": "Leads only. Keep members standing at ANY OF these pipeline stages."
        },
        {
          "name": "emailState",
          "type": "\"has\" | \"missing\" | \"unasked\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "phoneState",
          "type": "\"has\" | \"missing\" | \"unasked\"",
          "required": false,
          "description": "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)."
        },
        {
          "name": "employeeMin",
          "type": "integer",
          "required": false,
          "description": "Companies only. Keep companies with AT LEAST this many employees."
        },
        {
          "name": "employeeMax",
          "type": "integer",
          "required": false,
          "description": "Companies only. Keep companies with AT MOST this many employees."
        },
        {
          "name": "industry",
          "type": "string[]",
          "required": false,
          "description": "Companies only. Keep companies carrying ANY OF these industries, matched case-insensitively against the whole industry list, not just the one shown as `subtitle`."
        },
        {
          "name": "hqCountry",
          "type": "string[]",
          "required": false,
          "description": "Companies only. Keep companies whose HQ is in ANY OF these ISO-2 country codes."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "q": {
            "description": "Case-insensitive substring over the member's display fields.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "tags": {
            "description": "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.",
            "maxItems": 25,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 30
            }
          },
          "sort": {
            "description": "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.",
            "type": "string",
            "enum": [
              "person",
              "title",
              "company",
              "email",
              "phone",
              "industry",
              "qualification",
              "owner",
              "added",
              "source"
            ]
          },
          "direction": {
            "description": "Order direction; read as `asc` once a `sort` field is named.",
            "type": "string",
            "enum": [
              "asc",
              "desc"
            ]
          },
          "qualificationStatus": {
            "description": "Keep members whose ICP verdict is ANY OF these: qualified, disqualified, unevaluated. `unevaluated` means nobody has judged them — never that they failed.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "ownerId": {
            "description": "Keep members owned by ANY OF these user ids. Ownership rides the entity, not the list.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "tagId": {
            "description": "Keep members carrying ANY OF these tag ids — an OR, and the counterpart to `tags` above, which is an AND over NAMES.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "addedAfter": {
            "description": "Keep members added to THIS list at or after this ISO timestamp.",
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "addedBefore": {
            "description": "Keep members added to THIS list strictly before this ISO timestamp.",
            "type": "string",
            "format": "date-time",
            "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z|([+-](?:[01]\\d|2[0-3]):[0-5]\\d)))$"
          },
          "hydrated": {
            "description": "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).",
            "type": "boolean"
          },
          "column": {
            "description": "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'.",
            "maxItems": 5,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 40,
                  "description": "The column name, as list_get reports it. A name this list lacks is refused."
                },
                "equals": {
                  "description": "Keep members whose WHOLE cell equals this, case-insensitively. The empty string asks for the cells nobody has filled in.",
                  "type": "string",
                  "maxLength": 2000
                },
                "contains": {
                  "description": "Keep members whose cell CONTAINS this, case-insensitively. Matched literally.",
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 2000
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "signalKey": {
            "description": "Leads only. Keep members that arrived from ANY OF these signals.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "crmStage": {
            "description": "Leads only. Keep members standing at ANY OF these pipeline stages.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "new",
                "invite_sent",
                "connected",
                "contacted",
                "replied",
                "interested",
                "meeting_booked",
                "won",
                "on_hold",
                "lost",
                "discarded"
              ]
            }
          },
          "emailState": {
            "description": "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.",
            "type": "string",
            "enum": [
              "has",
              "missing",
              "unasked"
            ]
          },
          "phoneState": {
            "description": "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).",
            "type": "string",
            "enum": [
              "has",
              "missing",
              "unasked"
            ]
          },
          "employeeMin": {
            "description": "Companies only. Keep companies with AT LEAST this many employees.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "employeeMax": {
            "description": "Companies only. Keep companies with AT MOST this many employees.",
            "type": "integer",
            "minimum": 0,
            "maximum": 9007199254740991
          },
          "industry": {
            "description": "Companies only. Keep companies carrying ANY OF these industries, matched case-insensitively against the whole industry list, not just the one shown as `subtitle`.",
            "minItems": 1,
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 200
            }
          },
          "hqCountry": {
            "description": "Companies only. Keep companies whose HQ is in ANY OF these ISO-2 country codes.",
            "minItems": 1,
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2
            }
          }
        },
        "required": [
          "asset",
          "id"
        ]
      }
    },
    {
      "name": "list_update",
      "title": "Rename a list, or set its custom columns",
      "description": "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.\n\nCustom 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.\n\n`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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "name",
          "type": "string",
          "required": true,
          "description": "The list's name. ALWAYS required here, even when you only mean to change `columns` — send the current name back to keep it."
        },
        {
          "name": "columns",
          "type": "object[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "audience",
          "type": "object | null",
          "required": false,
          "description": "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)."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "The list's name. ALWAYS required here, even when you only mean to change `columns` — send the current name back to keep it."
          },
          "columns": {
            "description": "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.",
            "maxItems": 100,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 40,
                  "description": "The column's name, as it appears on the list."
                },
                "renameFrom": {
                  "description": "The column this entry RENAMES. Carry it and every member's values travel to the new name; omit it and you have deleted one column and added an empty one.",
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 40
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "audience": {
            "description": "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).",
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "include": {
                    "type": "object",
                    "properties": {
                      "match": {
                        "type": "string",
                        "enum": [
                          "all",
                          "any"
                        ],
                        "description": "How this group's conditions join. `all` means a lead must satisfy EVERY condition; `any` means one is enough. Inside a single condition the fields always AND and the values in a field always OR, whatever `match` says — so `{ crmStage: ['on_hold','lost'], tagId: ['t1'] }` reads as (on_hold OR lost) AND t1."
                      },
                      "conditions": {
                        "minItems": 1,
                        "maxItems": 10,
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "crmStage": {
                              "description": "Keep leads standing at ANY of these Pipeline columns. `on_hold` is the parked pile — people a campaign ran out of things to say to, or whose conversation went quiet for two weeks. `new` is NOT 'people arriving': it is every lead nobody has worked yet, including every CSV row ever imported, so a rule over it can select tens of thousands.",
                              "minItems": 1,
                              "type": "array",
                              "items": {
                                "type": "string",
                                "enum": [
                                  "new",
                                  "invite_sent",
                                  "connected",
                                  "contacted",
                                  "replied",
                                  "interested",
                                  "meeting_booked",
                                  "won",
                                  "on_hold",
                                  "lost",
                                  "discarded"
                                ]
                              }
                            },
                            "tagId": {
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              ]
                            },
                            "ownerId": {
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              ]
                            },
                            "qualificationStatus": {
                              "description": "Keep leads whose ICP verdict is ANY of these. A lead the audience adds can still be qualified afterwards like any other member — `list_qualify` works on an audience list exactly as it does on a hand-made one.",
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              ]
                            },
                            "lastContacted": {
                              "description": "Keep leads last contacted inside this window — `7d`, `30d`, `90d`, `never`, or `YYYY-MM-DD..YYYY-MM-DD`. Counts BOTH a campaign send and a message in the chat. This is the field that makes an `exclude` group safe: `exclude` + `lastContacted: '30d'` is how you say 'nobody who heard from me this month'. Resolved against the clock at every evaluation, so a stored `30d` always means the 30 days before tonight's run, never the 30 days before the day you saved it.",
                              "type": "string"
                            },
                            "companyId": {
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              ]
                            },
                            "createdAfter": {
                              "description": "Keep only leads that ARRIVED in the org after this instant (ISO-8601, e.g. `2026-09-21T00:00:00.000Z`). This is what 'the people coming in' means: `crmStage: ['new']` on its own is the whole untouched backlog — every CSV row ever imported — which in the largest orgs is tens of thousands of people who have been sitting there for a year. Pair the two: `{ crmStage: ['new'], createdAfter: '<now>' }` is a flow for people who arrive from here on. An ABSOLUTE instant, not a rolling window, so the audience does not quietly forget the people it admitted last month.",
                              "type": "string",
                              "format": "date-time",
                              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
                            }
                          },
                          "additionalProperties": false
                        },
                        "description": "The conditions this group joins, 1 to 10. Each must name at least one field; an empty condition is refused rather than read as 'everyone'."
                      }
                    },
                    "required": [
                      "match",
                      "conditions"
                    ],
                    "description": "Who the rule selects. Required — a rule with only an exclude would select everyone and then take some away, which is never what an operator means."
                  },
                  "exclude": {
                    "description": "Who to take back OUT of that selection. Optional, and evaluated as `include AND NOT exclude`, so both apply at once. A lead the exclude cannot decide about — no owner on file when you exclude by owner, no employer when you exclude by company — is KEPT, not dropped.",
                    "type": "object",
                    "properties": {
                      "match": {
                        "type": "string",
                        "enum": [
                          "all",
                          "any"
                        ],
                        "description": "How this group's conditions join. `all` means a lead must satisfy EVERY condition; `any` means one is enough. Inside a single condition the fields always AND and the values in a field always OR, whatever `match` says — so `{ crmStage: ['on_hold','lost'], tagId: ['t1'] }` reads as (on_hold OR lost) AND t1."
                      },
                      "conditions": {
                        "minItems": 1,
                        "maxItems": 10,
                        "type": "array",
                        "items": {
                          "type": "object",
                          "properties": {
                            "crmStage": {
                              "description": "Keep leads standing at ANY of these Pipeline columns. `on_hold` is the parked pile — people a campaign ran out of things to say to, or whose conversation went quiet for two weeks. `new` is NOT 'people arriving': it is every lead nobody has worked yet, including every CSV row ever imported, so a rule over it can select tens of thousands.",
                              "minItems": 1,
                              "type": "array",
                              "items": {
                                "type": "string",
                                "enum": [
                                  "new",
                                  "invite_sent",
                                  "connected",
                                  "contacted",
                                  "replied",
                                  "interested",
                                  "meeting_booked",
                                  "won",
                                  "on_hold",
                                  "lost",
                                  "discarded"
                                ]
                              }
                            },
                            "tagId": {
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              ]
                            },
                            "ownerId": {
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              ]
                            },
                            "qualificationStatus": {
                              "description": "Keep leads whose ICP verdict is ANY of these. A lead the audience adds can still be qualified afterwards like any other member — `list_qualify` works on an audience list exactly as it does on a hand-made one.",
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              ]
                            },
                            "lastContacted": {
                              "description": "Keep leads last contacted inside this window — `7d`, `30d`, `90d`, `never`, or `YYYY-MM-DD..YYYY-MM-DD`. Counts BOTH a campaign send and a message in the chat. This is the field that makes an `exclude` group safe: `exclude` + `lastContacted: '30d'` is how you say 'nobody who heard from me this month'. Resolved against the clock at every evaluation, so a stored `30d` always means the 30 days before tonight's run, never the 30 days before the day you saved it.",
                              "type": "string"
                            },
                            "companyId": {
                              "anyOf": [
                                {
                                  "type": "string"
                                },
                                {
                                  "type": "array",
                                  "items": {
                                    "type": "string"
                                  }
                                }
                              ]
                            },
                            "createdAfter": {
                              "description": "Keep only leads that ARRIVED in the org after this instant (ISO-8601, e.g. `2026-09-21T00:00:00.000Z`). This is what 'the people coming in' means: `crmStage: ['new']` on its own is the whole untouched backlog — every CSV row ever imported — which in the largest orgs is tens of thousands of people who have been sitting there for a year. Pair the two: `{ crmStage: ['new'], createdAfter: '<now>' }` is a flow for people who arrive from here on. An ABSOLUTE instant, not a rolling window, so the audience does not quietly forget the people it admitted last month.",
                              "type": "string",
                              "format": "date-time",
                              "pattern": "^(?:(?:\\d\\d[2468][048]|\\d\\d[13579][26]|\\d\\d0[48]|[02468][048]00|[13579][26]00)-02-29|\\d{4}-(?:(?:0[13578]|1[02])-(?:0[1-9]|[12]\\d|3[01])|(?:0[469]|11)-(?:0[1-9]|[12]\\d|30)|(?:02)-(?:0[1-9]|1\\d|2[0-8])))T(?:(?:[01]\\d|2[0-3]):[0-5]\\d(?::[0-5]\\d(?:\\.\\d+)?)?(?:Z))$"
                            }
                          },
                          "additionalProperties": false
                        },
                        "description": "The conditions this group joins, 1 to 10. Each must name at least one field; an empty condition is refused rather than read as 'everyone'."
                      }
                    },
                    "required": [
                      "match",
                      "conditions"
                    ]
                  }
                },
                "required": [
                  "include"
                ]
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "asset",
          "id",
          "name"
        ]
      }
    },
    {
      "name": "newsletter_subscribers",
      "title": "List a newsletter’s subscribers",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "newsletter",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false,
          "description": "Continue from a previous page. Omit for the first page."
        },
        {
          "name": "count",
          "type": "integer",
          "required": false,
          "description": "Subscribers per page (default 50, max 100). LinkedIn refuses anything higher — asking for 250 fails the whole call, it does not clamp."
        },
        {
          "name": "intoList",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "exhaust",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "maxResults",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "newsletter": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "description": "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": {
            "description": "Continue from a previous page. Omit for the first page.",
            "type": "string",
            "maxLength": 65536
          },
          "count": {
            "description": "Subscribers per page (default 50, max 100). LinkedIn refuses anything higher — asking for 250 fails the whole call, it does not clamp.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "intoList": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "exhaust": {
            "description": "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.",
            "type": "boolean"
          },
          "maxResults": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "newsletter"
        ]
      }
    },
    {
      "name": "owner_set",
      "title": "Assign an owner to leads or companies",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of row these items are — people or companies."
        },
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "The rows to assign, each naming its subject by ONE of `memberId`, `url`, or `providerId`."
        },
        {
          "name": "owner",
          "type": "object | null",
          "required": true,
          "description": "Who to assign every item to — one org member, by `userId` or `email`. The same owner is set on all of them."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of row these items are — people or companies."
          },
          "items": {
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "memberId": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "required": [
                    "memberId"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2048,
                      "description": "The person's LinkedIn profile URL, used when you do not have their `memberId`. Name each item exactly one way."
                    }
                  },
                  "required": [
                    "url"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "providerId": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 256
                    }
                  },
                  "required": [
                    "providerId"
                  ]
                }
              ]
            },
            "description": "The rows to assign, each naming its subject by ONE of `memberId`, `url`, or `providerId`."
          },
          "owner": {
            "anyOf": [
              {
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 320,
                        "description": "The owner's email, when you do not have their `userId`. Must be a member of this org."
                      }
                    },
                    "required": [
                      "email"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "userId": {
                        "type": "string",
                        "minLength": 1
                      }
                    },
                    "required": [
                      "userId"
                    ]
                  }
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "Who to assign every item to — one org member, by `userId` or `email`. The same owner is set on all of them."
          }
        },
        "required": [
          "asset",
          "items",
          "owner"
        ]
      }
    },
    {
      "name": "pipeline_board",
      "title": "Read the pipeline board",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "stage",
          "type": "\"new\" | \"invite_sent\" | \"connected\" | \"contacted\" | \"replied\" | \"interested\" | \"meeting_booked\" | \"won\" | \"on_hold\" | \"lost\" | \"discarded\"",
          "required": false,
          "description": "Return this column's people as well as the counts. Omit for counts only, which is what \"how is the pipeline doing\" needs."
        },
        {
          "name": "lastContacted",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false,
          "description": "Resume the named column from a previous call — pass the `cursor` it returned."
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "description": "How many people to return when `stage` is named.",
          "default": "25"
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "stage": {
            "description": "Return this column's people as well as the counts. Omit for counts only, which is what \"how is the pipeline doing\" needs.",
            "type": "string",
            "enum": [
              "new",
              "invite_sent",
              "connected",
              "contacted",
              "replied",
              "interested",
              "meeting_booked",
              "won",
              "on_hold",
              "lost",
              "discarded"
            ]
          },
          "lastContacted": {
            "description": "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.",
            "type": "string"
          },
          "cursor": {
            "description": "Resume the named column from a previous call — pass the `cursor` it returned.",
            "type": "string",
            "maxLength": 65536
          },
          "limit": {
            "default": 25,
            "description": "How many people to return when `stage` is named.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        }
      }
    },
    {
      "name": "pipeline_stage_set",
      "title": "Move people on the pipeline board",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "The people to move, each naming its subject by ONE of `memberId`, `url`, or `providerId`."
        },
        {
          "name": "stage",
          "type": "\"new\" | \"invite_sent\" | \"connected\" | \"contacted\" | \"replied\" | \"interested\" | \"meeting_booked\" | \"won\" | \"on_hold\" | \"lost\" | \"discarded\"",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "items": {
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "memberId": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "required": [
                    "memberId"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2048,
                      "description": "The person's LinkedIn profile URL, used when you do not have their `memberId`. Name each item exactly one way."
                    }
                  },
                  "required": [
                    "url"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "providerId": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 256
                    }
                  },
                  "required": [
                    "providerId"
                  ]
                }
              ]
            },
            "description": "The people to move, each naming its subject by ONE of `memberId`, `url`, or `providerId`."
          },
          "stage": {
            "type": "string",
            "enum": [
              "new",
              "invite_sent",
              "connected",
              "contacted",
              "replied",
              "interested",
              "meeting_booked",
              "won",
              "on_hold",
              "lost",
              "discarded"
            ],
            "description": "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."
          }
        },
        "required": [
          "items",
          "stage"
        ]
      }
    },
    {
      "name": "post_interactions",
      "title": "Who engaged with one post",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "url",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "intoList",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        },
        {
          "name": "exhaust",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "maxResults",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048,
            "description": "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": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          },
          "exhaust": {
            "description": "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.",
            "type": "boolean"
          },
          "maxResults": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "url"
        ]
      }
    },
    {
      "name": "profile_comments",
      "title": "The comments a person has written",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "target",
          "type": "object",
          "required": true,
          "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "target": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "url",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "A LinkedIn profile or company URL."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "public_identifier",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The vanity slug (e.g. \"jane-doe\") or company universal name."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "provider_id",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "Unipile/LinkedIn internal id (e.g. \"ACoAAB…\" or the Sales-Nav \"ACwAAB…\")."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              }
            ],
            "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          }
        },
        "required": [
          "target"
        ]
      }
    },
    {
      "name": "profile_posts",
      "title": "A profile's recent posts",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "target",
          "type": "object",
          "required": true,
          "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "target": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "url",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "A LinkedIn profile or company URL."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "public_identifier",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The vanity slug (e.g. \"jane-doe\") or company universal name."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "provider_id",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "Unipile/LinkedIn internal id (e.g. \"ACoAAB…\" or the Sales-Nav \"ACwAAB…\")."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              }
            ],
            "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          }
        },
        "required": [
          "target"
        ]
      }
    },
    {
      "name": "profile_reactions",
      "title": "The posts a person has reacted to",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "target",
          "type": "object",
          "required": true,
          "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "target": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "url",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "A LinkedIn profile or company URL."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "public_identifier",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The vanity slug (e.g. \"jane-doe\") or company universal name."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "provider_id",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "Unipile/LinkedIn internal id (e.g. \"ACoAAB…\" or the Sales-Nav \"ACwAAB…\")."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              }
            ],
            "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          }
        },
        "required": [
          "target"
        ]
      }
    },
    {
      "name": "qualification_get",
      "title": "Read one member's qualification verdict",
      "description": "WHY one lead or company carries the verdict it does (`{ asset, id }`) — the stored scorecard. Ids come from list_members. Spends nothing, changes nothing.\n\nReturns `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.\n\nOn 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.\n\n`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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of list: 'leads' (people) or 'companies'."
        },
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The member's id, exactly as `list_members` returned it — a lead id under `asset: 'leads'`, a company id under `asset: 'companies'`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of list: 'leads' (people) or 'companies'."
          },
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The member's id, exactly as `list_members` returned it — a lead id under `asset: 'leads'`, a company id under `asset: 'companies'`."
          }
        },
        "required": [
          "asset",
          "id"
        ]
      }
    },
    {
      "name": "qualification_set",
      "title": "Decide about people or companies yourself",
      "description": "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`.\n\nYour 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.\n\nUse 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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": false,
          "description": "Who you are deciding about: 'leads' (people, the default) or 'companies'. Companies are named by `memberId` (from `list_members`), `url` or `providerId`, like people.",
          "default": "\"leads\""
        },
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "The people (or companies) to decide about, each naming its subject by ONE of `memberId`, `url`, or `providerId`."
        },
        {
          "name": "qualification",
          "type": "\"qualified\" | \"disqualified\" | null",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "default": "leads",
            "description": "Who you are deciding about: 'leads' (people, the default) or 'companies'. Companies are named by `memberId` (from `list_members`), `url` or `providerId`, like people.",
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ]
          },
          "items": {
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "memberId": {
                      "type": "string",
                      "minLength": 1
                    }
                  },
                  "required": [
                    "memberId"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2048,
                      "description": "The person's LinkedIn profile URL, used when you do not have their `memberId`. Name each item exactly one way."
                    }
                  },
                  "required": [
                    "url"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "providerId": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 256
                    }
                  },
                  "required": [
                    "providerId"
                  ]
                }
              ]
            },
            "description": "The people (or companies) to decide about, each naming its subject by ONE of `memberId`, `url`, or `providerId`."
          },
          "qualification": {
            "anyOf": [
              {
                "type": "string",
                "enum": [
                  "qualified",
                  "disqualified"
                ]
              },
              {
                "type": "null"
              }
            ],
            "description": "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."
          }
        },
        "required": [
          "items",
          "qualification"
        ]
      }
    },
    {
      "name": "recruiter_saved_searches",
      "title": "List Recruiter saved searches",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "keywords",
          "type": "string",
          "required": false,
          "description": "Narrow the list by name. Omit to list everything the seat has saved."
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "description": "How many to return (default 10, max 100)."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "keywords": {
            "description": "Narrow the list by name. Omit to list everything the seat has saved.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "limit": {
            "description": "How many to return (default 10, max 100).",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        }
      }
    },
    {
      "name": "search_people",
      "title": "Search people (classic)",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "url",
          "type": "string",
          "required": false
        },
        {
          "name": "keywords",
          "type": "string",
          "required": false
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "filters",
          "type": "object[]",
          "required": false
        },
        {
          "name": "networkDistance",
          "type": "1 | 2 | 3[]",
          "required": false,
          "description": "Connection degree: 1 (1st), 2 (2nd), 3 (3rd+). LinkedIn native filter CONNECTIONS."
        },
        {
          "name": "profileLanguage",
          "type": "string[]",
          "required": false,
          "description": "Profile languages as 2-letter ISO-639-1 codes (e.g. [\"en\",\"es\"])."
        },
        {
          "name": "openTo",
          "type": "\"proBono\" | \"boardMember\"[]",
          "required": false
        },
        {
          "name": "connectionsOf",
          "type": "string[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "followersOf",
          "type": "string[]",
          "required": false,
          "description": "LinkedIn provider ids (`ACoAA…`, from people.suggest — NOT numeric member ids) whose followers to search within."
        },
        {
          "name": "advancedKeywords",
          "type": "object",
          "required": false
        },
        {
          "name": "intoList",
          "type": "string",
          "required": false
        },
        {
          "name": "exhaust",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "maxResults",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "minLength": 1,
            "maxLength": 2048
          },
          "keywords": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4096
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          },
          "limit": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "filters": {
            "maxItems": 40,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "location",
                    "industry",
                    "company",
                    "past_company",
                    "school",
                    "service"
                  ],
                  "description": "Which id-resolved dimension: location, industry, company, past_company, school, service."
                },
                "query": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256,
                  "description": "The value as a human-readable NAME (never an id). Phrase it the way LinkedIn names things — canonical industry names (\"Financial Services\", not \"fintech\"), real place names (\"United States\", \"Greater Madrid Metropolitan Area\"). The best single typeahead match is resolved and its label echoed back."
                }
              },
              "required": [
                "type",
                "query"
              ]
            }
          },
          "networkDistance": {
            "description": "Connection degree: 1 (1st), 2 (2nd), 3 (3rd+). LinkedIn native filter CONNECTIONS.",
            "minItems": 1,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "number",
                  "const": 1
                },
                {
                  "type": "number",
                  "const": 2
                },
                {
                  "type": "number",
                  "const": 3
                }
              ]
            }
          },
          "profileLanguage": {
            "description": "Profile languages as 2-letter ISO-639-1 codes (e.g. [\"en\",\"es\"]).",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2
            }
          },
          "openTo": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "proBono",
                "boardMember"
              ]
            }
          },
          "connectionsOf": {
            "description": "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.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            }
          },
          "followersOf": {
            "description": "LinkedIn provider ids (`ACoAA…`, from people.suggest — NOT numeric member ids) whose followers to search within.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            }
          },
          "advancedKeywords": {
            "type": "object",
            "properties": {
              "firstName": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128
              },
              "lastName": {
                "type": "string",
                "minLength": 1,
                "maxLength": 128
              },
              "title": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256
              },
              "company": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256
              },
              "school": {
                "type": "string",
                "minLength": 1,
                "maxLength": 256
              }
            }
          },
          "intoList": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "exhaust": {
            "description": "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.",
            "type": "boolean"
          },
          "maxResults": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        }
      }
    },
    {
      "name": "search_posts",
      "title": "Search LinkedIn posts",
      "description": "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`.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "datePosted",
          "type": "\"past-24h\" | \"past-week\" | \"past-month\"",
          "required": false,
          "description": "One of LinkedIn's own recency windows."
        },
        {
          "name": "sortBy",
          "type": "\"relevance\" | \"date\"",
          "required": false,
          "description": "Result order. Defaults to relevance."
        },
        {
          "name": "contentType",
          "type": "\"videos\" | \"images\" | \"live_videos\" | \"collaborative_articles\" | \"documents\"",
          "required": false,
          "description": "Only posts carrying this kind of attachment."
        },
        {
          "name": "firstDegreeOnly",
          "type": "boolean",
          "required": false,
          "description": "Only posts by the seat's 1st-degree connections."
        },
        {
          "name": "postedByMe",
          "type": "boolean",
          "required": false,
          "description": "Only the seat's own posts."
        },
        {
          "name": "postedByFollowed",
          "type": "boolean",
          "required": false,
          "description": "Only posts by people the seat follows."
        },
        {
          "name": "fromMember",
          "type": "string[]",
          "required": false,
          "description": "Author NAMES — only posts written by these people."
        },
        {
          "name": "fromCompany",
          "type": "string[]",
          "required": false,
          "description": "Company NAMES — only posts published by these pages."
        },
        {
          "name": "mentioningMember",
          "type": "string[]",
          "required": false,
          "description": "Only posts that @-mention these people, by NAME."
        },
        {
          "name": "mentioningCompany",
          "type": "string[]",
          "required": false,
          "description": "Only posts that @-mention these companies, by NAME."
        },
        {
          "name": "authorIndustry",
          "type": "string[]",
          "required": false,
          "description": "Industry NAMES — only posts whose AUTHOR works in one of them."
        },
        {
          "name": "authorCompany",
          "type": "string[]",
          "required": false,
          "description": "Company NAMES — only posts whose AUTHOR works at one of them."
        },
        {
          "name": "authorKeywords",
          "type": "string",
          "required": false,
          "description": "Free text matched against the AUTHOR's profile, e.g. a job title."
        },
        {
          "name": "keywords",
          "type": "string",
          "required": true,
          "description": "Text matched against the post body. Boolean OR/AND/NOT allowed."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "datePosted": {
            "description": "One of LinkedIn's own recency windows.",
            "type": "string",
            "enum": [
              "past-24h",
              "past-week",
              "past-month"
            ]
          },
          "sortBy": {
            "description": "Result order. Defaults to relevance.",
            "type": "string",
            "enum": [
              "relevance",
              "date"
            ]
          },
          "contentType": {
            "description": "Only posts carrying this kind of attachment.",
            "type": "string",
            "enum": [
              "videos",
              "images",
              "live_videos",
              "collaborative_articles",
              "documents"
            ]
          },
          "firstDegreeOnly": {
            "description": "Only posts by the seat's 1st-degree connections.",
            "type": "boolean"
          },
          "postedByMe": {
            "description": "Only the seat's own posts.",
            "type": "boolean"
          },
          "postedByFollowed": {
            "description": "Only posts by people the seat follows.",
            "type": "boolean"
          },
          "fromMember": {
            "description": "Author NAMES — only posts written by these people.",
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "fromCompany": {
            "description": "Company NAMES — only posts published by these pages.",
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "mentioningMember": {
            "description": "Only posts that @-mention these people, by NAME.",
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "mentioningCompany": {
            "description": "Only posts that @-mention these companies, by NAME.",
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "authorIndustry": {
            "description": "Industry NAMES — only posts whose AUTHOR works in one of them.",
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "authorCompany": {
            "description": "Company NAMES — only posts whose AUTHOR works at one of them.",
            "minItems": 1,
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1,
              "maxLength": 256
            }
          },
          "authorKeywords": {
            "description": "Free text matched against the AUTHOR's profile, e.g. a job title.",
            "type": "string",
            "minLength": 1,
            "maxLength": 256
          },
          "keywords": {
            "type": "string",
            "minLength": 1,
            "maxLength": 4096,
            "description": "Text matched against the post body. Boolean OR/AND/NOT allowed."
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          }
        },
        "required": [
          "keywords"
        ]
      }
    },
    {
      "name": "search_recruiter",
      "title": "Search (LinkedIn Recruiter)",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "url",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "keywords",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "description": "Page size. Recruiter allows up to 100."
        },
        {
          "name": "intoList",
          "type": "string",
          "required": false,
          "description": "Where this page's people are appended — always a LEADS list, because Recruiter answers with people and nothing else."
        },
        {
          "name": "exhaust",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "maxResults",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "url": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 2048
          },
          "keywords": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 500
          },
          "cursor": {
            "type": "string",
            "minLength": 1
          },
          "limit": {
            "description": "Page size. Recruiter allows up to 100.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "intoList": {
            "description": "Where this page's people are appended — always a LEADS list, because Recruiter answers with people and nothing else.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "exhaust": {
            "description": "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.",
            "type": "boolean"
          },
          "maxResults": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        }
      }
    },
    {
      "name": "search_sales_navigator",
      "title": "Search (Sales Navigator)",
      "description": "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`).",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "category",
          "type": "\"people\" | \"companies\"",
          "required": false,
          "description": "Search people (default) or companies. Some fields are valid for only one category.",
          "default": "\"people\""
        },
        {
          "name": "keywords",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "filters",
          "type": "object[]",
          "required": false,
          "description": "Id-resolved dimensions (location, industry, company, …). Same dimension repeated = OR; different dimensions = AND (each narrows the results)."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "firstName",
          "type": "string",
          "required": false
        },
        {
          "name": "lastName",
          "type": "string",
          "required": false
        },
        {
          "name": "role",
          "type": "object",
          "required": false,
          "description": "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."
        },
        {
          "name": "seniority",
          "type": "object",
          "required": false,
          "description": "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\"."
        },
        {
          "name": "tenure",
          "type": "object[]",
          "required": false,
          "description": "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}."
        },
        {
          "name": "tenureAtCompany",
          "type": "object[]",
          "required": false,
          "description": "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}."
        },
        {
          "name": "tenureAtRole",
          "type": "object[]",
          "required": false,
          "description": "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}."
        },
        {
          "name": "companyHeadcount",
          "type": "\"1-10\" | \"11-50\" | \"51-200\" | \"201-500\" | \"501-1000\" | \"1001-5000\" | \"5001-10000\" | \"10001+\"[]",
          "required": false,
          "description": "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\"])."
        },
        {
          "name": "companyType",
          "type": "\"public_company\" | \"privately_held\" | \"non_profit\" | \"educational_institution\" | \"partnership\" | \"self_employed\" | \"self_owned\" | \"government_agency\"[]",
          "required": false,
          "description": "Ownership type of the person's current employer (people search; e.g. privately_held, public_company). A companies search has no company-type facet."
        },
        {
          "name": "profileLanguage",
          "type": "string[]",
          "required": false,
          "description": "Profile languages as 2-letter ISO codes (e.g. [\"en\",\"es\"])."
        },
        {
          "name": "networkDistance",
          "type": "1 | 2 | 3 | \"GROUP\"[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "connectionsOf",
          "type": "string[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "changedJobs",
          "type": "boolean",
          "required": false,
          "description": "Spotlight: only people who recently changed jobs."
        },
        {
          "name": "postedOnLinkedin",
          "type": "boolean",
          "required": false
        },
        {
          "name": "mentionedInNews",
          "type": "boolean",
          "required": false
        },
        {
          "name": "followingYourCompany",
          "type": "boolean",
          "required": false
        },
        {
          "name": "pastColleague",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "sharedExperiences",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "viewedYourProfileRecently",
          "type": "boolean",
          "required": false
        },
        {
          "name": "viewedProfileRecently",
          "type": "boolean",
          "required": false
        },
        {
          "name": "messagedRecently",
          "type": "boolean",
          "required": false
        },
        {
          "name": "includeSavedLeads",
          "type": "boolean",
          "required": false
        },
        {
          "name": "includeSavedAccounts",
          "type": "boolean",
          "required": false
        },
        {
          "name": "hasJobOffers",
          "type": "boolean",
          "required": false,
          "description": "Company search: only companies currently hiring."
        },
        {
          "name": "headcount",
          "type": "\"1-10\" | \"11-50\" | \"51-200\" | \"201-500\" | \"501-1000\" | \"1001-5000\" | \"5001-10000\" | \"10001+\"[]",
          "required": false,
          "description": "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\"])."
        },
        {
          "name": "headcountGrowth",
          "type": "object",
          "required": false,
          "description": "Company headcount growth rate as a {min,max} percentage range."
        },
        {
          "name": "annualRevenue",
          "type": "object",
          "required": false,
          "description": "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."
        },
        {
          "name": "followersCount",
          "type": "object[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "fortune",
          "type": "object[]",
          "required": false,
          "description": "Fortune ranking {min,max} ranges (e.g. Fortune 500). Breakpoints — min: 0, 51, 101, 251; max: 50, 100, 250, 500."
        },
        {
          "name": "recentActivities",
          "type": "\"senior_leadership_changes\" | \"funding_events\"[]",
          "required": false,
          "description": "Company signals: senior_leadership_changes and/or funding_events."
        },
        {
          "name": "departmentHeadcount",
          "type": "object",
          "required": false
        },
        {
          "name": "departmentHeadcountGrowth",
          "type": "object",
          "required": false
        },
        {
          "name": "savedAccounts",
          "type": "string[]",
          "required": false
        },
        {
          "name": "savedSearchId",
          "type": "string",
          "required": false
        },
        {
          "name": "recentSearchId",
          "type": "string",
          "required": false
        },
        {
          "name": "lastViewedAt",
          "type": "number",
          "required": false
        },
        {
          "name": "withinArea",
          "type": "number",
          "required": false,
          "description": "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."
        },
        {
          "name": "icpId",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "withoutIcp",
          "type": "object",
          "required": false,
          "description": "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."
        },
        {
          "name": "intoList",
          "type": "string",
          "required": false
        },
        {
          "name": "exhaust",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "maxResults",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "companyLists",
          "type": "object[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "category": {
            "default": "people",
            "description": "Search people (default) or companies. Some fields are valid for only one category.",
            "type": "string",
            "enum": [
              "people",
              "companies"
            ]
          },
          "keywords": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 2000
          },
          "filters": {
            "description": "Id-resolved dimensions (location, industry, company, …). Same dimension repeated = OR; different dimensions = AND (each narrows the results).",
            "maxItems": 40,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "type": {
                  "type": "string",
                  "enum": [
                    "location",
                    "industry",
                    "function",
                    "current_company",
                    "past_company",
                    "company_location",
                    "school",
                    "past_role",
                    "groups",
                    "persona",
                    "technologies",
                    "account_lists",
                    "lead_lists",
                    "postal_code"
                  ],
                  "description": "Which id-resolved dimension. People: location, industry, function, current_company, past_company, company_location, school, past_role, groups, persona, account_lists, lead_lists, postal_code. Companies: location, industry, technologies, account_lists, postal_code — the rest are people-only and are REFUSED here rather than quietly ignored. On a companies search the company IS the subject, so its location is plain `location` (`company_location` is accepted as a synonym for it, and means the EMPLOYER’s location only on a people search). `groups` is measured from YOU, like the saved lists: LinkedIn only knows the groups your own account has JOINED, so it works only on your OWN Sales Navigator seat and is otherwise refused, rather than answered about a different audience. To reach the people in a group, use `group_members` instead — it reads the group’s own member list on your own seat: any public group, or a members-only one your seat has joined."
                },
                "query": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256,
                  "description": "The value as a human-readable NAME (never an id). Phrase it the way LinkedIn names things — canonical industry names (\"Financial Services\", not \"fintech\"), real place names (\"Greater Madrid Metropolitan Area\" or the country). A `postal_code` is the FULL code as the country writes it — Argentina's \"C1425ECB\", not \"1425\", which resolves to a code abroad; check the echoed label names the right country. Resolution takes the best single typeahead match and echoes the resolved label back."
                },
                "mode": {
                  "description": "\"include\" (default) requires the value; \"exclude\" filters it out.",
                  "type": "string",
                  "enum": [
                    "include",
                    "exclude"
                  ]
                }
              },
              "required": [
                "type",
                "query"
              ]
            }
          },
          "cursor": {
            "description": "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.",
            "type": "string",
            "maxLength": 65536
          },
          "limit": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "firstName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "lastName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "role": {
            "description": "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.",
            "type": "object",
            "properties": {
              "include": {
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1
                }
              },
              "exclude": {
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1
                }
              }
            }
          },
          "seniority": {
            "description": "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\".",
            "type": "object",
            "properties": {
              "include": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "owner/partner",
                    "cxo",
                    "vice_president",
                    "director",
                    "experienced_manager",
                    "entry_level_manager",
                    "strategic",
                    "senior",
                    "entry_level",
                    "in_training"
                  ]
                }
              },
              "exclude": {
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "owner/partner",
                    "cxo",
                    "vice_president",
                    "director",
                    "experienced_manager",
                    "entry_level_manager",
                    "strategic",
                    "senior",
                    "entry_level",
                    "in_training"
                  ]
                }
              }
            }
          },
          "tenure": {
            "description": "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}.",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "min": {
                  "type": "number"
                },
                "max": {
                  "type": "number"
                }
              }
            }
          },
          "tenureAtCompany": {
            "description": "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}.",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "min": {
                  "type": "number"
                },
                "max": {
                  "type": "number"
                }
              }
            }
          },
          "tenureAtRole": {
            "description": "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}.",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "min": {
                  "type": "number"
                },
                "max": {
                  "type": "number"
                }
              }
            }
          },
          "companyHeadcount": {
            "description": "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\"]).",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "1-10",
                "11-50",
                "51-200",
                "201-500",
                "501-1000",
                "1001-5000",
                "5001-10000",
                "10001+"
              ]
            }
          },
          "companyType": {
            "description": "Ownership type of the person's current employer (people search; e.g. privately_held, public_company). A companies search has no company-type facet.",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "public_company",
                "privately_held",
                "non_profit",
                "educational_institution",
                "partnership",
                "self_employed",
                "self_owned",
                "government_agency"
              ]
            }
          },
          "profileLanguage": {
            "description": "Profile languages as 2-letter ISO codes (e.g. [\"en\",\"es\"]).",
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 2,
              "maxLength": 2
            }
          },
          "networkDistance": {
            "description": "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.",
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "number",
                  "const": 1
                },
                {
                  "type": "number",
                  "const": 2
                },
                {
                  "type": "number",
                  "const": 3
                },
                {
                  "type": "string",
                  "const": "GROUP"
                }
              ]
            }
          },
          "connectionsOf": {
            "description": "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.",
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "changedJobs": {
            "description": "Spotlight: only people who recently changed jobs.",
            "type": "boolean"
          },
          "postedOnLinkedin": {
            "type": "boolean"
          },
          "mentionedInNews": {
            "type": "boolean"
          },
          "followingYourCompany": {
            "type": "boolean"
          },
          "pastColleague": {
            "description": "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.",
            "type": "boolean"
          },
          "sharedExperiences": {
            "description": "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.",
            "type": "boolean"
          },
          "viewedYourProfileRecently": {
            "type": "boolean"
          },
          "viewedProfileRecently": {
            "type": "boolean"
          },
          "messagedRecently": {
            "type": "boolean"
          },
          "includeSavedLeads": {
            "type": "boolean"
          },
          "includeSavedAccounts": {
            "type": "boolean"
          },
          "hasJobOffers": {
            "description": "Company search: only companies currently hiring.",
            "type": "boolean"
          },
          "headcount": {
            "description": "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\"]).",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "1-10",
                "11-50",
                "51-200",
                "201-500",
                "501-1000",
                "1001-5000",
                "5001-10000",
                "10001+"
              ]
            }
          },
          "headcountGrowth": {
            "description": "Company headcount growth rate as a {min,max} percentage range.",
            "type": "object",
            "properties": {
              "min": {
                "type": "number"
              },
              "max": {
                "type": "number"
              }
            }
          },
          "annualRevenue": {
            "description": "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.",
            "type": "object",
            "properties": {
              "currency": {
                "type": "string",
                "minLength": 3,
                "maxLength": 3
              },
              "min": {
                "type": "number"
              },
              "max": {
                "type": "number"
              }
            },
            "required": [
              "currency",
              "min",
              "max"
            ]
          },
          "followersCount": {
            "description": "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.",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "min": {
                  "type": "number"
                },
                "max": {
                  "type": "number"
                }
              }
            }
          },
          "fortune": {
            "description": "Fortune ranking {min,max} ranges (e.g. Fortune 500). Breakpoints — min: 0, 51, 101, 251; max: 50, 100, 250, 500.",
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "min": {
                  "type": "number"
                },
                "max": {
                  "type": "number"
                }
              }
            }
          },
          "recentActivities": {
            "description": "Company signals: senior_leadership_changes and/or funding_events.",
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "senior_leadership_changes",
                "funding_events"
              ]
            }
          },
          "departmentHeadcount": {
            "type": "object",
            "properties": {
              "department": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "min": {
                "type": "number"
              },
              "max": {
                "type": "number"
              }
            },
            "required": [
              "department"
            ]
          },
          "departmentHeadcountGrowth": {
            "type": "object",
            "properties": {
              "department": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "min": {
                "type": "number"
              },
              "max": {
                "type": "number"
              }
            },
            "required": [
              "department"
            ]
          },
          "savedAccounts": {
            "type": "array",
            "items": {
              "type": "string"
            }
          },
          "savedSearchId": {
            "type": "string",
            "pattern": "^\\d+$"
          },
          "recentSearchId": {
            "type": "string",
            "pattern": "^\\d+$"
          },
          "lastViewedAt": {
            "type": "number"
          },
          "withinArea": {
            "description": "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.",
            "type": "number",
            "minimum": 0
          },
          "icpId": {
            "description": "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.",
            "type": "string",
            "minLength": 1
          },
          "withoutIcp": {
            "description": "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.",
            "type": "object",
            "properties": {
              "reason": {
                "type": "string",
                "enum": [
                  "creating_icp",
                  "editing_icp",
                  "exploring",
                  "named_accounts"
                ],
                "description": "`creating_icp`: the org has no ICP for this yet. `editing_icp`: you are adjusting an existing ICP. `exploring`: you are finding out which titles, keywords or markets exist before deciding, and no ICP fits this audience yet. `named_accounts`: you are finding the people at companies the operator named — the search must include a `current_company` filter or `companyLists`."
              },
              "note": {
                "type": "string",
                "minLength": 1,
                "maxLength": 280,
                "description": "One sentence on why no ICP applies to this search."
              }
            },
            "required": [
              "reason",
              "note"
            ]
          },
          "intoList": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "exhaust": {
            "description": "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.",
            "type": "boolean"
          },
          "maxResults": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "companyLists": {
            "description": "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.",
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "minLength": 1,
                  "description": "A Kairon company list id (from `list_list`)."
                },
                "mode": {
                  "description": "\"include\" (default) = people AT these companies; \"exclude\" = people NOT at them.",
                  "type": "string",
                  "enum": [
                    "include",
                    "exclude"
                  ]
                }
              },
              "required": [
                "id"
              ]
            }
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        }
      }
    },
    {
      "name": "signal_search",
      "title": "Search by behavioural signal",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "signal",
          "type": "\"posted_with_keyword\" | \"engaged_with_keyword\" | \"engaged_with_profile\" | \"recently_connected\" | \"viewed_my_profile\"",
          "required": true,
          "description": "Which signal to run. It decides which `config` fields are read — a signal that takes no parameters ignores `config` entirely."
        },
        {
          "name": "config",
          "type": "object",
          "required": false,
          "description": "Parameters for the chosen `signal`. Fields the signal does not use are ignored."
        },
        {
          "name": "intoList",
          "type": "string",
          "required": false,
          "description": "Where this page's PEOPLE are appended — always a leads list, whichever signal ran, because every searchable signal answers with people. Only the person is added; the post and the engagement are why they are here, not members."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        },
        {
          "name": "exhaust",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "maxResults",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "signal": {
            "type": "string",
            "enum": [
              "posted_with_keyword",
              "engaged_with_keyword",
              "engaged_with_profile",
              "recently_connected",
              "viewed_my_profile"
            ],
            "description": "Which signal to run. It decides which `config` fields are read — a signal that takes no parameters ignores `config` entirely."
          },
          "config": {
            "description": "Parameters for the chosen `signal`. Fields the signal does not use are ignored.",
            "type": "object",
            "properties": {
              "datePosted": {
                "description": "One of LinkedIn's own recency windows.",
                "type": "string",
                "enum": [
                  "past-24h",
                  "past-week",
                  "past-month"
                ]
              },
              "sortBy": {
                "description": "Result order. Defaults to relevance.",
                "type": "string",
                "enum": [
                  "relevance",
                  "date"
                ]
              },
              "contentType": {
                "description": "Only posts carrying this kind of attachment.",
                "type": "string",
                "enum": [
                  "videos",
                  "images",
                  "live_videos",
                  "collaborative_articles",
                  "documents"
                ]
              },
              "firstDegreeOnly": {
                "description": "Only posts by the seat's 1st-degree connections.",
                "type": "boolean"
              },
              "postedByMe": {
                "description": "Only the seat's own posts.",
                "type": "boolean"
              },
              "postedByFollowed": {
                "description": "Only posts by people the seat follows.",
                "type": "boolean"
              },
              "fromMember": {
                "description": "Author NAMES — only posts written by these people.",
                "minItems": 1,
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                }
              },
              "fromCompany": {
                "description": "Company NAMES — only posts published by these pages.",
                "minItems": 1,
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                }
              },
              "mentioningMember": {
                "description": "Only posts that @-mention these people, by NAME.",
                "minItems": 1,
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                }
              },
              "mentioningCompany": {
                "description": "Only posts that @-mention these companies, by NAME.",
                "minItems": 1,
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                }
              },
              "authorIndustry": {
                "description": "Industry NAMES — only posts whose AUTHOR works in one of them.",
                "minItems": 1,
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                }
              },
              "authorCompany": {
                "description": "Company NAMES — only posts whose AUTHOR works at one of them.",
                "minItems": 1,
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 256
                }
              },
              "authorKeywords": {
                "description": "Free text matched against the AUTHOR's profile, e.g. a job title.",
                "type": "string",
                "minLength": 1,
                "maxLength": 256
              },
              "keyword": {
                "description": "A full LinkedIn boolean query for the keyword signals — `OR` / `AND` / `NOT`, quoted phrases and parentheses all run verbatim.",
                "type": "string",
                "minLength": 1,
                "maxLength": 4096
              },
              "target": {
                "description": "The profile whose engagers the `engaged_with_profile` signal harvests.",
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "person",
                      "company"
                    ],
                    "description": "Whether `url` points at a person or a company."
                  },
                  "url": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The LinkedIn URL of that profile or page."
                  }
                },
                "required": [
                  "kind",
                  "url"
                ]
              }
            }
          },
          "intoList": {
            "description": "Where this page's PEOPLE are appended — always a leads list, whichever signal ran, because every searchable signal answers with people. Only the person is added; the post and the engagement are why they are here, not members.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          },
          "exhaust": {
            "description": "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.",
            "type": "boolean"
          },
          "maxResults": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 2000
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "signal"
        ]
      }
    },
    {
      "name": "source_delete",
      "title": "Detach a lead list's signal source",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "listId",
          "type": "string",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "listId": {
            "type": "string",
            "minLength": 1,
            "description": "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."
          }
        },
        "required": [
          "listId"
        ]
      }
    },
    {
      "name": "source_get",
      "title": "Read a lead list's signal source",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "listId",
          "type": "string",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "listId": {
            "type": "string",
            "minLength": 1,
            "description": "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."
          }
        },
        "required": [
          "listId"
        ]
      }
    },
    {
      "name": "source_requalify",
      "title": "Re-qualify a source run's candidates against the current ICP",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "leadIds",
          "type": "string[]",
          "required": true,
          "description": "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`."
        },
        {
          "name": "listId",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "runId",
          "type": "string",
          "required": true,
          "description": "The run whose candidates to re-judge, from `source_runs`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "leadIds": {
            "minItems": 1,
            "maxItems": 200,
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "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": {
            "type": "string",
            "minLength": 1,
            "description": "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": {
            "type": "string",
            "description": "The run whose candidates to re-judge, from `source_runs`."
          }
        },
        "required": [
          "leadIds",
          "listId",
          "runId"
        ]
      }
    },
    {
      "name": "source_run",
      "title": "Run a lead list's signal source now",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "listId",
          "type": "string",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "listId": {
            "type": "string",
            "minLength": 1,
            "description": "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."
          }
        },
        "required": [
          "listId"
        ]
      }
    },
    {
      "name": "source_runs",
      "title": "Read a signal source's runs, or one run's candidates",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "listId",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "runId",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "description": "Rows per page, 1-100. Defaults to 25."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "listId": {
            "type": "string",
            "minLength": 1,
            "description": "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": {
            "description": "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.",
            "type": "string",
            "minLength": 1
          },
          "limit": {
            "description": "Rows per page, 1-100. Defaults to 25.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "cursor": {
            "description": "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.",
            "type": "string",
            "maxLength": 65536
          }
        },
        "required": [
          "listId"
        ]
      }
    },
    {
      "name": "source_set",
      "title": "Attach or replace a lead list's signal source",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "listId",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "icpId",
          "type": "string | null",
          "required": false,
          "description": "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."
        },
        {
          "name": "signals",
          "type": "object[]",
          "required": true,
          "description": "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."
        },
        {
          "name": "status",
          "type": "\"active\" | \"paused\"",
          "required": false,
          "description": "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`."
        },
        {
          "name": "contactFilter",
          "type": "object",
          "required": false,
          "description": "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."
        },
        {
          "name": "dailyTarget",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "peoplePerCompany",
          "type": "integer",
          "required": false,
          "description": "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."
        },
        {
          "name": "seatId",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "acceptExtraListCharge",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "listId": {
            "type": "string",
            "minLength": 1,
            "description": "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": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ]
          },
          "signals": {
            "minItems": 1,
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "changed_jobs",
                      "description": "People who recently changed jobs. Needs no Sales Navigator seat of your own: the search runs on Kairon’s Seat Pool. The exception is an ICP whose searched side has a viewer-relative filter (networkDistance, connectionsOf, pastColleague, sharedExperiences…, or a group, account-list or lead-list filter): this signal then reads on the list’s seat, which must have its own Sales Navigator (seatId)."
                    },
                    "config": {
                      "description": "No configuration — this signal takes none.",
                      "type": "object",
                      "properties": {}
                    }
                  },
                  "required": [
                    "signalKey"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "posted_recently",
                      "description": "People who posted recently. Needs no Sales Navigator seat of your own: the search runs on Kairon’s Seat Pool. The exception is an ICP whose searched side has a viewer-relative filter (networkDistance, connectionsOf, pastColleague, sharedExperiences…, or a group, account-list or lead-list filter): this signal then reads on the list’s seat, which must have its own Sales Navigator (seatId)."
                    },
                    "config": {
                      "description": "No configuration — this signal takes none.",
                      "type": "object",
                      "properties": {}
                    }
                  },
                  "required": [
                    "signalKey"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "viewed_my_profile",
                      "description": "People who viewed the seat’s profile. Needs a paid seat."
                    },
                    "config": {
                      "description": "No configuration — this signal takes none.",
                      "type": "object",
                      "properties": {}
                    }
                  },
                  "required": [
                    "signalKey"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "recently_connected",
                      "description": "People who recently accepted a connection. Any seat."
                    },
                    "config": {
                      "description": "No configuration — this signal takes none.",
                      "type": "object",
                      "properties": {}
                    }
                  },
                  "required": [
                    "signalKey"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "company_raised_funds",
                      "description": "Buyers at companies that recently raised funding. Needs no Sales Navigator seat of your own: the search runs on Kairon’s Seat Pool. The exception is an ICP whose searched side has a viewer-relative filter (networkDistance, connectionsOf, pastColleague, sharedExperiences…, or a group, account-list or lead-list filter): this signal then reads on the list’s seat, which must have its own Sales Navigator (seatId). Takes no config — both hops read the targeted ICP."
                    },
                    "config": {
                      "description": "No configuration — this signal takes none.",
                      "type": "object",
                      "properties": {}
                    }
                  },
                  "required": [
                    "signalKey"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "company_leadership_change",
                      "description": "Buyers at companies with a recent leadership change. Needs no Sales Navigator seat of your own: the search runs on Kairon’s Seat Pool. The exception is an ICP whose searched side has a viewer-relative filter (networkDistance, connectionsOf, pastColleague, sharedExperiences…, or a group, account-list or lead-list filter): this signal then reads on the list’s seat, which must have its own Sales Navigator (seatId). Takes no config — both hops read the targeted ICP."
                    },
                    "config": {
                      "description": "No configuration — this signal takes none.",
                      "type": "object",
                      "properties": {}
                    }
                  },
                  "required": [
                    "signalKey"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "lookalike",
                      "description": "People matching the targeted ICP, a few every day, with no event required — the signal to add when a list must keep growing while the event signals are quiet. Needs an ICP with Sales Navigator criteria on at least one side (an ICP with none is refused). Needs no Sales Navigator seat of your own: the search runs on Kairon’s Seat Pool. The exception is an ICP whose searched side has a viewer-relative filter (networkDistance, connectionsOf, pastColleague, sharedExperiences…, or a group, account-list or lead-list filter): this signal then reads on the list’s seat, which must have its own Sales Navigator (seatId). Takes no config — it walks the ICP’s own search. Unlike the event signals it carries no \"why now\", so a lead it appends gives the message writer less to open on."
                    },
                    "config": {
                      "description": "No configuration — this signal takes none.",
                      "type": "object",
                      "properties": {}
                    }
                  },
                  "required": [
                    "signalKey"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "posted_with_keyword",
                      "description": "People who published a post matching a query. Any seat. May appear MORE THAN ONCE on a source — one per query."
                    },
                    "config": {
                      "type": "object",
                      "properties": {
                        "keyword": {
                          "type": "string",
                          "minLength": 1,
                          "description": "The boolean search query the posts search runs, e.g. \"hiring SDR\" OR \"sales lead\"."
                        }
                      },
                      "required": [
                        "keyword"
                      ],
                      "description": "The query this signal watches for."
                    }
                  },
                  "required": [
                    "signalKey",
                    "config"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "engaged_with_keyword",
                      "description": "People who ENGAGED with posts matching a query — the engagers, not the authors. Any seat. May appear more than once."
                    },
                    "config": {
                      "type": "object",
                      "properties": {
                        "keyword": {
                          "type": "string",
                          "minLength": 1,
                          "description": "The boolean search query the posts search runs, e.g. \"hiring SDR\" OR \"sales lead\"."
                        }
                      },
                      "required": [
                        "keyword"
                      ],
                      "description": "The query this signal watches for."
                    }
                  },
                  "required": [
                    "signalKey",
                    "config"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "engaged_with_profile",
                      "description": "People who engaged with a chosen profile’s posts. Any seat. May appear more than once — one per watched profile."
                    },
                    "config": {
                      "type": "object",
                      "properties": {
                        "target": {
                          "type": "object",
                          "properties": {
                            "kind": {
                              "type": "string",
                              "enum": [
                                "person",
                                "company"
                              ],
                              "description": "Whether the watched profile is a person or a company."
                            },
                            "url": {
                              "type": "string",
                              "minLength": 1,
                              "description": "The LinkedIn URL of the profile whose engagers this surfaces."
                            }
                          },
                          "required": [
                            "kind",
                            "url"
                          ],
                          "description": "The profile being watched."
                        }
                      },
                      "required": [
                        "target"
                      ],
                      "description": "Whose engagement this signal surfaces."
                    }
                  },
                  "required": [
                    "signalKey",
                    "config"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "engaged_with_post",
                      "description": "People who commented on or reacted to ONE named post. Any seat. May appear more than once — one per watched post. Short-lived by nature: a post stops gathering engagement after a few days, and this signal then goes quiet on its own."
                    },
                    "config": {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "minLength": 1,
                          "description": "The LinkedIn permalink of the post whose engagers this surfaces."
                        }
                      },
                      "required": [
                        "url"
                      ],
                      "description": "The post this signal watches."
                    }
                  },
                  "required": [
                    "signalKey",
                    "config"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "company_followers",
                      "description": "People who FOLLOW a company page the source’s seat administers — an opted-in audience. Walks the whole follower list once, a few pages a day, then keeps picking up new followers. Any seat, but only for a page that seat runs. May appear more than once — one per page."
                    },
                    "config": {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 512,
                          "description": "The LinkedIn company page link (https://www.linkedin.com/company/<slug>). It must be a page the source’s seat ADMINISTERS — LinkedIn shows followers to admins only, and the save is refused otherwise."
                        }
                      },
                      "required": [
                        "url"
                      ],
                      "description": "The company page this signal walks."
                    }
                  },
                  "required": [
                    "signalKey",
                    "config"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "company_hiring",
                      "description": "Buyers at companies that just published a job posting matching a search — the hiring signal. The people search at each company uses the ICP’s people criteria. Both searches run on Kairon’s Seat Pool, so no seat of your own is needed — unless the job search sets inYourNetwork, or the ICP’s people side has a viewer-relative filter (that one needs a seat with its own Sales Navigator). Either case reads on the list’s seat (seatId). May appear more than once — one per search (e.g. one per country)."
                    },
                    "config": {
                      "type": "object",
                      "properties": {
                        "keywords": {
                          "description": "What the company is hiring FOR — a product, stack or role (\"Oracle\", \"NetSuite\", \"SAP HCM\"). This is the market sweep: it returns every company hiring that profile, so it DISCOVERS companies rather than filtering ones you already hold. Requires `region` unless `company` is also set. A product name usually needs no translation — \"Oracle\" catches Portuguese and Spanish postings alike.",
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 512
                        },
                        "region": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 200,
                          "description": "The ONE country to watch, in plain language (\"Brazil\") or as a LinkedIn geo id. Required: without it LinkedIn silently searches only the seat's own country. Watch another country with another hiring signal."
                        },
                        "datePosted": {
                          "description": "How many days back to look. A sweep defaults to 30 days, which is almost always right: LinkedIn removes a posting when the role closes, so 90 days measured 170 postings against 151 at 30 — to find MORE companies repeat the sweep in a few weeks rather than widening it. A COMPANY CHECK has no default window: \"is this company hiring\" is asked of everything still open, so an empty answer means \"not hiring\" rather than \"nothing lately\".",
                          "type": "integer",
                          "minimum": 1,
                          "maximum": 365
                        },
                        "roles": {
                          "description": "Job TITLES from LinkedIn's own taxonomy, in plain language (\"Product Analyst\", \"Head of Growth\") — resolved to title ids for you and echoed back. This is the sharpest filter this tool has and usually beats `keywords`: measured on the same Argentina sweep, `keywords: \"Product Analyst\"` returned 1241 postings while `roles: [\"Product Analyst\"]` returned 17, because keywords match anywhere in a posting while a title id matches the ROLE. LinkedIn treats several titles as OR — pass the variants you mean (\"Product Analyst\", \"Senior Product Analyst\").",
                          "minItems": 1,
                          "maxItems": 10,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 200
                          }
                        },
                        "functions": {
                          "description": "Job FUNCTIONS — the department a role belongs to (\"Product Management\", \"Engineering\", \"Marketing\"), resolved to LinkedIn ids and echoed back. Much wider than `roles`: Product Management alone returned 207 postings where the \"Product Analyst\" title returned 17. Reach for it to size a department across a market; reach for `roles` to find one job.",
                          "minItems": 1,
                          "maxItems": 10,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 200
                          }
                        },
                        "industries": {
                          "description": "The HIRING COMPANY's industry, in plain language (\"Financial Services\", \"Software Development\") — resolved to LinkedIn ids and echoed back. This filters by the employer, not by the role, so it is how a sweep is aimed at the market you actually sell to.",
                          "minItems": 1,
                          "maxItems": 10,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 200
                          }
                        },
                        "seniority": {
                          "description": "LinkedIn's EXPERIENCE LEVEL on the posting. This is the ONLY way to ask for seniority — putting \"Senior\" in `keywords` does the opposite of what it looks like, because LinkedIn ORs the words: the same sweep went from 1241 hits to 2480, wider, not narrower. Several levels are OR (\"senior and above\" is `[\"mid_senior\",\"director\",\"executive\"]`, measured 675 against a 1241 baseline).",
                          "minItems": 1,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "executive",
                              "director",
                              "mid_senior",
                              "associate",
                              "entry",
                              "intern"
                            ]
                          }
                        },
                        "jobType": {
                          "description": "LinkedIn's JOB TYPE — the employment arrangement. Several are OR.",
                          "minItems": 1,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "full_time",
                              "part_time",
                              "contract",
                              "temporary",
                              "volunteer",
                              "internship",
                              "other"
                            ]
                          }
                        },
                        "presence": {
                          "description": "On-site / hybrid / remote, as the posting declares it. Several are OR.",
                          "minItems": 1,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "on_site",
                              "hybrid",
                              "remote"
                            ]
                          }
                        },
                        "underTenApplicants": {
                          "description": "LinkedIn's UNDER 10 APPLICANTS. The sharpest recency filter there is — it says the posting is not just recent but still UNWORKED, and it cut the same 1241-hit sweep to 127. Use it when the value of the signal is getting there first.",
                          "type": "boolean"
                        },
                        "inYourNetwork": {
                          "description": "LinkedIn's IN YOUR NETWORK — only companies where the seat already has a connection. Warm ground: it cut the same sweep from 1241 to 265, and every hit comes with a path in.",
                          "type": "boolean"
                        },
                        "easyApply": {
                          "description": "LinkedIn's EASY APPLY.",
                          "type": "boolean"
                        },
                        "hasVerifications": {
                          "description": "LinkedIn's HAS VERIFICATIONS — the poster verified their identity.",
                          "type": "boolean"
                        },
                        "fairChanceEmployer": {
                          "description": "LinkedIn's FAIR CHANCE EMPLOYER.",
                          "type": "boolean"
                        },
                        "benefits": {
                          "description": "LinkedIn's BENEFITS, as the posting declares them. Several are OR.",
                          "minItems": 1,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "medical_insurance",
                              "vision_insurance",
                              "dental_insurance",
                              "disability_insurance",
                              "401(k)",
                              "pension_plan",
                              "paid_maternity_leave",
                              "paid_paternity_leave",
                              "commuter_benefits",
                              "student_loan_assistance",
                              "tuition_assistance"
                            ]
                          }
                        },
                        "commitments": {
                          "description": "LinkedIn's COMMITMENTS, as the company declares them. Several are OR.",
                          "minItems": 1,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "career_growth_and_learning",
                              "diversity_equity_and_inclusion",
                              "environmental_sustainability",
                              "social_impact",
                              "work_life_balance"
                            ]
                          }
                        },
                        "minimumSalary": {
                          "description": "LinkedIn's SALARY floor. Both fields are required together, and `value` is in THOUSANDS from a fixed ladder LinkedIn will not interpolate — dollars (USD/AUD/CAD) step 40…200, pounds (GBP) step 20…100. Off-ladder numbers are refused rather than rounded.",
                          "oneOf": [
                            {
                              "type": "object",
                              "properties": {
                                "currency": {
                                  "type": "string",
                                  "enum": [
                                    "USD",
                                    "AUD",
                                    "CAD"
                                  ],
                                  "description": "A dollar currency — these three share one ladder (40…200, in 20s)."
                                },
                                "value": {
                                  "anyOf": [
                                    {
                                      "type": "number",
                                      "const": 40
                                    },
                                    {
                                      "type": "number",
                                      "const": 60
                                    },
                                    {
                                      "type": "number",
                                      "const": 80
                                    },
                                    {
                                      "type": "number",
                                      "const": 100
                                    },
                                    {
                                      "type": "number",
                                      "const": 120
                                    },
                                    {
                                      "type": "number",
                                      "const": 140
                                    },
                                    {
                                      "type": "number",
                                      "const": 160
                                    },
                                    {
                                      "type": "number",
                                      "const": 180
                                    },
                                    {
                                      "type": "number",
                                      "const": 200
                                    }
                                  ],
                                  "description": "In THOUSANDS of dollars, from LinkedIn's fixed ladder (40 = \"$40,000+\")."
                                }
                              },
                              "required": [
                                "currency",
                                "value"
                              ]
                            },
                            {
                              "type": "object",
                              "properties": {
                                "currency": {
                                  "type": "string",
                                  "const": "GBP",
                                  "description": "Pounds — a different ladder (20…100, in 10s)."
                                },
                                "value": {
                                  "anyOf": [
                                    {
                                      "type": "number",
                                      "const": 20
                                    },
                                    {
                                      "type": "number",
                                      "const": 30
                                    },
                                    {
                                      "type": "number",
                                      "const": 40
                                    },
                                    {
                                      "type": "number",
                                      "const": 50
                                    },
                                    {
                                      "type": "number",
                                      "const": 60
                                    },
                                    {
                                      "type": "number",
                                      "const": 70
                                    },
                                    {
                                      "type": "number",
                                      "const": 80
                                    },
                                    {
                                      "type": "number",
                                      "const": 90
                                    },
                                    {
                                      "type": "number",
                                      "const": 100
                                    }
                                  ],
                                  "description": "In THOUSANDS of pounds, from LinkedIn's fixed ladder (20 = \"£20,000+\")."
                                }
                              },
                              "required": [
                                "currency",
                                "value"
                              ]
                            }
                          ]
                        }
                      },
                      "required": [
                        "region"
                      ],
                      "additionalProperties": false,
                      "description": "The job search to watch: every job_search market-sweep filter. `region` (one country, plain language) is REQUIRED, and so is at least one of `roles`, `keywords`, `functions`, `industries` — `roles` (job titles) is the sharpest. LinkedIn has no language filter: ask for Spanish-speaking roles with `keywords` such as \"español\". Checked with one job search when saved: a region or term that matches nothing on LinkedIn is refused."
                    }
                  },
                  "required": [
                    "signalKey",
                    "config"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "minLength": 1,
                      "description": "The id of an EXISTING signal, exactly as source_get returned it. Echo it to keep that signal and its place in the feed. Omit it to add a new signal. An id this source does not carry is refused, never treated as new."
                    },
                    "signalKey": {
                      "type": "string",
                      "const": "parked_lead_news",
                      "description": "The people already on this org’s Pipeline in Nurture, brought back when something happens to them. The ONLY signal that reads your own data instead of searching LinkedIn — so it grows a list with people you already gave up on, not strangers. It must be the ONLY signal on its source, and it never appears twice."
                    },
                    "config": {
                      "type": "object",
                      "properties": {
                        "triggers": {
                          "minItems": 1,
                          "type": "array",
                          "items": {
                            "type": "string",
                            "enum": [
                              "changed_jobs",
                              "company_news",
                              "posted"
                            ]
                          },
                          "description": "Which news wakes a parked lead. At least one. `changed_jobs` — they moved to a new company, the strongest trigger there is; when the list has an ICP, the wake re-checks them against it at the new place. `company_news` — the company they work at published a new post. `posted` — they published something you can reply to."
                        },
                        "cooldownDays": {
                          "default": 90,
                          "description": "How many days a lead is left alone after being woken. Defaults to 90, never below 7. It is what stops someone who posts every week being written to every week — the answer to \"how often may we come back\", which nothing else in the system decides.",
                          "type": "integer",
                          "minimum": 7,
                          "maximum": 9007199254740991
                        }
                      },
                      "required": [
                        "triggers"
                      ],
                      "description": "Which news wakes a parked lead, and how long they are left alone afterwards. `triggers`: `changed_jobs` (they moved company — the strongest one, and it re-checks them against the ICP at the new company), `company_news` (the company they work at published a new post), `posted` (they published something you can reply to). At least one. `cooldownDays` defaults to 90 and is never below 7: it is what stops someone who posts every week being written to every week."
                    }
                  },
                  "required": [
                    "signalKey",
                    "config"
                  ]
                }
              ]
            },
            "description": "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": {
            "description": "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`.",
            "type": "string",
            "enum": [
              "active",
              "paused"
            ]
          },
          "contactFilter": {
            "description": "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.",
            "type": "object",
            "properties": {
              "buckets": {
                "minItems": 1,
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "new",
                    "unanswered",
                    "in_conversation"
                  ]
                },
                "description": "Which contact-history buckets may enroll: `new` (no conversation either way, and nothing a Kairon campaign sent), `unanswered` (we messaged, no reply), `in_conversation` (they wrote to us). Unique, at least one. All three means enroll everyone. Anyone outside the selected buckets is skipped, not queued, so a narrow set is why a large audience enrolls almost nobody."
              },
              "scope": {
                "type": "string",
                "enum": [
                  "seat",
                  "org"
                ],
                "description": "Whose prior-contact history is read: `seat` (this campaign's sending seat) or `org` (every LinkedIn seat). Copying a `seat` filter onto a DIFFERENT seat changes what it means — the new seat's history is empty, so people the original filter excluded now enroll."
              }
            },
            "required": [
              "buckets",
              "scope"
            ]
          },
          "dailyTarget": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 1000
          },
          "peoplePerCompany": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "seatId": {
            "description": "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.",
            "type": "string",
            "minLength": 1
          },
          "acceptExtraListCharge": {
            "description": "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.",
            "type": "boolean"
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "listId",
          "signals"
        ]
      }
    },
    {
      "name": "tag_delete",
      "title": "Delete a tag",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "tag_list",
      "title": "Read the organization's tags",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "tag_save",
      "title": "Create or update a tag",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": false
        },
        {
          "name": "name",
          "type": "string",
          "required": false,
          "description": "The tag name. Required when creating; on an existing `id` it renames the tag everywhere."
        },
        {
          "name": "color",
          "type": "\"stone\" | \"clay\" | \"moss\" | \"sky\" | \"plum\" | \"amber\" | \"rust\" | \"slate\"",
          "required": false,
          "description": "Display colour for the tag. Omit and one is chosen."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1
          },
          "name": {
            "description": "The tag name. Required when creating; on an existing `id` it renames the tag everywhere.",
            "type": "string",
            "minLength": 1,
            "maxLength": 30
          },
          "color": {
            "description": "Display colour for the tag. Omit and one is chosen.",
            "type": "string",
            "enum": [
              "stone",
              "clay",
              "moss",
              "sky",
              "plum",
              "amber",
              "rust",
              "slate"
            ]
          }
        }
      }
    },
    {
      "name": "tag_set",
      "title": "Set which tags a lead or company carries",
      "description": "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`.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "asset",
          "type": "\"leads\" | \"companies\"",
          "required": true,
          "description": "Which kind of row these items are — people or companies."
        },
        {
          "name": "items",
          "type": "object[]",
          "required": true,
          "description": "The rows to tag, each naming its subject by ONE of `memberId`, `url`, or `providerId`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "asset": {
            "type": "string",
            "enum": [
              "leads",
              "companies"
            ],
            "description": "Which kind of row these items are — people or companies."
          },
          "items": {
            "minItems": 1,
            "maxItems": 500,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "memberId": {
                      "type": "string",
                      "minLength": 1
                    },
                    "tags": {
                      "maxItems": 25,
                      "type": "array",
                      "items": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 30
                      },
                      "description": "The subject's COMPLETE tag set after this call — it replaces what is there, it does not add. Send `[]` to clear every tag."
                    }
                  },
                  "required": [
                    "memberId",
                    "tags"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2048,
                      "description": "The person's LinkedIn profile URL, used when you do not have their `memberId`. Name each item exactly one way."
                    },
                    "tags": {
                      "maxItems": 25,
                      "type": "array",
                      "items": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 30
                      },
                      "description": "The subject's COMPLETE tag set after this call — it replaces what is there, it does not add. Send `[]` to clear every tag."
                    }
                  },
                  "required": [
                    "url",
                    "tags"
                  ]
                },
                {
                  "type": "object",
                  "properties": {
                    "providerId": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 256
                    },
                    "tags": {
                      "maxItems": 25,
                      "type": "array",
                      "items": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 30
                      },
                      "description": "The subject's COMPLETE tag set after this call — it replaces what is there, it does not add. Send `[]` to clear every tag."
                    }
                  },
                  "required": [
                    "providerId",
                    "tags"
                  ]
                }
              ]
            },
            "description": "The rows to tag, each naming its subject by ONE of `memberId`, `url`, or `providerId`."
          }
        },
        "required": [
          "asset",
          "items"
        ]
      }
    },
    {
      "name": "webdata_find",
      "title": "Find a data endpoint outside LinkedIn",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "query",
          "type": "string",
          "required": true,
          "description": "What data you are after, in plain words. Up to 1000 characters."
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "description": "How many matches to return. Default 5, at most 40."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "query": {
            "type": "string",
            "minLength": 1,
            "maxLength": 1000,
            "description": "What data you are after, in plain words. Up to 1000 characters."
          },
          "limit": {
            "description": "How many matches to return. Default 5, at most 40.",
            "type": "integer",
            "minimum": 1,
            "maximum": 40
          }
        },
        "required": [
          "query"
        ]
      }
    },
    {
      "name": "webdata_inspect",
      "title": "Read one data endpoint's inputs and price",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "provider",
          "type": "string",
          "required": true,
          "description": "The `provider` slug from `webdata_find`, e.g. `apify`."
        },
        {
          "name": "endpoint",
          "type": "string",
          "required": true,
          "description": "The `endpoint` path from `webdata_find`, e.g. `/apidojo/tweet-scraper`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "The `provider` slug from `webdata_find`, e.g. `apify`."
          },
          "endpoint": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "The `endpoint` path from `webdata_find`, e.g. `/apidojo/tweet-scraper`."
          }
        },
        "required": [
          "provider",
          "endpoint"
        ]
      }
    },
    {
      "name": "webdata_quote",
      "title": "Price a data run before running it",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "provider",
          "type": "string",
          "required": true,
          "description": "The `provider` slug from `webdata_find`, e.g. `apify`."
        },
        {
          "name": "endpoint",
          "type": "string",
          "required": true,
          "description": "The `endpoint` path from `webdata_find`, e.g. `/apidojo/tweet-scraper`."
        },
        {
          "name": "input",
          "type": "object",
          "required": false,
          "description": "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`."
        },
        {
          "name": "expectedResults",
          "type": "integer",
          "required": false,
          "description": "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`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "The `provider` slug from `webdata_find`, e.g. `apify`."
          },
          "endpoint": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "The `endpoint` path from `webdata_find`, e.g. `/apidojo/tweet-scraper`."
          },
          "input": {
            "description": "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`.",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          },
          "expectedResults": {
            "description": "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`.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100000
          }
        },
        "required": [
          "provider",
          "endpoint"
        ]
      }
    },
    {
      "name": "webdata_result",
      "title": "Read back a data run that was still going",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "runId",
          "type": "string",
          "required": true,
          "description": "The `runId` a still-running `webdata_run` returned."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "runId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "The `runId` a still-running `webdata_run` returned."
          }
        },
        "required": [
          "runId"
        ]
      }
    },
    {
      "name": "webdata_run",
      "title": "Run a data endpoint (spends the organization's allowance)",
      "description": "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.",
      "cluster": "list-building",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "provider",
          "type": "string",
          "required": true,
          "description": "The `provider` slug from `webdata_find`, e.g. `apify`."
        },
        {
          "name": "endpoint",
          "type": "string",
          "required": true,
          "description": "The `endpoint` path from `webdata_find`, e.g. `/apidojo/tweet-scraper`."
        },
        {
          "name": "input",
          "type": "object",
          "required": false,
          "description": "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`."
        },
        {
          "name": "quoteId",
          "type": "string",
          "required": true,
          "description": "The `quoteId` `webdata_quote` returned for this exact provider, endpoint and input."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "provider": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "The `provider` slug from `webdata_find`, e.g. `apify`."
          },
          "endpoint": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "The `endpoint` path from `webdata_find`, e.g. `/apidojo/tweet-scraper`."
          },
          "input": {
            "description": "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`.",
            "type": "object",
            "propertyNames": {
              "type": "string"
            },
            "additionalProperties": {}
          },
          "quoteId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 512,
            "description": "The `quoteId` `webdata_quote` returned for this exact provider, endpoint and input."
          }
        },
        "required": [
          "provider",
          "endpoint",
          "quoteId"
        ]
      }
    },
    {
      "name": "campaign_activate",
      "title": "Activate a campaign — starts sending to real people",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "startNow",
          "type": "boolean",
          "required": false,
          "description": "Default true: enroll today's first batch immediately instead of waiting for the next send window to open.",
          "default": "true"
        },
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "startNow": {
            "default": true,
            "description": "Default true: enroll today's first batch immediately instead of waiting for the next send window to open.",
            "type": "boolean"
          },
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "campaign_check_accepts",
      "title": "Check LinkedIn for new accepts",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "campaign_copy",
      "title": "Who still needs a per-lead line",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "missingOnly",
          "type": "boolean",
          "required": false,
          "description": "True 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.",
          "default": "false"
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "default": "50"
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1
          },
          "missingOnly": {
            "default": false,
            "description": "True 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.",
            "type": "boolean"
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          },
          "limit": {
            "default": 50,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "campaign_create",
      "title": "Create a draft campaign",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "name",
          "type": "string",
          "required": true,
          "description": "The campaign's name, for the operator to recognise it in a list. Not seen by any lead."
        },
        {
          "name": "channelAccountIds",
          "type": "string[]",
          "required": true,
          "description": "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`.)"
        },
        {
          "name": "icpId",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 200,
            "description": "The campaign's name, for the operator to recognise it in a list. Not seen by any lead."
          },
          "channelAccountIds": {
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            },
            "description": "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": {
            "description": "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.",
            "type": "string",
            "minLength": 1
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "name",
          "channelAccountIds"
        ]
      }
    },
    {
      "name": "campaign_get",
      "title": "One campaign in full — config, audience and both graphs",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "campaign_lead_start",
      "title": "Start one queued person today, ahead of the pacing",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        },
        {
          "name": "leadId",
          "type": "string",
          "required": true,
          "description": "The lead to start, as `campaign_runs` or `list_members` returned its id."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          },
          "leadId": {
            "type": "string",
            "minLength": 1,
            "description": "The lead to start, as `campaign_runs` or `list_members` returned its id."
          }
        },
        "required": [
          "id",
          "leadId"
        ]
      }
    },
    {
      "name": "campaign_list",
      "title": "List the org’s campaigns",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "q",
          "type": "string",
          "required": false,
          "description": "Case-insensitive substring of the campaign's name."
        },
        {
          "name": "status",
          "type": "string | string[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "initiative",
          "type": "string | \"none\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false,
          "description": "Opaque keyset token from the previous page's `nextCursor`. Omit for the first page."
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "description": "Campaigns per page, 1-100.",
          "default": "25"
        },
        {
          "name": "sort",
          "type": "\"name\" | \"status\" | \"sender\" | \"updated\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "direction",
          "type": "\"asc\" | \"desc\"",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "q": {
            "description": "Case-insensitive substring of the campaign's name.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "status": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "initiative": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "string",
                "const": "none"
              }
            ]
          },
          "cursor": {
            "description": "Opaque keyset token from the previous page's `nextCursor`. Omit for the first page.",
            "type": "string",
            "maxLength": 4096
          },
          "limit": {
            "default": 25,
            "description": "Campaigns per page, 1-100.",
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "sort": {
            "description": "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.",
            "type": "string",
            "enum": [
              "name",
              "status",
              "sender",
              "updated"
            ]
          },
          "direction": {
            "description": "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.",
            "type": "string",
            "enum": [
              "asc",
              "desc"
            ]
          }
        }
      }
    },
    {
      "name": "campaign_pause",
      "title": "Pause an active campaign",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "campaign_preview",
      "title": "Read what a step would actually send, before anyone gets it",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        },
        {
          "name": "nodeKey",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "leadIds",
          "type": "string[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "graph",
          "type": "\"published\" | \"draft\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "senderUserId",
          "type": "string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          },
          "nodeKey": {
            "type": "string",
            "minLength": 1,
            "description": "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": {
            "description": "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.",
            "minItems": 1,
            "maxItems": 3,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            }
          },
          "graph": {
            "description": "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.",
            "type": "string",
            "enum": [
              "published",
              "draft"
            ]
          },
          "senderUserId": {
            "description": "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.",
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "id",
          "nodeKey"
        ]
      }
    },
    {
      "name": "campaign_publish",
      "title": "Validate and publish the campaign's draft graph",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        },
        {
          "name": "migrateInFlight",
          "type": "boolean",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          },
          "migrateInFlight": {
            "description": "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.",
            "type": "boolean"
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "campaign_review_feed",
      "title": "The review desk — what is about to fire, and what is waiting on you",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "kind",
          "type": "\"autopilot\" | \"review\" | \"failed\" | \"done\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "campaignId",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "kind": {
            "description": "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.",
            "type": "string",
            "enum": [
              "autopilot",
              "review",
              "failed",
              "done"
            ]
          },
          "campaignId": {
            "description": "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.",
            "type": "string",
            "minLength": 1
          },
          "limit": {
            "description": "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.",
            "type": "integer",
            "minimum": 1,
            "maximum": 200
          }
        }
      }
    },
    {
      "name": "campaign_run_approve",
      "title": "Approve a manual campaign’s parked send — it goes out",
      "description": "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`.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "runId",
          "type": "string",
          "required": true,
          "description": "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`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "runId": {
            "type": "string",
            "pattern": "^srun_.*",
            "description": "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`."
          }
        },
        "required": [
          "runId"
        ]
      }
    },
    {
      "name": "campaign_run_cancel",
      "title": "End these people’s sequences — nothing further is sent to them",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "runIds",
          "type": "string[]",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "runIds": {
            "minItems": 1,
            "maxItems": 100,
            "type": "array",
            "items": {
              "type": "string",
              "pattern": "^srun_.*"
            },
            "description": "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."
          }
        },
        "required": [
          "runIds"
        ]
      }
    },
    {
      "name": "campaign_run_get",
      "title": "One sequence run, step by step",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "runId",
          "type": "string",
          "required": true,
          "description": "One run's id, as `campaign_runs` returned it — a run is one enrolled lead."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "runId": {
            "type": "string",
            "minLength": 1,
            "description": "One run's id, as `campaign_runs` returned it — a run is one enrolled lead."
          }
        },
        "required": [
          "runId"
        ]
      }
    },
    {
      "name": "campaign_run_skip_step",
      "title": "Skip the step this person is on — their sequence goes on",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "runId",
          "type": "string",
          "required": true,
          "description": "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`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "runId": {
            "type": "string",
            "pattern": "^srun_.*",
            "description": "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`."
          }
        },
        "required": [
          "runId"
        ]
      }
    },
    {
      "name": "campaign_run_skip_wait",
      "title": "Collapse the delay this run is waiting on",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "runId",
          "type": "string",
          "required": true,
          "description": "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`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "runId": {
            "type": "string",
            "pattern": "^srun_.*",
            "description": "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`."
          }
        },
        "required": [
          "runId"
        ]
      }
    },
    {
      "name": "campaign_runs",
      "title": "The campaign's sequence runs",
      "description": "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`.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "bucket",
          "type": "\"waiting\" | \"didnt_accept\" | \"didnt_answer\" | \"replied\" | \"skipped\" | \"error\" | \"interested\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "stage",
          "type": "string | string[]",
          "required": false,
          "description": "Activity states to include; several read as \"any of these\"."
        },
        {
          "name": "nodeKey",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "q",
          "type": "string",
          "required": false,
          "description": "Find a person by name, headline, company or LinkedIn handle."
        },
        {
          "name": "dateFrom",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "dateTo",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "listId",
          "type": "string | string[]",
          "required": false,
          "description": "Only people enrolled from these source lists."
        },
        {
          "name": "qualificationStatus",
          "type": "string | string[]",
          "required": false,
          "description": "Only people with these ICP verdicts; several read as \"any of these\"."
        },
        {
          "name": "funnel",
          "type": "\"invite_sent\" | \"invite_accepted\" | \"message_sent\" | \"replied\" | \"interested\"",
          "required": false,
          "description": "Only the people one Performance funnel card counts: `invite_sent`, `invite_accepted`, `message_sent`, `replied` or `interested`."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false,
          "default": "50"
        },
        {
          "name": "id",
          "type": "string",
          "required": true
        },
        {
          "name": "includeQualification",
          "type": "boolean",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "bucket": {
            "description": "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.",
            "type": "string",
            "enum": [
              "waiting",
              "didnt_accept",
              "didnt_answer",
              "replied",
              "skipped",
              "error",
              "interested"
            ]
          },
          "stage": {
            "description": "Activity states to include; several read as \"any of these\".",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "nodeKey": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "q": {
            "description": "Find a person by name, headline, company or LinkedIn handle.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "dateFrom": {
            "description": "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.",
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "dateTo": {
            "description": "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.",
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
          },
          "listId": {
            "description": "Only people enrolled from these source lists.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "qualificationStatus": {
            "description": "Only people with these ICP verdicts; several read as \"any of these\".",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "array",
                "items": {
                  "type": "string"
                }
              }
            ]
          },
          "funnel": {
            "description": "Only the people one Performance funnel card counts: `invite_sent`, `invite_accepted`, `message_sent`, `replied` or `interested`.",
            "type": "string",
            "enum": [
              "invite_sent",
              "invite_accepted",
              "message_sent",
              "replied",
              "interested"
            ]
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          },
          "limit": {
            "default": 50,
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          },
          "id": {
            "type": "string",
            "minLength": 1
          },
          "includeQualification": {
            "description": "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.",
            "type": "boolean"
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "campaign_seat_add",
      "title": "Add a sender to a campaign",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "channelAccountId",
          "type": "string",
          "required": true,
          "description": "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)."
        },
        {
          "name": "dailyEnrollmentLimit",
          "type": "integer",
          "required": false,
          "description": "This sender's own new leads per day, 1-200. Omit to use the campaign's default (`dailyEnrollmentLimit`)."
        },
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "channelAccountId": {
            "type": "string",
            "minLength": 1,
            "description": "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": {
            "description": "This sender's own new leads per day, 1-200. Omit to use the campaign's default (`dailyEnrollmentLimit`).",
            "type": "integer",
            "minimum": 1,
            "maximum": 200
          },
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          }
        },
        "required": [
          "channelAccountId",
          "id"
        ]
      }
    },
    {
      "name": "campaign_seat_remove",
      "title": "Remove a sender from a campaign",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        },
        {
          "name": "channelAccountId",
          "type": "string",
          "required": true,
          "description": "The sender seat — a channel account id from the campaign view’s `seats`."
        },
        {
          "name": "leads",
          "type": "\"finish\" | \"stop\"",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          },
          "channelAccountId": {
            "type": "string",
            "minLength": 1,
            "description": "The sender seat — a channel account id from the campaign view’s `seats`."
          },
          "leads": {
            "type": "string",
            "enum": [
              "finish",
              "stop"
            ],
            "description": "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."
          }
        },
        "required": [
          "id",
          "channelAccountId",
          "leads"
        ]
      }
    },
    {
      "name": "campaign_seat_update",
      "title": "Set a sender's daily limit",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "dailyEnrollmentLimit",
          "type": "integer | null",
          "required": true,
          "description": "This sender's own new leads per day, 1-200, or `null` to go back to the campaign's default."
        },
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        },
        {
          "name": "channelAccountId",
          "type": "string",
          "required": true,
          "description": "The sender seat — a channel account id from the campaign view’s `seats`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "dailyEnrollmentLimit": {
            "anyOf": [
              {
                "type": "integer",
                "minimum": 1,
                "maximum": 200
              },
              {
                "type": "null"
              }
            ],
            "description": "This sender's own new leads per day, 1-200, or `null` to go back to the campaign's default."
          },
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          },
          "channelAccountId": {
            "type": "string",
            "minLength": 1,
            "description": "The sender seat — a channel account id from the campaign view’s `seats`."
          }
        },
        "required": [
          "dailyEnrollmentLimit",
          "id",
          "channelAccountId"
        ]
      }
    },
    {
      "name": "campaign_send_edit",
      "title": "Rewrite a booked send before it fires",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "nodeRunId",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "body",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "subject",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "nodeRunId": {
            "type": "string",
            "pattern": "^nrun_.*",
            "description": "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": {
            "type": "string",
            "maxLength": 8000,
            "description": "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": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "nodeRunId",
          "body"
        ]
      }
    },
    {
      "name": "campaign_send_now",
      "title": "Fire this person’s next send immediately",
      "description": "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`).",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "runId",
          "type": "string",
          "required": true,
          "description": "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`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "runId": {
            "type": "string",
            "pattern": "^srun_.*",
            "description": "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`."
          }
        },
        "required": [
          "runId"
        ]
      }
    },
    {
      "name": "campaign_set_graph",
      "title": "Replace the campaign's draft sequence graph",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        },
        {
          "name": "graph",
          "type": "object",
          "required": true,
          "description": "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`."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          },
          "graph": {
            "description": "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`.",
            "type": "object",
            "properties": {
              "nodes": {
                "maxItems": 100,
                "type": "array",
                "items": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "send_invite",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "note": {
                              "default": "",
                              "description": "Invite note, and the seed when `draftMode` is `ai_template`. In `ai_template` this seed is authoritative: the writer reproduces it word for word and adds nothing, so EVERY per-lead detail must sit inside a `{{...}}` slot written here. A seed with no slots sends the identical message to every lead. A slot holds a description of the fact to write there, phrased as a clause — e.g. `{{one short clause, 8-14 words, on what their company does}}`. 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. At publish it must be ≤300 characters, or ≤250 once it contains any `{{token}}` (the braces count), leaving room for what the token expands to. Over that, publish fails with `invite_note_too_long`. A note needs a sender with LinkedIn Premium or Sales Navigator (`canSendInviteNote` on `seat_list`); on any other seat leave it empty — `campaign_set_graph` refuses a note there.",
                              "type": "string",
                              "maxLength": 2000
                            },
                            "draftMode": {
                              "description": "How the copy is produced. `template`: sent as written — only allowlisted `{{variables}}` and per-lead `{{@slots}}` are substituted, no AI. `ai_template`: the AI personalizes YOUR seed, filling its `{{described slots}}` and nothing else. Absent means `template`.",
                              "type": "string",
                              "enum": [
                                "template",
                                "ai_template"
                              ]
                            },
                            "instruction": {
                              "description": "Optional note on tone and register, layered on the seed — NOT where the copy comes from. It reaches the writer AFTER the rule that says to keep the seed as written, so anything it asks for that the seed has no `{{slot}}` for is silently ignored: \"personalize the opening with their company\" does nothing unless a slot sits there. Write the copy in the seed.",
                              "type": "string",
                              "maxLength": 4000
                            },
                            "language": {
                              "description": "Force every message on this step into one language, or set 'auto' so each lead is written to in their OWN language, translating your seed faithfully. OMIT IT and the step uses the default language the sender chose in their settings, or 'auto' when they chose none — which is usually what you want. This rule outranks the seed's original language.",
                              "anyOf": [
                                {
                                  "type": "string",
                                  "const": "auto"
                                },
                                {
                                  "type": "string",
                                  "enum": [
                                    "aa",
                                    "ab",
                                    "ae",
                                    "af",
                                    "ak",
                                    "am",
                                    "an",
                                    "ar",
                                    "as",
                                    "av",
                                    "ay",
                                    "az",
                                    "ba",
                                    "be",
                                    "bg",
                                    "bh",
                                    "bi",
                                    "bm",
                                    "bn",
                                    "bo",
                                    "br",
                                    "bs",
                                    "ca",
                                    "ce",
                                    "ch",
                                    "co",
                                    "cr",
                                    "cs",
                                    "cu",
                                    "cv",
                                    "cy",
                                    "da",
                                    "de",
                                    "dv",
                                    "dz",
                                    "ee",
                                    "el",
                                    "en",
                                    "eo",
                                    "es",
                                    "et",
                                    "eu",
                                    "fa",
                                    "ff",
                                    "fi",
                                    "fj",
                                    "fo",
                                    "fr",
                                    "fy",
                                    "ga",
                                    "gd",
                                    "gl",
                                    "gn",
                                    "gu",
                                    "gv",
                                    "ha",
                                    "he",
                                    "hi",
                                    "ho",
                                    "hr",
                                    "ht",
                                    "hu",
                                    "hy",
                                    "hz",
                                    "ia",
                                    "id",
                                    "ie",
                                    "ig",
                                    "ii",
                                    "ik",
                                    "io",
                                    "is",
                                    "it",
                                    "iu",
                                    "ja",
                                    "jv",
                                    "ka",
                                    "kg",
                                    "ki",
                                    "kj",
                                    "kk",
                                    "kl",
                                    "km",
                                    "kn",
                                    "ko",
                                    "kr",
                                    "ks",
                                    "ku",
                                    "kv",
                                    "kw",
                                    "ky",
                                    "la",
                                    "lb",
                                    "lg",
                                    "li",
                                    "ln",
                                    "lo",
                                    "lt",
                                    "lu",
                                    "lv",
                                    "mg",
                                    "mh",
                                    "mi",
                                    "mk",
                                    "ml",
                                    "mn",
                                    "mr",
                                    "ms",
                                    "mt",
                                    "my",
                                    "na",
                                    "nb",
                                    "nd",
                                    "ne",
                                    "ng",
                                    "nl",
                                    "nn",
                                    "no",
                                    "nr",
                                    "nv",
                                    "ny",
                                    "oc",
                                    "oj",
                                    "om",
                                    "or",
                                    "os",
                                    "pa",
                                    "pi",
                                    "pl",
                                    "ps",
                                    "pt",
                                    "qu",
                                    "rm",
                                    "rn",
                                    "ro",
                                    "ru",
                                    "rw",
                                    "sa",
                                    "sc",
                                    "sd",
                                    "se",
                                    "sg",
                                    "si",
                                    "sk",
                                    "sl",
                                    "sm",
                                    "sn",
                                    "so",
                                    "sq",
                                    "sr",
                                    "ss",
                                    "st",
                                    "su",
                                    "sv",
                                    "sw",
                                    "ta",
                                    "te",
                                    "tg",
                                    "th",
                                    "ti",
                                    "tk",
                                    "tl",
                                    "tn",
                                    "to",
                                    "tr",
                                    "ts",
                                    "tt",
                                    "tw",
                                    "ty",
                                    "ug",
                                    "uk",
                                    "ur",
                                    "uz",
                                    "ve",
                                    "vi",
                                    "vo",
                                    "wa",
                                    "wo",
                                    "xh",
                                    "yi",
                                    "yo",
                                    "za",
                                    "zh",
                                    "zu"
                                  ]
                                }
                              ]
                            },
                            "localization": {
                              "description": "The seed translated into other languages, one per ISO 639-1 code, each keeping every `{{…}}` slot byte-exact and every `---` line. Kairon writes a language here the first time it drafts a lead in it; you may edit any entry and it is then used as written for every later lead in that language. Leave it out to let Kairon translate.",
                              "type": "object",
                              "properties": {
                                "seedHash": {
                                  "type": "string",
                                  "minLength": 1,
                                  "description": "Hash of the seed text these translations were made for. Kairon sets it; when the seed changes, the whole object goes stale and is rebuilt. Copy it through unchanged."
                                },
                                "seedLanguage": {
                                  "type": "string",
                                  "enum": [
                                    "aa",
                                    "ab",
                                    "ae",
                                    "af",
                                    "ak",
                                    "am",
                                    "an",
                                    "ar",
                                    "as",
                                    "av",
                                    "ay",
                                    "az",
                                    "ba",
                                    "be",
                                    "bg",
                                    "bh",
                                    "bi",
                                    "bm",
                                    "bn",
                                    "bo",
                                    "br",
                                    "bs",
                                    "ca",
                                    "ce",
                                    "ch",
                                    "co",
                                    "cr",
                                    "cs",
                                    "cu",
                                    "cv",
                                    "cy",
                                    "da",
                                    "de",
                                    "dv",
                                    "dz",
                                    "ee",
                                    "el",
                                    "en",
                                    "eo",
                                    "es",
                                    "et",
                                    "eu",
                                    "fa",
                                    "ff",
                                    "fi",
                                    "fj",
                                    "fo",
                                    "fr",
                                    "fy",
                                    "ga",
                                    "gd",
                                    "gl",
                                    "gn",
                                    "gu",
                                    "gv",
                                    "ha",
                                    "he",
                                    "hi",
                                    "ho",
                                    "hr",
                                    "ht",
                                    "hu",
                                    "hy",
                                    "hz",
                                    "ia",
                                    "id",
                                    "ie",
                                    "ig",
                                    "ii",
                                    "ik",
                                    "io",
                                    "is",
                                    "it",
                                    "iu",
                                    "ja",
                                    "jv",
                                    "ka",
                                    "kg",
                                    "ki",
                                    "kj",
                                    "kk",
                                    "kl",
                                    "km",
                                    "kn",
                                    "ko",
                                    "kr",
                                    "ks",
                                    "ku",
                                    "kv",
                                    "kw",
                                    "ky",
                                    "la",
                                    "lb",
                                    "lg",
                                    "li",
                                    "ln",
                                    "lo",
                                    "lt",
                                    "lu",
                                    "lv",
                                    "mg",
                                    "mh",
                                    "mi",
                                    "mk",
                                    "ml",
                                    "mn",
                                    "mr",
                                    "ms",
                                    "mt",
                                    "my",
                                    "na",
                                    "nb",
                                    "nd",
                                    "ne",
                                    "ng",
                                    "nl",
                                    "nn",
                                    "no",
                                    "nr",
                                    "nv",
                                    "ny",
                                    "oc",
                                    "oj",
                                    "om",
                                    "or",
                                    "os",
                                    "pa",
                                    "pi",
                                    "pl",
                                    "ps",
                                    "pt",
                                    "qu",
                                    "rm",
                                    "rn",
                                    "ro",
                                    "ru",
                                    "rw",
                                    "sa",
                                    "sc",
                                    "sd",
                                    "se",
                                    "sg",
                                    "si",
                                    "sk",
                                    "sl",
                                    "sm",
                                    "sn",
                                    "so",
                                    "sq",
                                    "sr",
                                    "ss",
                                    "st",
                                    "su",
                                    "sv",
                                    "sw",
                                    "ta",
                                    "te",
                                    "tg",
                                    "th",
                                    "ti",
                                    "tk",
                                    "tl",
                                    "tn",
                                    "to",
                                    "tr",
                                    "ts",
                                    "tt",
                                    "tw",
                                    "ty",
                                    "ug",
                                    "uk",
                                    "ur",
                                    "uz",
                                    "ve",
                                    "vi",
                                    "vo",
                                    "wa",
                                    "wo",
                                    "xh",
                                    "yi",
                                    "yo",
                                    "za",
                                    "zh",
                                    "zu"
                                  ],
                                  "description": "The ISO 639-1 code of the language the seed itself is written in, detected once by Kairon."
                                },
                                "translations": {
                                  "type": "object",
                                  "propertyNames": {
                                    "type": "string",
                                    "enum": [
                                      "aa",
                                      "ab",
                                      "ae",
                                      "af",
                                      "ak",
                                      "am",
                                      "an",
                                      "ar",
                                      "as",
                                      "av",
                                      "ay",
                                      "az",
                                      "ba",
                                      "be",
                                      "bg",
                                      "bh",
                                      "bi",
                                      "bm",
                                      "bn",
                                      "bo",
                                      "br",
                                      "bs",
                                      "ca",
                                      "ce",
                                      "ch",
                                      "co",
                                      "cr",
                                      "cs",
                                      "cu",
                                      "cv",
                                      "cy",
                                      "da",
                                      "de",
                                      "dv",
                                      "dz",
                                      "ee",
                                      "el",
                                      "en",
                                      "eo",
                                      "es",
                                      "et",
                                      "eu",
                                      "fa",
                                      "ff",
                                      "fi",
                                      "fj",
                                      "fo",
                                      "fr",
                                      "fy",
                                      "ga",
                                      "gd",
                                      "gl",
                                      "gn",
                                      "gu",
                                      "gv",
                                      "ha",
                                      "he",
                                      "hi",
                                      "ho",
                                      "hr",
                                      "ht",
                                      "hu",
                                      "hy",
                                      "hz",
                                      "ia",
                                      "id",
                                      "ie",
                                      "ig",
                                      "ii",
                                      "ik",
                                      "io",
                                      "is",
                                      "it",
                                      "iu",
                                      "ja",
                                      "jv",
                                      "ka",
                                      "kg",
                                      "ki",
                                      "kj",
                                      "kk",
                                      "kl",
                                      "km",
                                      "kn",
                                      "ko",
                                      "kr",
                                      "ks",
                                      "ku",
                                      "kv",
                                      "kw",
                                      "ky",
                                      "la",
                                      "lb",
                                      "lg",
                                      "li",
                                      "ln",
                                      "lo",
                                      "lt",
                                      "lu",
                                      "lv",
                                      "mg",
                                      "mh",
                                      "mi",
                                      "mk",
                                      "ml",
                                      "mn",
                                      "mr",
                                      "ms",
                                      "mt",
                                      "my",
                                      "na",
                                      "nb",
                                      "nd",
                                      "ne",
                                      "ng",
                                      "nl",
                                      "nn",
                                      "no",
                                      "nr",
                                      "nv",
                                      "ny",
                                      "oc",
                                      "oj",
                                      "om",
                                      "or",
                                      "os",
                                      "pa",
                                      "pi",
                                      "pl",
                                      "ps",
                                      "pt",
                                      "qu",
                                      "rm",
                                      "rn",
                                      "ro",
                                      "ru",
                                      "rw",
                                      "sa",
                                      "sc",
                                      "sd",
                                      "se",
                                      "sg",
                                      "si",
                                      "sk",
                                      "sl",
                                      "sm",
                                      "sn",
                                      "so",
                                      "sq",
                                      "sr",
                                      "ss",
                                      "st",
                                      "su",
                                      "sv",
                                      "sw",
                                      "ta",
                                      "te",
                                      "tg",
                                      "th",
                                      "ti",
                                      "tk",
                                      "tl",
                                      "tn",
                                      "to",
                                      "tr",
                                      "ts",
                                      "tt",
                                      "tw",
                                      "ty",
                                      "ug",
                                      "uk",
                                      "ur",
                                      "uz",
                                      "ve",
                                      "vi",
                                      "vo",
                                      "wa",
                                      "wo",
                                      "xh",
                                      "yi",
                                      "yo",
                                      "za",
                                      "zh",
                                      "zu"
                                    ]
                                  },
                                  "additionalProperties": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "description": "One translated seed per ISO 639-1 code. Each must keep every `{{…}}` slot byte-exact and in order, and the same number of `---` lines, or publish refuses it."
                                }
                              },
                              "required": [
                                "seedHash",
                                "seedLanguage",
                                "translations"
                              ]
                            },
                            "reviewBeforeSend": {
                              "description": "Default false. True holds every send this step drafts for the operator to approve, even when the campaign is on autopilot. Nothing is scheduled until they approve it.",
                              "type": "boolean"
                            },
                            "verifyBeforeSend": {
                              "description": "DEPRECATED, and accepted only so campaigns published before the rename still parse. Set `reviewBeforeSend` instead; if both are present the new one wins.",
                              "type": "boolean"
                            },
                            "adaptToCulture": {
                              "description": "Retired: accepted for old steps and ignored. Do not send it.",
                              "type": "boolean"
                            }
                          },
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "wait_connection_accepted",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "timeoutDays": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 30,
                              "description": "How many whole days to wait for the invite to be accepted, 1-30. On expiry the run leaves by the `timeout` edge; wire one or those leads stop here."
                            },
                            "withdrawOnTimeout": {
                              "description": "Withdraw the pending invite when the wait times out. Default true — it reclaims LinkedIn pending-invite quota, which is capped. Set false to leave the invite standing, so someone who accepts weeks later still lands on the `accepted` edge of a later wait.",
                              "type": "boolean"
                            }
                          },
                          "required": [
                            "timeoutDays"
                          ],
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "delay",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "value": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 43200,
                              "description": "How long to pause before the next step, counted in `unit`. The total may not exceed 30 days."
                            },
                            "unit": {
                              "type": "string",
                              "enum": [
                                "minutes",
                                "hours",
                                "days"
                              ],
                              "description": "The unit `value` is counted in. Use `minutes`/`hours` for the hot follow-up right after an invite is accepted, `days` for a nurture gap. Counted in real time, not send windows — the next step then waits for the window to open."
                            }
                          },
                          "required": [
                            "value",
                            "unit"
                          ],
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "wait_reply",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "value": {
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 43200,
                              "description": "How long to wait for the lead to reply, counted in `unit`. The total may not exceed 30 days."
                            },
                            "unit": {
                              "type": "string",
                              "enum": [
                                "minutes",
                                "hours",
                                "days"
                              ],
                              "description": "The unit `value` is counted in. `minutes`/`hours` for a same-day nudge, `days` for a normal follow-up gap."
                            }
                          },
                          "required": [
                            "value",
                            "unit"
                          ],
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "agentic_reply",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "instruction": {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 8000,
                              "description": "REQUIRED. The goal — what the operator wants out of this conversation, in one line. E.g. \"Get them to book a call with me.\" or \"Find out if they own hiring, or who does.\" This is the only field the step needs: the agent chooses its own wording and works out for itself when the conversation is finished. It does NOT invent substance — anything it cannot answer straight from this goal and `additionalInstructions` is handed to a human instead, so the more `additionalInstructions` says, the fewer turns come back to the operator."
                            },
                            "additionalInstructions": {
                              "default": "",
                              "description": "Optional, and it decides how much the agent can answer on its own: it hands a turn to a human whenever the answer is not straightforward from the goal plus this field. Put here the facts it may use (dates, prices, links — STATE EVERY ONE, it must never state a fact you did not give it), when to treat the conversation as finished, what to hand straight to a human whatever it knows, how to answer an objection you expect, and any other steering. E.g. \"The event is Tuesday 12 March, 6pm, at our office. If they ask who else is going, hand it to me. If they say it is too far, offer the recording.\"",
                              "type": "string",
                              "maxLength": 8000
                            },
                            "silence": {
                              "default": {
                                "value": 7,
                                "unit": "days"
                              },
                              "description": "How long the agent keeps listening — for her FIRST reply, and after each of its own answers. She goes quiet past this and the step leaves by `no_reply`, so a follow-up can still chase her.",
                              "type": "object",
                              "properties": {
                                "value": {
                                  "type": "integer",
                                  "minimum": 1,
                                  "maximum": 43200,
                                  "description": "How long to keep listening for her next message, counted in `unit`. The total may not exceed 30 days."
                                },
                                "unit": {
                                  "type": "string",
                                  "enum": [
                                    "minutes",
                                    "hours",
                                    "days"
                                  ],
                                  "description": "The unit `value` is counted in."
                                }
                              },
                              "required": [
                                "value",
                                "unit"
                              ]
                            },
                            "maxTurns": {
                              "description": "How many times the agent may answer before it hands off for good and the run ends. Defaults to 10.",
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 20
                            },
                            "attachments": {
                              "description": "Files the agent may send with an answer when they fit the conversation — each at most once per conversation. Say in the instruction when to send which. Images and PDFs; on LinkedIn also one voice note and one video. Each is `{ fileName, mimeType, url }` (https) or `{ fileName, mimeType, dataBase64 }`, as `message_send` takes; a file `campaign_get` returned may be sent back as is.",
                              "maxItems": 5,
                              "type": "array",
                              "items": {
                                "anyOf": [
                                  {
                                    "type": "object",
                                    "properties": {
                                      "storageKey": {
                                        "type": "string",
                                        "maxLength": 512,
                                        "pattern": "^step-attachments\\/.*"
                                      },
                                      "fileName": {
                                        "type": "string",
                                        "minLength": 1,
                                        "maxLength": 255
                                      },
                                      "mimeType": {
                                        "type": "string",
                                        "minLength": 1,
                                        "maxLength": 255
                                      },
                                      "byteSize": {
                                        "type": "integer",
                                        "exclusiveMinimum": 0,
                                        "maximum": 9007199254740991
                                      }
                                    },
                                    "required": [
                                      "storageKey",
                                      "fileName",
                                      "mimeType",
                                      "byteSize"
                                    ],
                                    "additionalProperties": false
                                  },
                                  {
                                    "anyOf": [
                                      {
                                        "type": "object",
                                        "properties": {
                                          "fileName": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file name, with its extension — what the recipient sees."
                                          },
                                          "mimeType": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                                          },
                                          "dataBase64": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 4194304,
                                            "description": "The file itself, base64-encoded. Use this OR `url`, never both."
                                          }
                                        },
                                        "required": [
                                          "fileName",
                                          "mimeType",
                                          "dataBase64"
                                        ],
                                        "additionalProperties": false
                                      },
                                      {
                                        "type": "object",
                                        "properties": {
                                          "fileName": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file name, with its extension — what the recipient sees."
                                          },
                                          "mimeType": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                                          },
                                          "url": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 2048,
                                            "format": "uri",
                                            "description": "An https address we fetch server-side. Use this OR `dataBase64`, never both. Reach for it when you cannot produce base64."
                                          }
                                        },
                                        "required": [
                                          "fileName",
                                          "mimeType",
                                          "url"
                                        ],
                                        "additionalProperties": false
                                      }
                                    ]
                                  }
                                ]
                              }
                            }
                          },
                          "required": [
                            "instruction"
                          ],
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "send_message",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "body": {
                              "default": "",
                              "description": "The message. In `ai_template` this seed is authoritative: the writer reproduces it word for word and adds nothing, so EVERY per-lead detail must sit inside a `{{...}}` slot written here. A seed with no slots sends the identical message to every lead. A slot holds a description of the fact to write there, phrased as a clause — e.g. `{{one short clause, 8-14 words, on what their company does}}`. 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. 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.",
                              "type": "string",
                              "maxLength": 8000
                            },
                            "draftMode": {
                              "description": "How the copy is produced. `template`: sent as written — only allowlisted `{{variables}}` and per-lead `{{@slots}}` are substituted, no AI. `ai_template`: the AI personalizes YOUR seed, filling its `{{described slots}}` and nothing else. Absent means `template`.",
                              "type": "string",
                              "enum": [
                                "template",
                                "ai_template"
                              ]
                            },
                            "instruction": {
                              "description": "Optional note on tone and register, layered on the seed — NOT where the copy comes from. It reaches the writer AFTER the rule that says to keep the seed as written, so anything it asks for that the seed has no `{{slot}}` for is silently ignored: \"personalize the opening with their company\" does nothing unless a slot sits there. Write the copy in the seed.",
                              "type": "string",
                              "maxLength": 4000
                            },
                            "language": {
                              "description": "Force every message on this step into one language, or set 'auto' so each lead is written to in their OWN language, translating your seed faithfully. OMIT IT and the step uses the default language the sender chose in their settings, or 'auto' when they chose none — which is usually what you want. This rule outranks the seed's original language.",
                              "anyOf": [
                                {
                                  "type": "string",
                                  "const": "auto"
                                },
                                {
                                  "type": "string",
                                  "enum": [
                                    "aa",
                                    "ab",
                                    "ae",
                                    "af",
                                    "ak",
                                    "am",
                                    "an",
                                    "ar",
                                    "as",
                                    "av",
                                    "ay",
                                    "az",
                                    "ba",
                                    "be",
                                    "bg",
                                    "bh",
                                    "bi",
                                    "bm",
                                    "bn",
                                    "bo",
                                    "br",
                                    "bs",
                                    "ca",
                                    "ce",
                                    "ch",
                                    "co",
                                    "cr",
                                    "cs",
                                    "cu",
                                    "cv",
                                    "cy",
                                    "da",
                                    "de",
                                    "dv",
                                    "dz",
                                    "ee",
                                    "el",
                                    "en",
                                    "eo",
                                    "es",
                                    "et",
                                    "eu",
                                    "fa",
                                    "ff",
                                    "fi",
                                    "fj",
                                    "fo",
                                    "fr",
                                    "fy",
                                    "ga",
                                    "gd",
                                    "gl",
                                    "gn",
                                    "gu",
                                    "gv",
                                    "ha",
                                    "he",
                                    "hi",
                                    "ho",
                                    "hr",
                                    "ht",
                                    "hu",
                                    "hy",
                                    "hz",
                                    "ia",
                                    "id",
                                    "ie",
                                    "ig",
                                    "ii",
                                    "ik",
                                    "io",
                                    "is",
                                    "it",
                                    "iu",
                                    "ja",
                                    "jv",
                                    "ka",
                                    "kg",
                                    "ki",
                                    "kj",
                                    "kk",
                                    "kl",
                                    "km",
                                    "kn",
                                    "ko",
                                    "kr",
                                    "ks",
                                    "ku",
                                    "kv",
                                    "kw",
                                    "ky",
                                    "la",
                                    "lb",
                                    "lg",
                                    "li",
                                    "ln",
                                    "lo",
                                    "lt",
                                    "lu",
                                    "lv",
                                    "mg",
                                    "mh",
                                    "mi",
                                    "mk",
                                    "ml",
                                    "mn",
                                    "mr",
                                    "ms",
                                    "mt",
                                    "my",
                                    "na",
                                    "nb",
                                    "nd",
                                    "ne",
                                    "ng",
                                    "nl",
                                    "nn",
                                    "no",
                                    "nr",
                                    "nv",
                                    "ny",
                                    "oc",
                                    "oj",
                                    "om",
                                    "or",
                                    "os",
                                    "pa",
                                    "pi",
                                    "pl",
                                    "ps",
                                    "pt",
                                    "qu",
                                    "rm",
                                    "rn",
                                    "ro",
                                    "ru",
                                    "rw",
                                    "sa",
                                    "sc",
                                    "sd",
                                    "se",
                                    "sg",
                                    "si",
                                    "sk",
                                    "sl",
                                    "sm",
                                    "sn",
                                    "so",
                                    "sq",
                                    "sr",
                                    "ss",
                                    "st",
                                    "su",
                                    "sv",
                                    "sw",
                                    "ta",
                                    "te",
                                    "tg",
                                    "th",
                                    "ti",
                                    "tk",
                                    "tl",
                                    "tn",
                                    "to",
                                    "tr",
                                    "ts",
                                    "tt",
                                    "tw",
                                    "ty",
                                    "ug",
                                    "uk",
                                    "ur",
                                    "uz",
                                    "ve",
                                    "vi",
                                    "vo",
                                    "wa",
                                    "wo",
                                    "xh",
                                    "yi",
                                    "yo",
                                    "za",
                                    "zh",
                                    "zu"
                                  ]
                                }
                              ]
                            },
                            "localization": {
                              "description": "The seed translated into other languages, one per ISO 639-1 code, each keeping every `{{…}}` slot byte-exact and every `---` line. Kairon writes a language here the first time it drafts a lead in it; you may edit any entry and it is then used as written for every later lead in that language. Leave it out to let Kairon translate.",
                              "type": "object",
                              "properties": {
                                "seedHash": {
                                  "type": "string",
                                  "minLength": 1,
                                  "description": "Hash of the seed text these translations were made for. Kairon sets it; when the seed changes, the whole object goes stale and is rebuilt. Copy it through unchanged."
                                },
                                "seedLanguage": {
                                  "type": "string",
                                  "enum": [
                                    "aa",
                                    "ab",
                                    "ae",
                                    "af",
                                    "ak",
                                    "am",
                                    "an",
                                    "ar",
                                    "as",
                                    "av",
                                    "ay",
                                    "az",
                                    "ba",
                                    "be",
                                    "bg",
                                    "bh",
                                    "bi",
                                    "bm",
                                    "bn",
                                    "bo",
                                    "br",
                                    "bs",
                                    "ca",
                                    "ce",
                                    "ch",
                                    "co",
                                    "cr",
                                    "cs",
                                    "cu",
                                    "cv",
                                    "cy",
                                    "da",
                                    "de",
                                    "dv",
                                    "dz",
                                    "ee",
                                    "el",
                                    "en",
                                    "eo",
                                    "es",
                                    "et",
                                    "eu",
                                    "fa",
                                    "ff",
                                    "fi",
                                    "fj",
                                    "fo",
                                    "fr",
                                    "fy",
                                    "ga",
                                    "gd",
                                    "gl",
                                    "gn",
                                    "gu",
                                    "gv",
                                    "ha",
                                    "he",
                                    "hi",
                                    "ho",
                                    "hr",
                                    "ht",
                                    "hu",
                                    "hy",
                                    "hz",
                                    "ia",
                                    "id",
                                    "ie",
                                    "ig",
                                    "ii",
                                    "ik",
                                    "io",
                                    "is",
                                    "it",
                                    "iu",
                                    "ja",
                                    "jv",
                                    "ka",
                                    "kg",
                                    "ki",
                                    "kj",
                                    "kk",
                                    "kl",
                                    "km",
                                    "kn",
                                    "ko",
                                    "kr",
                                    "ks",
                                    "ku",
                                    "kv",
                                    "kw",
                                    "ky",
                                    "la",
                                    "lb",
                                    "lg",
                                    "li",
                                    "ln",
                                    "lo",
                                    "lt",
                                    "lu",
                                    "lv",
                                    "mg",
                                    "mh",
                                    "mi",
                                    "mk",
                                    "ml",
                                    "mn",
                                    "mr",
                                    "ms",
                                    "mt",
                                    "my",
                                    "na",
                                    "nb",
                                    "nd",
                                    "ne",
                                    "ng",
                                    "nl",
                                    "nn",
                                    "no",
                                    "nr",
                                    "nv",
                                    "ny",
                                    "oc",
                                    "oj",
                                    "om",
                                    "or",
                                    "os",
                                    "pa",
                                    "pi",
                                    "pl",
                                    "ps",
                                    "pt",
                                    "qu",
                                    "rm",
                                    "rn",
                                    "ro",
                                    "ru",
                                    "rw",
                                    "sa",
                                    "sc",
                                    "sd",
                                    "se",
                                    "sg",
                                    "si",
                                    "sk",
                                    "sl",
                                    "sm",
                                    "sn",
                                    "so",
                                    "sq",
                                    "sr",
                                    "ss",
                                    "st",
                                    "su",
                                    "sv",
                                    "sw",
                                    "ta",
                                    "te",
                                    "tg",
                                    "th",
                                    "ti",
                                    "tk",
                                    "tl",
                                    "tn",
                                    "to",
                                    "tr",
                                    "ts",
                                    "tt",
                                    "tw",
                                    "ty",
                                    "ug",
                                    "uk",
                                    "ur",
                                    "uz",
                                    "ve",
                                    "vi",
                                    "vo",
                                    "wa",
                                    "wo",
                                    "xh",
                                    "yi",
                                    "yo",
                                    "za",
                                    "zh",
                                    "zu"
                                  ],
                                  "description": "The ISO 639-1 code of the language the seed itself is written in, detected once by Kairon."
                                },
                                "translations": {
                                  "type": "object",
                                  "propertyNames": {
                                    "type": "string",
                                    "enum": [
                                      "aa",
                                      "ab",
                                      "ae",
                                      "af",
                                      "ak",
                                      "am",
                                      "an",
                                      "ar",
                                      "as",
                                      "av",
                                      "ay",
                                      "az",
                                      "ba",
                                      "be",
                                      "bg",
                                      "bh",
                                      "bi",
                                      "bm",
                                      "bn",
                                      "bo",
                                      "br",
                                      "bs",
                                      "ca",
                                      "ce",
                                      "ch",
                                      "co",
                                      "cr",
                                      "cs",
                                      "cu",
                                      "cv",
                                      "cy",
                                      "da",
                                      "de",
                                      "dv",
                                      "dz",
                                      "ee",
                                      "el",
                                      "en",
                                      "eo",
                                      "es",
                                      "et",
                                      "eu",
                                      "fa",
                                      "ff",
                                      "fi",
                                      "fj",
                                      "fo",
                                      "fr",
                                      "fy",
                                      "ga",
                                      "gd",
                                      "gl",
                                      "gn",
                                      "gu",
                                      "gv",
                                      "ha",
                                      "he",
                                      "hi",
                                      "ho",
                                      "hr",
                                      "ht",
                                      "hu",
                                      "hy",
                                      "hz",
                                      "ia",
                                      "id",
                                      "ie",
                                      "ig",
                                      "ii",
                                      "ik",
                                      "io",
                                      "is",
                                      "it",
                                      "iu",
                                      "ja",
                                      "jv",
                                      "ka",
                                      "kg",
                                      "ki",
                                      "kj",
                                      "kk",
                                      "kl",
                                      "km",
                                      "kn",
                                      "ko",
                                      "kr",
                                      "ks",
                                      "ku",
                                      "kv",
                                      "kw",
                                      "ky",
                                      "la",
                                      "lb",
                                      "lg",
                                      "li",
                                      "ln",
                                      "lo",
                                      "lt",
                                      "lu",
                                      "lv",
                                      "mg",
                                      "mh",
                                      "mi",
                                      "mk",
                                      "ml",
                                      "mn",
                                      "mr",
                                      "ms",
                                      "mt",
                                      "my",
                                      "na",
                                      "nb",
                                      "nd",
                                      "ne",
                                      "ng",
                                      "nl",
                                      "nn",
                                      "no",
                                      "nr",
                                      "nv",
                                      "ny",
                                      "oc",
                                      "oj",
                                      "om",
                                      "or",
                                      "os",
                                      "pa",
                                      "pi",
                                      "pl",
                                      "ps",
                                      "pt",
                                      "qu",
                                      "rm",
                                      "rn",
                                      "ro",
                                      "ru",
                                      "rw",
                                      "sa",
                                      "sc",
                                      "sd",
                                      "se",
                                      "sg",
                                      "si",
                                      "sk",
                                      "sl",
                                      "sm",
                                      "sn",
                                      "so",
                                      "sq",
                                      "sr",
                                      "ss",
                                      "st",
                                      "su",
                                      "sv",
                                      "sw",
                                      "ta",
                                      "te",
                                      "tg",
                                      "th",
                                      "ti",
                                      "tk",
                                      "tl",
                                      "tn",
                                      "to",
                                      "tr",
                                      "ts",
                                      "tt",
                                      "tw",
                                      "ty",
                                      "ug",
                                      "uk",
                                      "ur",
                                      "uz",
                                      "ve",
                                      "vi",
                                      "vo",
                                      "wa",
                                      "wo",
                                      "xh",
                                      "yi",
                                      "yo",
                                      "za",
                                      "zh",
                                      "zu"
                                    ]
                                  },
                                  "additionalProperties": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "description": "One translated seed per ISO 639-1 code. Each must keep every `{{…}}` slot byte-exact and in order, and the same number of `---` lines, or publish refuses it."
                                }
                              },
                              "required": [
                                "seedHash",
                                "seedLanguage",
                                "translations"
                              ]
                            },
                            "reviewBeforeSend": {
                              "description": "Default false. True holds every send this step drafts for the operator to approve, even when the campaign is on autopilot. Nothing is scheduled until they approve it.",
                              "type": "boolean"
                            },
                            "verifyBeforeSend": {
                              "description": "DEPRECATED, and accepted only so campaigns published before the rename still parse. Set `reviewBeforeSend` instead; if both are present the new one wins.",
                              "type": "boolean"
                            },
                            "adaptToCulture": {
                              "description": "Retired: accepted for old steps and ignored. Do not send it.",
                              "type": "boolean"
                            },
                            "condition": {
                              "description": "Only send when one of their own recent posts meets this test - e.g. \"a new job or a promotion\". The newest post that meets it becomes what the message refers to; a post that is bad news for its author never meets it. When none does, the step is SKIPPED and the lead moves on; nothing is sent. Omit it and the message sends as usual.",
                              "type": "string",
                              "maxLength": 1000
                            },
                            "maxPostAgeDays": {
                              "description": "With a condition: Only consider their own posts from the last this-many days, 1-90. Defaults to 90. In a loop, the step also only considers posts published after it last acted on a post, or last checked and found none that met its condition.",
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 90
                            },
                            "bySender": {
                              "description": "Optional per-sender text, keyed by the sender's user id (`seat_list` → `ownerUserId`). A lead sent by that sender gets this version; every other sender gets the step's own `body`. Same draft mode and language as the step.",
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {
                                "type": "object",
                                "properties": {
                                  "body": {
                                    "type": "string",
                                    "maxLength": 8000,
                                    "description": "This sender's message, in place of the step's `body`. In `ai_template` this seed is authoritative: the writer reproduces it word for word and adds nothing, so EVERY per-lead detail must sit inside a `{{...}}` slot written here. A seed with no slots sends the identical message to every lead. A slot holds a description of the fact to write there, phrased as a clause — e.g. `{{one short clause, 8-14 words, on what their company does}}`."
                                  },
                                  "instruction": {
                                    "description": "This sender's note, in place of the step's `instruction`. Optional note on tone and register, layered on the seed — NOT where the copy comes from. It reaches the writer AFTER the rule that says to keep the seed as written, so anything it asks for that the seed has no `{{slot}}` for is silently ignored: \"personalize the opening with their company\" does nothing unless a slot sits there. Write the copy in the seed.",
                                    "type": "string",
                                    "maxLength": 4000
                                  },
                                  "localization": {
                                    "description": "The seed translated into other languages, one per ISO 639-1 code, each keeping every `{{…}}` slot byte-exact and every `---` line. Kairon writes a language here the first time it drafts a lead in it; you may edit any entry and it is then used as written for every later lead in that language. Leave it out to let Kairon translate.",
                                    "type": "object",
                                    "properties": {
                                      "seedHash": {
                                        "type": "string",
                                        "minLength": 1,
                                        "description": "Hash of the seed text these translations were made for. Kairon sets it; when the seed changes, the whole object goes stale and is rebuilt. Copy it through unchanged."
                                      },
                                      "seedLanguage": {
                                        "type": "string",
                                        "enum": [
                                          "aa",
                                          "ab",
                                          "ae",
                                          "af",
                                          "ak",
                                          "am",
                                          "an",
                                          "ar",
                                          "as",
                                          "av",
                                          "ay",
                                          "az",
                                          "ba",
                                          "be",
                                          "bg",
                                          "bh",
                                          "bi",
                                          "bm",
                                          "bn",
                                          "bo",
                                          "br",
                                          "bs",
                                          "ca",
                                          "ce",
                                          "ch",
                                          "co",
                                          "cr",
                                          "cs",
                                          "cu",
                                          "cv",
                                          "cy",
                                          "da",
                                          "de",
                                          "dv",
                                          "dz",
                                          "ee",
                                          "el",
                                          "en",
                                          "eo",
                                          "es",
                                          "et",
                                          "eu",
                                          "fa",
                                          "ff",
                                          "fi",
                                          "fj",
                                          "fo",
                                          "fr",
                                          "fy",
                                          "ga",
                                          "gd",
                                          "gl",
                                          "gn",
                                          "gu",
                                          "gv",
                                          "ha",
                                          "he",
                                          "hi",
                                          "ho",
                                          "hr",
                                          "ht",
                                          "hu",
                                          "hy",
                                          "hz",
                                          "ia",
                                          "id",
                                          "ie",
                                          "ig",
                                          "ii",
                                          "ik",
                                          "io",
                                          "is",
                                          "it",
                                          "iu",
                                          "ja",
                                          "jv",
                                          "ka",
                                          "kg",
                                          "ki",
                                          "kj",
                                          "kk",
                                          "kl",
                                          "km",
                                          "kn",
                                          "ko",
                                          "kr",
                                          "ks",
                                          "ku",
                                          "kv",
                                          "kw",
                                          "ky",
                                          "la",
                                          "lb",
                                          "lg",
                                          "li",
                                          "ln",
                                          "lo",
                                          "lt",
                                          "lu",
                                          "lv",
                                          "mg",
                                          "mh",
                                          "mi",
                                          "mk",
                                          "ml",
                                          "mn",
                                          "mr",
                                          "ms",
                                          "mt",
                                          "my",
                                          "na",
                                          "nb",
                                          "nd",
                                          "ne",
                                          "ng",
                                          "nl",
                                          "nn",
                                          "no",
                                          "nr",
                                          "nv",
                                          "ny",
                                          "oc",
                                          "oj",
                                          "om",
                                          "or",
                                          "os",
                                          "pa",
                                          "pi",
                                          "pl",
                                          "ps",
                                          "pt",
                                          "qu",
                                          "rm",
                                          "rn",
                                          "ro",
                                          "ru",
                                          "rw",
                                          "sa",
                                          "sc",
                                          "sd",
                                          "se",
                                          "sg",
                                          "si",
                                          "sk",
                                          "sl",
                                          "sm",
                                          "sn",
                                          "so",
                                          "sq",
                                          "sr",
                                          "ss",
                                          "st",
                                          "su",
                                          "sv",
                                          "sw",
                                          "ta",
                                          "te",
                                          "tg",
                                          "th",
                                          "ti",
                                          "tk",
                                          "tl",
                                          "tn",
                                          "to",
                                          "tr",
                                          "ts",
                                          "tt",
                                          "tw",
                                          "ty",
                                          "ug",
                                          "uk",
                                          "ur",
                                          "uz",
                                          "ve",
                                          "vi",
                                          "vo",
                                          "wa",
                                          "wo",
                                          "xh",
                                          "yi",
                                          "yo",
                                          "za",
                                          "zh",
                                          "zu"
                                        ],
                                        "description": "The ISO 639-1 code of the language the seed itself is written in, detected once by Kairon."
                                      },
                                      "translations": {
                                        "type": "object",
                                        "propertyNames": {
                                          "type": "string",
                                          "enum": [
                                            "aa",
                                            "ab",
                                            "ae",
                                            "af",
                                            "ak",
                                            "am",
                                            "an",
                                            "ar",
                                            "as",
                                            "av",
                                            "ay",
                                            "az",
                                            "ba",
                                            "be",
                                            "bg",
                                            "bh",
                                            "bi",
                                            "bm",
                                            "bn",
                                            "bo",
                                            "br",
                                            "bs",
                                            "ca",
                                            "ce",
                                            "ch",
                                            "co",
                                            "cr",
                                            "cs",
                                            "cu",
                                            "cv",
                                            "cy",
                                            "da",
                                            "de",
                                            "dv",
                                            "dz",
                                            "ee",
                                            "el",
                                            "en",
                                            "eo",
                                            "es",
                                            "et",
                                            "eu",
                                            "fa",
                                            "ff",
                                            "fi",
                                            "fj",
                                            "fo",
                                            "fr",
                                            "fy",
                                            "ga",
                                            "gd",
                                            "gl",
                                            "gn",
                                            "gu",
                                            "gv",
                                            "ha",
                                            "he",
                                            "hi",
                                            "ho",
                                            "hr",
                                            "ht",
                                            "hu",
                                            "hy",
                                            "hz",
                                            "ia",
                                            "id",
                                            "ie",
                                            "ig",
                                            "ii",
                                            "ik",
                                            "io",
                                            "is",
                                            "it",
                                            "iu",
                                            "ja",
                                            "jv",
                                            "ka",
                                            "kg",
                                            "ki",
                                            "kj",
                                            "kk",
                                            "kl",
                                            "km",
                                            "kn",
                                            "ko",
                                            "kr",
                                            "ks",
                                            "ku",
                                            "kv",
                                            "kw",
                                            "ky",
                                            "la",
                                            "lb",
                                            "lg",
                                            "li",
                                            "ln",
                                            "lo",
                                            "lt",
                                            "lu",
                                            "lv",
                                            "mg",
                                            "mh",
                                            "mi",
                                            "mk",
                                            "ml",
                                            "mn",
                                            "mr",
                                            "ms",
                                            "mt",
                                            "my",
                                            "na",
                                            "nb",
                                            "nd",
                                            "ne",
                                            "ng",
                                            "nl",
                                            "nn",
                                            "no",
                                            "nr",
                                            "nv",
                                            "ny",
                                            "oc",
                                            "oj",
                                            "om",
                                            "or",
                                            "os",
                                            "pa",
                                            "pi",
                                            "pl",
                                            "ps",
                                            "pt",
                                            "qu",
                                            "rm",
                                            "rn",
                                            "ro",
                                            "ru",
                                            "rw",
                                            "sa",
                                            "sc",
                                            "sd",
                                            "se",
                                            "sg",
                                            "si",
                                            "sk",
                                            "sl",
                                            "sm",
                                            "sn",
                                            "so",
                                            "sq",
                                            "sr",
                                            "ss",
                                            "st",
                                            "su",
                                            "sv",
                                            "sw",
                                            "ta",
                                            "te",
                                            "tg",
                                            "th",
                                            "ti",
                                            "tk",
                                            "tl",
                                            "tn",
                                            "to",
                                            "tr",
                                            "ts",
                                            "tt",
                                            "tw",
                                            "ty",
                                            "ug",
                                            "uk",
                                            "ur",
                                            "uz",
                                            "ve",
                                            "vi",
                                            "vo",
                                            "wa",
                                            "wo",
                                            "xh",
                                            "yi",
                                            "yo",
                                            "za",
                                            "zh",
                                            "zu"
                                          ]
                                        },
                                        "additionalProperties": {
                                          "type": "string",
                                          "minLength": 1
                                        },
                                        "description": "One translated seed per ISO 639-1 code. Each must keep every `{{…}}` slot byte-exact and in order, and the same number of `---` lines, or publish refuses it."
                                      }
                                    },
                                    "required": [
                                      "seedHash",
                                      "seedLanguage",
                                      "translations"
                                    ]
                                  }
                                },
                                "required": [
                                  "body"
                                ]
                              }
                            },
                            "attachments": {
                              "description": "Files sent with this step to every lead it reaches: images and PDFs, plus one voice note and one video on a LinkedIn message. Each is `{ fileName, mimeType, url }` (https) or `{ fileName, mimeType, dataBase64 }`, as `message_send` takes; a file `campaign_get` returned may be sent back as is.",
                              "maxItems": 5,
                              "type": "array",
                              "items": {
                                "anyOf": [
                                  {
                                    "type": "object",
                                    "properties": {
                                      "storageKey": {
                                        "type": "string",
                                        "maxLength": 512,
                                        "pattern": "^step-attachments\\/.*"
                                      },
                                      "fileName": {
                                        "type": "string",
                                        "minLength": 1,
                                        "maxLength": 255
                                      },
                                      "mimeType": {
                                        "type": "string",
                                        "minLength": 1,
                                        "maxLength": 255
                                      },
                                      "byteSize": {
                                        "type": "integer",
                                        "exclusiveMinimum": 0,
                                        "maximum": 9007199254740991
                                      }
                                    },
                                    "required": [
                                      "storageKey",
                                      "fileName",
                                      "mimeType",
                                      "byteSize"
                                    ],
                                    "additionalProperties": false
                                  },
                                  {
                                    "anyOf": [
                                      {
                                        "type": "object",
                                        "properties": {
                                          "fileName": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file name, with its extension — what the recipient sees."
                                          },
                                          "mimeType": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                                          },
                                          "dataBase64": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 4194304,
                                            "description": "The file itself, base64-encoded. Use this OR `url`, never both."
                                          }
                                        },
                                        "required": [
                                          "fileName",
                                          "mimeType",
                                          "dataBase64"
                                        ],
                                        "additionalProperties": false
                                      },
                                      {
                                        "type": "object",
                                        "properties": {
                                          "fileName": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file name, with its extension — what the recipient sees."
                                          },
                                          "mimeType": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                                          },
                                          "url": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 2048,
                                            "format": "uri",
                                            "description": "An https address we fetch server-side. Use this OR `dataBase64`, never both. Reach for it when you cannot produce base64."
                                          }
                                        },
                                        "required": [
                                          "fileName",
                                          "mimeType",
                                          "url"
                                        ],
                                        "additionalProperties": false
                                      }
                                    ]
                                  }
                                ]
                              }
                            },
                            "attachmentsBySender": {
                              "description": "Optional per-sender files, keyed by the sender's user id (`seat_list` → `ownerUserId`): that sender's leads get these files instead of the step's `attachments` (an empty list sends none). Read-only notes — add files through `attachments` first.",
                              "type": "object",
                              "propertyNames": {
                                "type": "string"
                              },
                              "additionalProperties": {
                                "maxItems": 5,
                                "type": "array",
                                "items": {
                                  "type": "object",
                                  "properties": {
                                    "storageKey": {
                                      "type": "string",
                                      "maxLength": 512,
                                      "pattern": "^step-attachments\\/.*"
                                    },
                                    "fileName": {
                                      "type": "string",
                                      "minLength": 1,
                                      "maxLength": 255
                                    },
                                    "mimeType": {
                                      "type": "string",
                                      "minLength": 1,
                                      "maxLength": 255
                                    },
                                    "byteSize": {
                                      "type": "integer",
                                      "exclusiveMinimum": 0,
                                      "maximum": 9007199254740991
                                    }
                                  },
                                  "required": [
                                    "storageKey",
                                    "fileName",
                                    "mimeType",
                                    "byteSize"
                                  ],
                                  "additionalProperties": false
                                }
                              }
                            }
                          },
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "send_inmail",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "subject": {
                              "default": "",
                              "description": "Required. The InMail subject line — fixed text you write, never AI-written, in every draftMode. The only tokens allowed are template variables (firstName, lastName, companyName, jobTitle, location, headline, companyIndustry); a described slot or a per-lead {{@slot}} is refused. Empty is refused at save and at publish.",
                              "type": "string",
                              "maxLength": 200
                            },
                            "body": {
                              "default": "",
                              "description": "The InMail body. In `ai_template` this seed is authoritative: the writer reproduces it word for word and adds nothing, so EVERY per-lead detail must sit inside a `{{...}}` slot written here. A seed with no slots sends the identical message to every lead. A slot holds a description of the fact to write there, phrased as a clause — e.g. `{{one short clause, 8-14 words, on what their company does}}`. 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.",
                              "type": "string",
                              "maxLength": 8000
                            },
                            "draftMode": {
                              "description": "How the copy is produced. `template`: sent as written — only allowlisted `{{variables}}` and per-lead `{{@slots}}` are substituted, no AI. `ai_template`: the AI personalizes YOUR seed, filling its `{{described slots}}` and nothing else. Absent means `template`.",
                              "type": "string",
                              "enum": [
                                "template",
                                "ai_template"
                              ]
                            },
                            "instruction": {
                              "description": "Optional note on tone and register, layered on the seed — NOT where the copy comes from. It reaches the writer AFTER the rule that says to keep the seed as written, so anything it asks for that the seed has no `{{slot}}` for is silently ignored: \"personalize the opening with their company\" does nothing unless a slot sits there. Write the copy in the seed.",
                              "type": "string",
                              "maxLength": 4000
                            },
                            "language": {
                              "description": "Force every message on this step into one language, or set 'auto' so each lead is written to in their OWN language, translating your seed faithfully. OMIT IT and the step uses the default language the sender chose in their settings, or 'auto' when they chose none — which is usually what you want. This rule outranks the seed's original language.",
                              "anyOf": [
                                {
                                  "type": "string",
                                  "const": "auto"
                                },
                                {
                                  "type": "string",
                                  "enum": [
                                    "aa",
                                    "ab",
                                    "ae",
                                    "af",
                                    "ak",
                                    "am",
                                    "an",
                                    "ar",
                                    "as",
                                    "av",
                                    "ay",
                                    "az",
                                    "ba",
                                    "be",
                                    "bg",
                                    "bh",
                                    "bi",
                                    "bm",
                                    "bn",
                                    "bo",
                                    "br",
                                    "bs",
                                    "ca",
                                    "ce",
                                    "ch",
                                    "co",
                                    "cr",
                                    "cs",
                                    "cu",
                                    "cv",
                                    "cy",
                                    "da",
                                    "de",
                                    "dv",
                                    "dz",
                                    "ee",
                                    "el",
                                    "en",
                                    "eo",
                                    "es",
                                    "et",
                                    "eu",
                                    "fa",
                                    "ff",
                                    "fi",
                                    "fj",
                                    "fo",
                                    "fr",
                                    "fy",
                                    "ga",
                                    "gd",
                                    "gl",
                                    "gn",
                                    "gu",
                                    "gv",
                                    "ha",
                                    "he",
                                    "hi",
                                    "ho",
                                    "hr",
                                    "ht",
                                    "hu",
                                    "hy",
                                    "hz",
                                    "ia",
                                    "id",
                                    "ie",
                                    "ig",
                                    "ii",
                                    "ik",
                                    "io",
                                    "is",
                                    "it",
                                    "iu",
                                    "ja",
                                    "jv",
                                    "ka",
                                    "kg",
                                    "ki",
                                    "kj",
                                    "kk",
                                    "kl",
                                    "km",
                                    "kn",
                                    "ko",
                                    "kr",
                                    "ks",
                                    "ku",
                                    "kv",
                                    "kw",
                                    "ky",
                                    "la",
                                    "lb",
                                    "lg",
                                    "li",
                                    "ln",
                                    "lo",
                                    "lt",
                                    "lu",
                                    "lv",
                                    "mg",
                                    "mh",
                                    "mi",
                                    "mk",
                                    "ml",
                                    "mn",
                                    "mr",
                                    "ms",
                                    "mt",
                                    "my",
                                    "na",
                                    "nb",
                                    "nd",
                                    "ne",
                                    "ng",
                                    "nl",
                                    "nn",
                                    "no",
                                    "nr",
                                    "nv",
                                    "ny",
                                    "oc",
                                    "oj",
                                    "om",
                                    "or",
                                    "os",
                                    "pa",
                                    "pi",
                                    "pl",
                                    "ps",
                                    "pt",
                                    "qu",
                                    "rm",
                                    "rn",
                                    "ro",
                                    "ru",
                                    "rw",
                                    "sa",
                                    "sc",
                                    "sd",
                                    "se",
                                    "sg",
                                    "si",
                                    "sk",
                                    "sl",
                                    "sm",
                                    "sn",
                                    "so",
                                    "sq",
                                    "sr",
                                    "ss",
                                    "st",
                                    "su",
                                    "sv",
                                    "sw",
                                    "ta",
                                    "te",
                                    "tg",
                                    "th",
                                    "ti",
                                    "tk",
                                    "tl",
                                    "tn",
                                    "to",
                                    "tr",
                                    "ts",
                                    "tt",
                                    "tw",
                                    "ty",
                                    "ug",
                                    "uk",
                                    "ur",
                                    "uz",
                                    "ve",
                                    "vi",
                                    "vo",
                                    "wa",
                                    "wo",
                                    "xh",
                                    "yi",
                                    "yo",
                                    "za",
                                    "zh",
                                    "zu"
                                  ]
                                }
                              ]
                            },
                            "localization": {
                              "description": "The seed translated into other languages, one per ISO 639-1 code, each keeping every `{{…}}` slot byte-exact and every `---` line. Kairon writes a language here the first time it drafts a lead in it; you may edit any entry and it is then used as written for every later lead in that language. Leave it out to let Kairon translate.",
                              "type": "object",
                              "properties": {
                                "seedHash": {
                                  "type": "string",
                                  "minLength": 1,
                                  "description": "Hash of the seed text these translations were made for. Kairon sets it; when the seed changes, the whole object goes stale and is rebuilt. Copy it through unchanged."
                                },
                                "seedLanguage": {
                                  "type": "string",
                                  "enum": [
                                    "aa",
                                    "ab",
                                    "ae",
                                    "af",
                                    "ak",
                                    "am",
                                    "an",
                                    "ar",
                                    "as",
                                    "av",
                                    "ay",
                                    "az",
                                    "ba",
                                    "be",
                                    "bg",
                                    "bh",
                                    "bi",
                                    "bm",
                                    "bn",
                                    "bo",
                                    "br",
                                    "bs",
                                    "ca",
                                    "ce",
                                    "ch",
                                    "co",
                                    "cr",
                                    "cs",
                                    "cu",
                                    "cv",
                                    "cy",
                                    "da",
                                    "de",
                                    "dv",
                                    "dz",
                                    "ee",
                                    "el",
                                    "en",
                                    "eo",
                                    "es",
                                    "et",
                                    "eu",
                                    "fa",
                                    "ff",
                                    "fi",
                                    "fj",
                                    "fo",
                                    "fr",
                                    "fy",
                                    "ga",
                                    "gd",
                                    "gl",
                                    "gn",
                                    "gu",
                                    "gv",
                                    "ha",
                                    "he",
                                    "hi",
                                    "ho",
                                    "hr",
                                    "ht",
                                    "hu",
                                    "hy",
                                    "hz",
                                    "ia",
                                    "id",
                                    "ie",
                                    "ig",
                                    "ii",
                                    "ik",
                                    "io",
                                    "is",
                                    "it",
                                    "iu",
                                    "ja",
                                    "jv",
                                    "ka",
                                    "kg",
                                    "ki",
                                    "kj",
                                    "kk",
                                    "kl",
                                    "km",
                                    "kn",
                                    "ko",
                                    "kr",
                                    "ks",
                                    "ku",
                                    "kv",
                                    "kw",
                                    "ky",
                                    "la",
                                    "lb",
                                    "lg",
                                    "li",
                                    "ln",
                                    "lo",
                                    "lt",
                                    "lu",
                                    "lv",
                                    "mg",
                                    "mh",
                                    "mi",
                                    "mk",
                                    "ml",
                                    "mn",
                                    "mr",
                                    "ms",
                                    "mt",
                                    "my",
                                    "na",
                                    "nb",
                                    "nd",
                                    "ne",
                                    "ng",
                                    "nl",
                                    "nn",
                                    "no",
                                    "nr",
                                    "nv",
                                    "ny",
                                    "oc",
                                    "oj",
                                    "om",
                                    "or",
                                    "os",
                                    "pa",
                                    "pi",
                                    "pl",
                                    "ps",
                                    "pt",
                                    "qu",
                                    "rm",
                                    "rn",
                                    "ro",
                                    "ru",
                                    "rw",
                                    "sa",
                                    "sc",
                                    "sd",
                                    "se",
                                    "sg",
                                    "si",
                                    "sk",
                                    "sl",
                                    "sm",
                                    "sn",
                                    "so",
                                    "sq",
                                    "sr",
                                    "ss",
                                    "st",
                                    "su",
                                    "sv",
                                    "sw",
                                    "ta",
                                    "te",
                                    "tg",
                                    "th",
                                    "ti",
                                    "tk",
                                    "tl",
                                    "tn",
                                    "to",
                                    "tr",
                                    "ts",
                                    "tt",
                                    "tw",
                                    "ty",
                                    "ug",
                                    "uk",
                                    "ur",
                                    "uz",
                                    "ve",
                                    "vi",
                                    "vo",
                                    "wa",
                                    "wo",
                                    "xh",
                                    "yi",
                                    "yo",
                                    "za",
                                    "zh",
                                    "zu"
                                  ],
                                  "description": "The ISO 639-1 code of the language the seed itself is written in, detected once by Kairon."
                                },
                                "translations": {
                                  "type": "object",
                                  "propertyNames": {
                                    "type": "string",
                                    "enum": [
                                      "aa",
                                      "ab",
                                      "ae",
                                      "af",
                                      "ak",
                                      "am",
                                      "an",
                                      "ar",
                                      "as",
                                      "av",
                                      "ay",
                                      "az",
                                      "ba",
                                      "be",
                                      "bg",
                                      "bh",
                                      "bi",
                                      "bm",
                                      "bn",
                                      "bo",
                                      "br",
                                      "bs",
                                      "ca",
                                      "ce",
                                      "ch",
                                      "co",
                                      "cr",
                                      "cs",
                                      "cu",
                                      "cv",
                                      "cy",
                                      "da",
                                      "de",
                                      "dv",
                                      "dz",
                                      "ee",
                                      "el",
                                      "en",
                                      "eo",
                                      "es",
                                      "et",
                                      "eu",
                                      "fa",
                                      "ff",
                                      "fi",
                                      "fj",
                                      "fo",
                                      "fr",
                                      "fy",
                                      "ga",
                                      "gd",
                                      "gl",
                                      "gn",
                                      "gu",
                                      "gv",
                                      "ha",
                                      "he",
                                      "hi",
                                      "ho",
                                      "hr",
                                      "ht",
                                      "hu",
                                      "hy",
                                      "hz",
                                      "ia",
                                      "id",
                                      "ie",
                                      "ig",
                                      "ii",
                                      "ik",
                                      "io",
                                      "is",
                                      "it",
                                      "iu",
                                      "ja",
                                      "jv",
                                      "ka",
                                      "kg",
                                      "ki",
                                      "kj",
                                      "kk",
                                      "kl",
                                      "km",
                                      "kn",
                                      "ko",
                                      "kr",
                                      "ks",
                                      "ku",
                                      "kv",
                                      "kw",
                                      "ky",
                                      "la",
                                      "lb",
                                      "lg",
                                      "li",
                                      "ln",
                                      "lo",
                                      "lt",
                                      "lu",
                                      "lv",
                                      "mg",
                                      "mh",
                                      "mi",
                                      "mk",
                                      "ml",
                                      "mn",
                                      "mr",
                                      "ms",
                                      "mt",
                                      "my",
                                      "na",
                                      "nb",
                                      "nd",
                                      "ne",
                                      "ng",
                                      "nl",
                                      "nn",
                                      "no",
                                      "nr",
                                      "nv",
                                      "ny",
                                      "oc",
                                      "oj",
                                      "om",
                                      "or",
                                      "os",
                                      "pa",
                                      "pi",
                                      "pl",
                                      "ps",
                                      "pt",
                                      "qu",
                                      "rm",
                                      "rn",
                                      "ro",
                                      "ru",
                                      "rw",
                                      "sa",
                                      "sc",
                                      "sd",
                                      "se",
                                      "sg",
                                      "si",
                                      "sk",
                                      "sl",
                                      "sm",
                                      "sn",
                                      "so",
                                      "sq",
                                      "sr",
                                      "ss",
                                      "st",
                                      "su",
                                      "sv",
                                      "sw",
                                      "ta",
                                      "te",
                                      "tg",
                                      "th",
                                      "ti",
                                      "tk",
                                      "tl",
                                      "tn",
                                      "to",
                                      "tr",
                                      "ts",
                                      "tt",
                                      "tw",
                                      "ty",
                                      "ug",
                                      "uk",
                                      "ur",
                                      "uz",
                                      "ve",
                                      "vi",
                                      "vo",
                                      "wa",
                                      "wo",
                                      "xh",
                                      "yi",
                                      "yo",
                                      "za",
                                      "zh",
                                      "zu"
                                    ]
                                  },
                                  "additionalProperties": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "description": "One translated seed per ISO 639-1 code. Each must keep every `{{…}}` slot byte-exact and in order, and the same number of `---` lines, or publish refuses it."
                                }
                              },
                              "required": [
                                "seedHash",
                                "seedLanguage",
                                "translations"
                              ]
                            },
                            "reviewBeforeSend": {
                              "description": "Default false. True holds every send this step drafts for the operator to approve, even when the campaign is on autopilot. Nothing is scheduled until they approve it.",
                              "type": "boolean"
                            },
                            "verifyBeforeSend": {
                              "description": "DEPRECATED, and accepted only so campaigns published before the rename still parse. Set `reviewBeforeSend` instead; if both are present the new one wins.",
                              "type": "boolean"
                            },
                            "adaptToCulture": {
                              "description": "Retired: accepted for old steps and ignored. Do not send it.",
                              "type": "boolean"
                            },
                            "attachments": {
                              "description": "Files sent with this step to every lead it reaches: images and PDFs, plus one voice note and one video on a LinkedIn message. Each is `{ fileName, mimeType, url }` (https) or `{ fileName, mimeType, dataBase64 }`, as `message_send` takes; a file `campaign_get` returned may be sent back as is.",
                              "maxItems": 5,
                              "type": "array",
                              "items": {
                                "anyOf": [
                                  {
                                    "type": "object",
                                    "properties": {
                                      "storageKey": {
                                        "type": "string",
                                        "maxLength": 512,
                                        "pattern": "^step-attachments\\/.*"
                                      },
                                      "fileName": {
                                        "type": "string",
                                        "minLength": 1,
                                        "maxLength": 255
                                      },
                                      "mimeType": {
                                        "type": "string",
                                        "minLength": 1,
                                        "maxLength": 255
                                      },
                                      "byteSize": {
                                        "type": "integer",
                                        "exclusiveMinimum": 0,
                                        "maximum": 9007199254740991
                                      }
                                    },
                                    "required": [
                                      "storageKey",
                                      "fileName",
                                      "mimeType",
                                      "byteSize"
                                    ],
                                    "additionalProperties": false
                                  },
                                  {
                                    "anyOf": [
                                      {
                                        "type": "object",
                                        "properties": {
                                          "fileName": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file name, with its extension — what the recipient sees."
                                          },
                                          "mimeType": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                                          },
                                          "dataBase64": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 4194304,
                                            "description": "The file itself, base64-encoded. Use this OR `url`, never both."
                                          }
                                        },
                                        "required": [
                                          "fileName",
                                          "mimeType",
                                          "dataBase64"
                                        ],
                                        "additionalProperties": false
                                      },
                                      {
                                        "type": "object",
                                        "properties": {
                                          "fileName": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file name, with its extension — what the recipient sees."
                                          },
                                          "mimeType": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                                          },
                                          "url": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 2048,
                                            "format": "uri",
                                            "description": "An https address we fetch server-side. Use this OR `dataBase64`, never both. Reach for it when you cannot produce base64."
                                          }
                                        },
                                        "required": [
                                          "fileName",
                                          "mimeType",
                                          "url"
                                        ],
                                        "additionalProperties": false
                                      }
                                    ]
                                  }
                                ]
                              }
                            }
                          },
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "like_last_post",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "reaction": {
                              "default": "like",
                              "description": "Which reaction to leave on the lead's latest own post. 'auto' reads the post and picks the fitting one of the six; a named value always leaves that one. Defaults to 'like', which is what every campaign saved before this field existed does.",
                              "type": "string",
                              "enum": [
                                "like",
                                "celebrate",
                                "support",
                                "love",
                                "insightful",
                                "funny",
                                "auto"
                              ]
                            },
                            "maxPostAgeDays": {
                              "description": "Only react when the lead's latest own post is at most this many days old, 1-3650. When it is older, the step is SKIPPED and the lead moves on; nothing is liked. Defaults to 90 — a like on a years-old post, right before an invite, reads as automation.",
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 3650
                            }
                          },
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "visit_profile",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {},
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "follow_profile",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {},
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "remove_relation",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {},
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "comment_on_post",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "instruction": {
                              "description": "What the comment should say. UNLIKE a send step, this IS the whole brief: a comment has no operator template to personalize, so the guidance carries the intent alone.",
                              "type": "string",
                              "maxLength": 4000
                            },
                            "condition": {
                              "description": "Only comment when one of their own recent posts meets this test — e.g. \"the post is about AI\". The newest post that meets it is the one commented on; a post that is bad news for its author never meets it. When none does, the step is SKIPPED and the lead moves on to the next step; nothing is posted. Omit it and every eligible post qualifies.",
                              "type": "string",
                              "maxLength": 1000
                            },
                            "maxLength": {
                              "description": "Character ceiling for the comment, up to 1250. Omit and the cap is used.",
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 1250
                            },
                            "maxPostAgeDays": {
                              "description": "Only consider their own posts from the last this-many days, 1-90. Defaults to 90. In a loop, the step also only considers posts published after it last acted on a post, or last checked and found none that met its condition.",
                              "type": "integer",
                              "minimum": 1,
                              "maximum": 90
                            },
                            "language": {
                              "description": "Force every message on this step into one language, or set 'auto' so each lead is written to in their OWN language, translating your seed faithfully. OMIT IT and the step uses the default language the sender chose in their settings, or 'auto' when they chose none — which is usually what you want. This rule outranks the seed's original language.",
                              "anyOf": [
                                {
                                  "type": "string",
                                  "const": "auto"
                                },
                                {
                                  "type": "string",
                                  "enum": [
                                    "aa",
                                    "ab",
                                    "ae",
                                    "af",
                                    "ak",
                                    "am",
                                    "an",
                                    "ar",
                                    "as",
                                    "av",
                                    "ay",
                                    "az",
                                    "ba",
                                    "be",
                                    "bg",
                                    "bh",
                                    "bi",
                                    "bm",
                                    "bn",
                                    "bo",
                                    "br",
                                    "bs",
                                    "ca",
                                    "ce",
                                    "ch",
                                    "co",
                                    "cr",
                                    "cs",
                                    "cu",
                                    "cv",
                                    "cy",
                                    "da",
                                    "de",
                                    "dv",
                                    "dz",
                                    "ee",
                                    "el",
                                    "en",
                                    "eo",
                                    "es",
                                    "et",
                                    "eu",
                                    "fa",
                                    "ff",
                                    "fi",
                                    "fj",
                                    "fo",
                                    "fr",
                                    "fy",
                                    "ga",
                                    "gd",
                                    "gl",
                                    "gn",
                                    "gu",
                                    "gv",
                                    "ha",
                                    "he",
                                    "hi",
                                    "ho",
                                    "hr",
                                    "ht",
                                    "hu",
                                    "hy",
                                    "hz",
                                    "ia",
                                    "id",
                                    "ie",
                                    "ig",
                                    "ii",
                                    "ik",
                                    "io",
                                    "is",
                                    "it",
                                    "iu",
                                    "ja",
                                    "jv",
                                    "ka",
                                    "kg",
                                    "ki",
                                    "kj",
                                    "kk",
                                    "kl",
                                    "km",
                                    "kn",
                                    "ko",
                                    "kr",
                                    "ks",
                                    "ku",
                                    "kv",
                                    "kw",
                                    "ky",
                                    "la",
                                    "lb",
                                    "lg",
                                    "li",
                                    "ln",
                                    "lo",
                                    "lt",
                                    "lu",
                                    "lv",
                                    "mg",
                                    "mh",
                                    "mi",
                                    "mk",
                                    "ml",
                                    "mn",
                                    "mr",
                                    "ms",
                                    "mt",
                                    "my",
                                    "na",
                                    "nb",
                                    "nd",
                                    "ne",
                                    "ng",
                                    "nl",
                                    "nn",
                                    "no",
                                    "nr",
                                    "nv",
                                    "ny",
                                    "oc",
                                    "oj",
                                    "om",
                                    "or",
                                    "os",
                                    "pa",
                                    "pi",
                                    "pl",
                                    "ps",
                                    "pt",
                                    "qu",
                                    "rm",
                                    "rn",
                                    "ro",
                                    "ru",
                                    "rw",
                                    "sa",
                                    "sc",
                                    "sd",
                                    "se",
                                    "sg",
                                    "si",
                                    "sk",
                                    "sl",
                                    "sm",
                                    "sn",
                                    "so",
                                    "sq",
                                    "sr",
                                    "ss",
                                    "st",
                                    "su",
                                    "sv",
                                    "sw",
                                    "ta",
                                    "te",
                                    "tg",
                                    "th",
                                    "ti",
                                    "tk",
                                    "tl",
                                    "tn",
                                    "to",
                                    "tr",
                                    "ts",
                                    "tt",
                                    "tw",
                                    "ty",
                                    "ug",
                                    "uk",
                                    "ur",
                                    "uz",
                                    "ve",
                                    "vi",
                                    "vo",
                                    "wa",
                                    "wo",
                                    "xh",
                                    "yi",
                                    "yo",
                                    "za",
                                    "zh",
                                    "zu"
                                  ]
                                }
                              ]
                            },
                            "reviewBeforeSend": {
                              "description": "Default false. True holds every send this step drafts for the operator to approve, even when the campaign is on autopilot. Nothing is scheduled until they approve it.",
                              "type": "boolean"
                            },
                            "verifyBeforeSend": {
                              "description": "DEPRECATED, and accepted only so campaigns published before the rename still parse. Set `reviewBeforeSend` instead; if both are present the new one wins.",
                              "type": "boolean"
                            }
                          },
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "research",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "instructions": {
                              "type": "string",
                              "minLength": 1,
                              "maxLength": 4000,
                              "description": "What the researcher should dig for on each lead. A research step SENDS NOTHING — it spends daily enrollment producing briefs, so place one only when the operator asked for it."
                            }
                          },
                          "required": [
                            "instructions"
                          ],
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "send_email",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "subject": {
                              "default": "",
                              "description": "Required in managed mode (instantlyCampaignId empty) — fixed text you write, never AI-written, in every draftMode. Empty is refused at save and at publish. Ignored in custom mode (instantlyCampaignId set), where the operator's own Instantly campaign carries its own subject.",
                              "type": "string",
                              "maxLength": 200
                            },
                            "body": {
                              "default": "",
                              "description": "The email body. Unlike a LinkedIn send, this always renders as exact fixed text with `{{variables}}` substituted — `ai_template` is not implemented for email yet, so there is no seed-and-slots writing here. Required in managed mode; ignored in custom mode.",
                              "type": "string",
                              "maxLength": 8000
                            },
                            "draftMode": {
                              "description": "Fixed at `template` — `ai_template` is not implemented for `send_email` yet. Omit it or send 'template'; anything else is refused.",
                              "type": "string",
                              "const": "template"
                            },
                            "instruction": {
                              "description": "Has no effect today: it only applies to `ai_template`, which `send_email` does not implement yet. Write the copy in `body`.",
                              "type": "string",
                              "maxLength": 4000
                            },
                            "language": {
                              "description": "Force every message on this step into one language, or set 'auto' so each lead is written to in their OWN language, translating your seed faithfully. OMIT IT and the step uses the default language the sender chose in their settings, or 'auto' when they chose none — which is usually what you want. This rule outranks the seed's original language.",
                              "anyOf": [
                                {
                                  "type": "string",
                                  "const": "auto"
                                },
                                {
                                  "type": "string",
                                  "enum": [
                                    "aa",
                                    "ab",
                                    "ae",
                                    "af",
                                    "ak",
                                    "am",
                                    "an",
                                    "ar",
                                    "as",
                                    "av",
                                    "ay",
                                    "az",
                                    "ba",
                                    "be",
                                    "bg",
                                    "bh",
                                    "bi",
                                    "bm",
                                    "bn",
                                    "bo",
                                    "br",
                                    "bs",
                                    "ca",
                                    "ce",
                                    "ch",
                                    "co",
                                    "cr",
                                    "cs",
                                    "cu",
                                    "cv",
                                    "cy",
                                    "da",
                                    "de",
                                    "dv",
                                    "dz",
                                    "ee",
                                    "el",
                                    "en",
                                    "eo",
                                    "es",
                                    "et",
                                    "eu",
                                    "fa",
                                    "ff",
                                    "fi",
                                    "fj",
                                    "fo",
                                    "fr",
                                    "fy",
                                    "ga",
                                    "gd",
                                    "gl",
                                    "gn",
                                    "gu",
                                    "gv",
                                    "ha",
                                    "he",
                                    "hi",
                                    "ho",
                                    "hr",
                                    "ht",
                                    "hu",
                                    "hy",
                                    "hz",
                                    "ia",
                                    "id",
                                    "ie",
                                    "ig",
                                    "ii",
                                    "ik",
                                    "io",
                                    "is",
                                    "it",
                                    "iu",
                                    "ja",
                                    "jv",
                                    "ka",
                                    "kg",
                                    "ki",
                                    "kj",
                                    "kk",
                                    "kl",
                                    "km",
                                    "kn",
                                    "ko",
                                    "kr",
                                    "ks",
                                    "ku",
                                    "kv",
                                    "kw",
                                    "ky",
                                    "la",
                                    "lb",
                                    "lg",
                                    "li",
                                    "ln",
                                    "lo",
                                    "lt",
                                    "lu",
                                    "lv",
                                    "mg",
                                    "mh",
                                    "mi",
                                    "mk",
                                    "ml",
                                    "mn",
                                    "mr",
                                    "ms",
                                    "mt",
                                    "my",
                                    "na",
                                    "nb",
                                    "nd",
                                    "ne",
                                    "ng",
                                    "nl",
                                    "nn",
                                    "no",
                                    "nr",
                                    "nv",
                                    "ny",
                                    "oc",
                                    "oj",
                                    "om",
                                    "or",
                                    "os",
                                    "pa",
                                    "pi",
                                    "pl",
                                    "ps",
                                    "pt",
                                    "qu",
                                    "rm",
                                    "rn",
                                    "ro",
                                    "ru",
                                    "rw",
                                    "sa",
                                    "sc",
                                    "sd",
                                    "se",
                                    "sg",
                                    "si",
                                    "sk",
                                    "sl",
                                    "sm",
                                    "sn",
                                    "so",
                                    "sq",
                                    "sr",
                                    "ss",
                                    "st",
                                    "su",
                                    "sv",
                                    "sw",
                                    "ta",
                                    "te",
                                    "tg",
                                    "th",
                                    "ti",
                                    "tk",
                                    "tl",
                                    "tn",
                                    "to",
                                    "tr",
                                    "ts",
                                    "tt",
                                    "tw",
                                    "ty",
                                    "ug",
                                    "uk",
                                    "ur",
                                    "uz",
                                    "ve",
                                    "vi",
                                    "vo",
                                    "wa",
                                    "wo",
                                    "xh",
                                    "yi",
                                    "yo",
                                    "za",
                                    "zh",
                                    "zu"
                                  ]
                                }
                              ]
                            },
                            "localization": {
                              "description": "The seed translated into other languages, one per ISO 639-1 code, each keeping every `{{…}}` slot byte-exact and every `---` line. Kairon writes a language here the first time it drafts a lead in it; you may edit any entry and it is then used as written for every later lead in that language. Leave it out to let Kairon translate.",
                              "type": "object",
                              "properties": {
                                "seedHash": {
                                  "type": "string",
                                  "minLength": 1,
                                  "description": "Hash of the seed text these translations were made for. Kairon sets it; when the seed changes, the whole object goes stale and is rebuilt. Copy it through unchanged."
                                },
                                "seedLanguage": {
                                  "type": "string",
                                  "enum": [
                                    "aa",
                                    "ab",
                                    "ae",
                                    "af",
                                    "ak",
                                    "am",
                                    "an",
                                    "ar",
                                    "as",
                                    "av",
                                    "ay",
                                    "az",
                                    "ba",
                                    "be",
                                    "bg",
                                    "bh",
                                    "bi",
                                    "bm",
                                    "bn",
                                    "bo",
                                    "br",
                                    "bs",
                                    "ca",
                                    "ce",
                                    "ch",
                                    "co",
                                    "cr",
                                    "cs",
                                    "cu",
                                    "cv",
                                    "cy",
                                    "da",
                                    "de",
                                    "dv",
                                    "dz",
                                    "ee",
                                    "el",
                                    "en",
                                    "eo",
                                    "es",
                                    "et",
                                    "eu",
                                    "fa",
                                    "ff",
                                    "fi",
                                    "fj",
                                    "fo",
                                    "fr",
                                    "fy",
                                    "ga",
                                    "gd",
                                    "gl",
                                    "gn",
                                    "gu",
                                    "gv",
                                    "ha",
                                    "he",
                                    "hi",
                                    "ho",
                                    "hr",
                                    "ht",
                                    "hu",
                                    "hy",
                                    "hz",
                                    "ia",
                                    "id",
                                    "ie",
                                    "ig",
                                    "ii",
                                    "ik",
                                    "io",
                                    "is",
                                    "it",
                                    "iu",
                                    "ja",
                                    "jv",
                                    "ka",
                                    "kg",
                                    "ki",
                                    "kj",
                                    "kk",
                                    "kl",
                                    "km",
                                    "kn",
                                    "ko",
                                    "kr",
                                    "ks",
                                    "ku",
                                    "kv",
                                    "kw",
                                    "ky",
                                    "la",
                                    "lb",
                                    "lg",
                                    "li",
                                    "ln",
                                    "lo",
                                    "lt",
                                    "lu",
                                    "lv",
                                    "mg",
                                    "mh",
                                    "mi",
                                    "mk",
                                    "ml",
                                    "mn",
                                    "mr",
                                    "ms",
                                    "mt",
                                    "my",
                                    "na",
                                    "nb",
                                    "nd",
                                    "ne",
                                    "ng",
                                    "nl",
                                    "nn",
                                    "no",
                                    "nr",
                                    "nv",
                                    "ny",
                                    "oc",
                                    "oj",
                                    "om",
                                    "or",
                                    "os",
                                    "pa",
                                    "pi",
                                    "pl",
                                    "ps",
                                    "pt",
                                    "qu",
                                    "rm",
                                    "rn",
                                    "ro",
                                    "ru",
                                    "rw",
                                    "sa",
                                    "sc",
                                    "sd",
                                    "se",
                                    "sg",
                                    "si",
                                    "sk",
                                    "sl",
                                    "sm",
                                    "sn",
                                    "so",
                                    "sq",
                                    "sr",
                                    "ss",
                                    "st",
                                    "su",
                                    "sv",
                                    "sw",
                                    "ta",
                                    "te",
                                    "tg",
                                    "th",
                                    "ti",
                                    "tk",
                                    "tl",
                                    "tn",
                                    "to",
                                    "tr",
                                    "ts",
                                    "tt",
                                    "tw",
                                    "ty",
                                    "ug",
                                    "uk",
                                    "ur",
                                    "uz",
                                    "ve",
                                    "vi",
                                    "vo",
                                    "wa",
                                    "wo",
                                    "xh",
                                    "yi",
                                    "yo",
                                    "za",
                                    "zh",
                                    "zu"
                                  ],
                                  "description": "The ISO 639-1 code of the language the seed itself is written in, detected once by Kairon."
                                },
                                "translations": {
                                  "type": "object",
                                  "propertyNames": {
                                    "type": "string",
                                    "enum": [
                                      "aa",
                                      "ab",
                                      "ae",
                                      "af",
                                      "ak",
                                      "am",
                                      "an",
                                      "ar",
                                      "as",
                                      "av",
                                      "ay",
                                      "az",
                                      "ba",
                                      "be",
                                      "bg",
                                      "bh",
                                      "bi",
                                      "bm",
                                      "bn",
                                      "bo",
                                      "br",
                                      "bs",
                                      "ca",
                                      "ce",
                                      "ch",
                                      "co",
                                      "cr",
                                      "cs",
                                      "cu",
                                      "cv",
                                      "cy",
                                      "da",
                                      "de",
                                      "dv",
                                      "dz",
                                      "ee",
                                      "el",
                                      "en",
                                      "eo",
                                      "es",
                                      "et",
                                      "eu",
                                      "fa",
                                      "ff",
                                      "fi",
                                      "fj",
                                      "fo",
                                      "fr",
                                      "fy",
                                      "ga",
                                      "gd",
                                      "gl",
                                      "gn",
                                      "gu",
                                      "gv",
                                      "ha",
                                      "he",
                                      "hi",
                                      "ho",
                                      "hr",
                                      "ht",
                                      "hu",
                                      "hy",
                                      "hz",
                                      "ia",
                                      "id",
                                      "ie",
                                      "ig",
                                      "ii",
                                      "ik",
                                      "io",
                                      "is",
                                      "it",
                                      "iu",
                                      "ja",
                                      "jv",
                                      "ka",
                                      "kg",
                                      "ki",
                                      "kj",
                                      "kk",
                                      "kl",
                                      "km",
                                      "kn",
                                      "ko",
                                      "kr",
                                      "ks",
                                      "ku",
                                      "kv",
                                      "kw",
                                      "ky",
                                      "la",
                                      "lb",
                                      "lg",
                                      "li",
                                      "ln",
                                      "lo",
                                      "lt",
                                      "lu",
                                      "lv",
                                      "mg",
                                      "mh",
                                      "mi",
                                      "mk",
                                      "ml",
                                      "mn",
                                      "mr",
                                      "ms",
                                      "mt",
                                      "my",
                                      "na",
                                      "nb",
                                      "nd",
                                      "ne",
                                      "ng",
                                      "nl",
                                      "nn",
                                      "no",
                                      "nr",
                                      "nv",
                                      "ny",
                                      "oc",
                                      "oj",
                                      "om",
                                      "or",
                                      "os",
                                      "pa",
                                      "pi",
                                      "pl",
                                      "ps",
                                      "pt",
                                      "qu",
                                      "rm",
                                      "rn",
                                      "ro",
                                      "ru",
                                      "rw",
                                      "sa",
                                      "sc",
                                      "sd",
                                      "se",
                                      "sg",
                                      "si",
                                      "sk",
                                      "sl",
                                      "sm",
                                      "sn",
                                      "so",
                                      "sq",
                                      "sr",
                                      "ss",
                                      "st",
                                      "su",
                                      "sv",
                                      "sw",
                                      "ta",
                                      "te",
                                      "tg",
                                      "th",
                                      "ti",
                                      "tk",
                                      "tl",
                                      "tn",
                                      "to",
                                      "tr",
                                      "ts",
                                      "tt",
                                      "tw",
                                      "ty",
                                      "ug",
                                      "uk",
                                      "ur",
                                      "uz",
                                      "ve",
                                      "vi",
                                      "vo",
                                      "wa",
                                      "wo",
                                      "xh",
                                      "yi",
                                      "yo",
                                      "za",
                                      "zh",
                                      "zu"
                                    ]
                                  },
                                  "additionalProperties": {
                                    "type": "string",
                                    "minLength": 1
                                  },
                                  "description": "One translated seed per ISO 639-1 code. Each must keep every `{{…}}` slot byte-exact and in order, and the same number of `---` lines, or publish refuses it."
                                }
                              },
                              "required": [
                                "seedHash",
                                "seedLanguage",
                                "translations"
                              ]
                            },
                            "reviewBeforeSend": {
                              "description": "Default false. True holds every send this step drafts for the operator to approve, even when the campaign is on autopilot. Nothing is scheduled until they approve it.",
                              "type": "boolean"
                            },
                            "verifyBeforeSend": {
                              "description": "DEPRECATED, and accepted only so campaigns published before the rename still parse. Set `reviewBeforeSend` instead; if both are present the new one wins.",
                              "type": "boolean"
                            },
                            "adaptToCulture": {
                              "description": "Retired: accepted for old steps and ignored. Do not send it.",
                              "type": "boolean"
                            },
                            "instantlyCampaignId": {
                              "default": "",
                              "type": "string"
                            },
                            "enrichmentBudgetHours": {
                              "description": "Retired: accepted for old steps and ignored. Do not send it.",
                              "type": "number"
                            },
                            "attachments": {
                              "description": "Files sent with this step to every lead it reaches: images and PDFs, plus one voice note and one video on a LinkedIn message. Each is `{ fileName, mimeType, url }` (https) or `{ fileName, mimeType, dataBase64 }`, as `message_send` takes; a file `campaign_get` returned may be sent back as is.",
                              "maxItems": 5,
                              "type": "array",
                              "items": {
                                "anyOf": [
                                  {
                                    "type": "object",
                                    "properties": {
                                      "storageKey": {
                                        "type": "string",
                                        "maxLength": 512,
                                        "pattern": "^step-attachments\\/.*"
                                      },
                                      "fileName": {
                                        "type": "string",
                                        "minLength": 1,
                                        "maxLength": 255
                                      },
                                      "mimeType": {
                                        "type": "string",
                                        "minLength": 1,
                                        "maxLength": 255
                                      },
                                      "byteSize": {
                                        "type": "integer",
                                        "exclusiveMinimum": 0,
                                        "maximum": 9007199254740991
                                      }
                                    },
                                    "required": [
                                      "storageKey",
                                      "fileName",
                                      "mimeType",
                                      "byteSize"
                                    ],
                                    "additionalProperties": false
                                  },
                                  {
                                    "anyOf": [
                                      {
                                        "type": "object",
                                        "properties": {
                                          "fileName": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file name, with its extension — what the recipient sees."
                                          },
                                          "mimeType": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                                          },
                                          "dataBase64": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 4194304,
                                            "description": "The file itself, base64-encoded. Use this OR `url`, never both."
                                          }
                                        },
                                        "required": [
                                          "fileName",
                                          "mimeType",
                                          "dataBase64"
                                        ],
                                        "additionalProperties": false
                                      },
                                      {
                                        "type": "object",
                                        "properties": {
                                          "fileName": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file name, with its extension — what the recipient sees."
                                          },
                                          "mimeType": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 255,
                                            "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                                          },
                                          "url": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 2048,
                                            "format": "uri",
                                            "description": "An https address we fetch server-side. Use this OR `dataBase64`, never both. Reach for it when you cannot produce base64."
                                          }
                                        },
                                        "required": [
                                          "fileName",
                                          "mimeType",
                                          "url"
                                        ],
                                        "additionalProperties": false
                                      }
                                    ]
                                  }
                                ]
                              }
                            }
                          },
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "key": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 80,
                          "description": "A stable id you choose for this step; edges reference it by this name. Unique within the graph. Reuse the same key on a later `campaign_set_graph` and the step keeps its position and any config field you do not mention."
                        },
                        "type": {
                          "type": "string",
                          "const": "audience_split",
                          "description": "Which kind of step this is. It decides the shape of `config`, and which outcome edges the step may route on."
                        },
                        "config": {
                          "type": "object",
                          "properties": {
                            "weightA": {
                              "default": 50,
                              "description": "Percent of enrolling leads sent down branch A; the rest go down branch B. Use 50 for an even test — 0 or 100 turns the test off and is what applying a winner writes.",
                              "type": "integer",
                              "minimum": 0,
                              "maximum": 100
                            },
                            "metric": {
                              "type": "string",
                              "enum": [
                                "accept_rate",
                                "reply_rate"
                              ],
                              "description": "What counts as a win when comparing the two branches. Read by Split Review only — it changes nothing about how the campaign runs."
                            },
                            "checkpoint": {
                              "oneOf": [
                                {
                                  "type": "object",
                                  "properties": {
                                    "mode": {
                                      "type": "string",
                                      "const": "calculated",
                                      "description": "Work the sample size out from past results."
                                    },
                                    "minimumDetectableEffectPoints": {
                                      "type": "number",
                                      "minimum": 1,
                                      "maximum": 50,
                                      "description": "The smallest gap worth spotting, in percentage points — 10 means \"tell me once one branch is 10 points ahead\". Smaller numbers need many more leads."
                                    }
                                  },
                                  "required": [
                                    "mode",
                                    "minimumDetectableEffectPoints"
                                  ]
                                },
                                {
                                  "type": "object",
                                  "properties": {
                                    "mode": {
                                      "type": "string",
                                      "const": "manual",
                                      "description": "State the finish line yourself."
                                    },
                                    "by": {
                                      "oneOf": [
                                        {
                                          "type": "object",
                                          "properties": {
                                            "unit": {
                                              "type": "string",
                                              "const": "date",
                                              "description": "Finish on a calendar day."
                                            },
                                            "at": {
                                              "type": "string",
                                              "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
                                              "description": "The day the test may be judged, `YYYY-MM-DD`, read in the campaign’s own timezone."
                                            }
                                          },
                                          "required": [
                                            "unit",
                                            "at"
                                          ]
                                        },
                                        {
                                          "type": "object",
                                          "properties": {
                                            "unit": {
                                              "type": "string",
                                              "const": "count",
                                              "description": "Finish on a lead count."
                                            },
                                            "leadsPerBranch": {
                                              "type": "integer",
                                              "minimum": 1,
                                              "maximum": 9007199254740991,
                                              "description": "How many leads EACH branch must reach before a winner may be named."
                                            }
                                          },
                                          "required": [
                                            "unit",
                                            "leadsPerBranch"
                                          ]
                                        }
                                      ],
                                      "description": "Whether the finish line is a date or a per-branch lead count."
                                    }
                                  },
                                  "required": [
                                    "mode",
                                    "by"
                                  ]
                                }
                              ],
                              "description": "When a winner may be declared. No winner is ever named before this point, however far ahead one branch already looks — an early peek is how an A/B test lies."
                            }
                          },
                          "required": [
                            "metric",
                            "checkpoint"
                          ],
                          "additionalProperties": false,
                          "description": "Settings for this step type. The fields differ per `type`."
                        }
                      },
                      "required": [
                        "key",
                        "type",
                        "config"
                      ]
                    }
                  ]
                },
                "description": "Every step in the sequence. Layout is automatic — send no coordinates."
              },
              "edges": {
                "maxItems": 300,
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "fromKey": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 80,
                      "description": "The `key` of the step this edge leaves."
                    },
                    "toKey": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 80,
                      "description": "The `key` of the step it routes to."
                    },
                    "condition": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 40,
                      "description": "Which OUTCOME of `fromKey` takes this edge — e.g. `sent`, `accepted`, `timeout`, `done`, `default`. Each step type emits a fixed set, and a condition it does not emit is refused. Some are REQUIRED: leave `wait_connection_accepted`'s `accepted` or a `delay`'s `default` unwired and publish fails with `required_outcome_unwired`."
                    }
                  },
                  "required": [
                    "fromKey",
                    "toKey",
                    "condition"
                  ]
                },
                "description": "How one step's outcome routes to the next — these are fixed rules, and a violation is refused.\nA node emits the outcomes in [brackets]; an edge carries exactly one of them, to one of\nthe types after the arrow. A `*` outcome MUST route onward; any other may end the flow.\nA wait_connection_accepted needs a send_invite SOMEWHERE upstream — not necessarily\nright before it. Put a second one further down a `timeout` branch to re-ask whether\nthey have accepted yet; two waits back to back are refused. A wait that leads to\nanother wait on its `timeout` branch MUST set withdrawOnTimeout:false — the default\ntakes the invite back, leaving the later wait nothing that can be accepted.\nsend_invite [sent] → wait_connection_accepted\nwait_connection_accepted [timeout, accepted*] → delay, send_message, like_last_post, visit_profile, follow_profile, remove_relation, comment_on_post, send_inmail, send_email, audience_split, wait_reply, agentic_reply\ndelay [default*] → send_invite, wait_connection_accepted, send_message, like_last_post, visit_profile, follow_profile, remove_relation, comment_on_post, send_inmail, send_email, audience_split, wait_reply, agentic_reply\nsend_message [sent] → wait_connection_accepted, delay, like_last_post, visit_profile, follow_profile, remove_relation, comment_on_post, send_email, audience_split, wait_reply, agentic_reply\nlike_last_post [done] → send_invite, wait_connection_accepted, delay, send_message, visit_profile, follow_profile, remove_relation, comment_on_post, send_inmail, send_email, audience_split, wait_reply, agentic_reply\nvisit_profile [done] → send_invite, wait_connection_accepted, delay, send_message, like_last_post, follow_profile, remove_relation, comment_on_post, send_inmail, send_email, audience_split, wait_reply, agentic_reply\nfollow_profile [done] → send_invite, wait_connection_accepted, delay, send_message, like_last_post, visit_profile, remove_relation, comment_on_post, send_inmail, send_email, audience_split, wait_reply, agentic_reply\nremove_relation [done] → send_invite, wait_connection_accepted, delay, like_last_post, visit_profile, follow_profile, comment_on_post, send_inmail, send_email, audience_split, wait_reply\ncomment_on_post [commented] → send_invite, wait_connection_accepted, delay, send_message, like_last_post, visit_profile, follow_profile, remove_relation, send_inmail, send_email, audience_split, wait_reply, agentic_reply\nsend_inmail [sent] → wait_connection_accepted, delay, like_last_post, visit_profile, follow_profile, remove_relation, comment_on_post, send_email, audience_split, wait_reply, agentic_reply\nsend_email [sent] → send_invite, wait_connection_accepted, delay, send_message, like_last_post, visit_profile, follow_profile, remove_relation, comment_on_post, send_inmail, audience_split, wait_reply, agentic_reply\naudience_split [branch_a*, branch_b*] → send_invite, wait_connection_accepted, delay, send_message, like_last_post, visit_profile, follow_profile, remove_relation, comment_on_post, send_inmail, send_email, audience_split, wait_reply, agentic_reply\nwait_reply [timeout, replied] → send_invite, wait_connection_accepted, delay, send_message, like_last_post, visit_profile, follow_profile, remove_relation, comment_on_post, send_inmail, send_email, audience_split, agentic_reply\nagentic_reply [no_reply, done] → wait_connection_accepted, delay, like_last_post, visit_profile, follow_profile, remove_relation, comment_on_post, send_email, audience_split\nPATH RULE (the rows cannot show this): after a wait's `timeout` outcome the lead is NOT connected,\nso NO send_message may follow it — a `delay` in between changes nothing. It stays banned down\nthat branch until something says otherwise: a later gate's `accepted`, or a fresh accepted invite.\nInstead: nudge them with like_last_post / visit_profile / follow_profile / comment_on_post / send_inmail / send_email / audience_split / wait_reply / agentic_reply; re-ask with another wait_connection_accepted (the earlier wait then needs withdrawOnTimeout:false) and send off ITS `accepted`; send another invite and message off the new `accepted`.\nPATH RULE (the rows cannot show this): audience_split is SEE-THROUGH. It takes no time and\nsends nothing, so what may follow it is decided by the step BEFORE it, not by its own row above.\nA step you may not put directly after X you also may not put after a split that follows X."
              },
              "startNodeKey": {
                "anyOf": [
                  {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 80
                  },
                  {
                    "type": "null"
                  }
                ],
                "description": "The `key` of the step each lead begins on. `null` lets it be inferred when exactly one node has no inbound edge; name it when the graph has several roots or loops back through every step, or publish fails with `no_start`."
              }
            },
            "required": [
              "nodes",
              "edges",
              "startNodeKey"
            ]
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "id",
          "graph"
        ]
      }
    },
    {
      "name": "campaign_stats",
      "title": "How the campaign is doing",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        },
        {
          "name": "days",
          "type": "7 | 30",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          },
          "days": {
            "description": "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.",
            "anyOf": [
              {
                "type": "number",
                "const": 7
              },
              {
                "type": "number",
                "const": 30
              }
            ]
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "campaign_template_get",
      "title": "Read one campaign template in full",
      "description": "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`).",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "\"reactivation\" | \"nurturing\" | \"inbound-engagers\" | \"competitor-engagers\" | \"company-page-engagers\" | \"keyword\" | \"travel\" | \"third-party-event\" | \"own-event\" | \"webinar\" | \"networking\" | \"lead-magnet\"",
          "required": true,
          "description": "Which template — an `id` from `campaign_template_list`."
        },
        {
          "name": "locale",
          "type": "\"en\" | \"es\"",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "enum": [
              "reactivation",
              "nurturing",
              "inbound-engagers",
              "competitor-engagers",
              "company-page-engagers",
              "keyword",
              "travel",
              "third-party-event",
              "own-event",
              "webinar",
              "networking",
              "lead-magnet"
            ],
            "description": "Which template — an `id` from `campaign_template_list`."
          },
          "locale": {
            "description": "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.",
            "type": "string",
            "enum": [
              "en",
              "es"
            ]
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "campaign_template_list",
      "title": "List the campaign templates",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "locale",
          "type": "\"en\" | \"es\"",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "locale": {
            "description": "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.",
            "type": "string",
            "enum": [
              "en",
              "es"
            ]
          }
        }
      }
    },
    {
      "name": "campaign_update",
      "title": "Edit the campaign's configuration and audience",
      "description": "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).",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "name",
          "type": "string",
          "required": false,
          "description": "The campaign's name, for the operator to recognise it. Not seen by any lead."
        },
        {
          "name": "sendWindow",
          "type": "object",
          "required": false,
          "description": "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."
        },
        {
          "name": "autonomy",
          "type": "\"manual\" | \"autopilot\"",
          "required": false,
          "description": "`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."
        },
        {
          "name": "allowsReEntry",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "sendFromOwner",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "dailyEnrollmentLimit",
          "type": "integer",
          "required": false,
          "description": "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`."
        },
        {
          "name": "priority",
          "type": "integer | null",
          "required": false,
          "description": "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."
        },
        {
          "name": "endsOn",
          "type": "string | null",
          "required": false,
          "description": "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."
        },
        {
          "name": "enrollmentFilter",
          "type": "object",
          "required": false,
          "description": "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."
        },
        {
          "name": "icpId",
          "type": "string | null",
          "required": false,
          "description": "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."
        },
        {
          "name": "stopOnReply",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "connectionDegree",
          "type": "\"any\" | \"only_first\" | \"exclude_first\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "stopOnCompanyReply",
          "type": "\"off\" | \"any_reply\" | \"interested\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        },
        {
          "name": "leadListIds",
          "type": "string[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "initiativeId",
          "type": "string | null",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "name": {
            "description": "The campaign's name, for the operator to recognise it. Not seen by any lead.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "sendWindow": {
            "description": "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.",
            "type": "object",
            "properties": {
              "days": {
                "minItems": 1,
                "maxItems": 7,
                "type": "array",
                "items": {
                  "type": "integer",
                  "minimum": 0,
                  "maximum": 6
                },
                "description": "Weekdays sends may fire, 0 = Sunday to 6 = Saturday. At least one, no duplicates."
              },
              "startHour": {
                "type": "integer",
                "minimum": 0,
                "maximum": 23,
                "description": "First hour sends may fire, 0-23, in `timeZone`. Must be before `endHour`."
              },
              "endHour": {
                "type": "integer",
                "minimum": 0,
                "maximum": 23,
                "description": "Last hour sends may fire, 0-23, in `timeZone`. Must be after `startHour`."
              },
              "timeZone": {
                "description": "IANA zone the hours are read in, e.g. `America/Argentina/Buenos_Aires`. ASK the operator rather than assuming: a valid zone that is not theirs is accepted silently and sends at the wrong hour every day, with nothing to flag it.",
                "type": "string",
                "minLength": 1,
                "maxLength": 64
              }
            },
            "required": [
              "days",
              "startHour",
              "endHour",
              "timeZone"
            ]
          },
          "autonomy": {
            "description": "`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.",
            "type": "string",
            "enum": [
              "manual",
              "autopilot"
            ]
          },
          "allowsReEntry": {
            "description": "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.",
            "type": "boolean"
          },
          "sendFromOwner": {
            "description": "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.",
            "type": "boolean"
          },
          "dailyEnrollmentLimit": {
            "description": "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`.",
            "type": "integer",
            "minimum": 1,
            "maximum": 200
          },
          "priority": {
            "description": "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.",
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 3
              },
              {
                "type": "null"
              }
            ]
          },
          "endsOn": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              {
                "type": "null"
              }
            ]
          },
          "enrollmentFilter": {
            "description": "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.",
            "type": "object",
            "properties": {
              "buckets": {
                "minItems": 1,
                "type": "array",
                "items": {
                  "type": "string",
                  "enum": [
                    "new",
                    "unanswered",
                    "in_conversation"
                  ]
                },
                "description": "Which contact-history buckets may enroll: `new` (no conversation either way, and nothing a Kairon campaign sent), `unanswered` (we messaged, no reply), `in_conversation` (they wrote to us). Unique, at least one. All three means enroll everyone. Anyone outside the selected buckets is skipped, not queued, so a narrow set is why a large audience enrolls almost nobody."
              },
              "scope": {
                "type": "string",
                "enum": [
                  "seat",
                  "org"
                ],
                "description": "Whose prior-contact history is read: `seat` (this campaign's sending seat) or `org` (every LinkedIn seat). Copying a `seat` filter onto a DIFFERENT seat changes what it means — the new seat's history is empty, so people the original filter excluded now enroll."
              }
            },
            "required": [
              "buckets",
              "scope"
            ]
          },
          "icpId": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string",
                "minLength": 1
              },
              {
                "type": "null"
              }
            ]
          },
          "stopOnReply": {
            "description": "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.",
            "type": "boolean"
          },
          "connectionDegree": {
            "description": "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.",
            "type": "string",
            "enum": [
              "any",
              "only_first",
              "exclude_first"
            ]
          },
          "stopOnCompanyReply": {
            "description": "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.",
            "type": "string",
            "enum": [
              "off",
              "any_reply",
              "interested"
            ]
          },
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          },
          "leadListIds": {
            "description": "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.",
            "minItems": 1,
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string",
              "minLength": 1
            }
          },
          "initiativeId": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string"
              },
              {
                "type": "null"
              }
            ]
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "campaign_version_restore",
      "title": "Bring an earlier sequence graph back as the draft",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        },
        {
          "name": "versionNumber",
          "type": "integer",
          "required": true,
          "description": "The version to bring back, as `campaign_versions` listed it."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          },
          "versionNumber": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991,
            "description": "The version to bring back, as `campaign_versions` listed it."
          }
        },
        "required": [
          "id",
          "versionNumber"
        ]
      }
    },
    {
      "name": "campaign_versions",
      "title": "Read the campaign's earlier sequence graphs",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
        },
        {
          "name": "versionNumber",
          "type": "integer",
          "required": false,
          "description": "Omit for the list of versions. Give one to get THAT version’s full graph, in exactly the shape `campaign_set_graph` accepts."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The campaign id, as `campaign_list` or `campaign_create` returned it."
          },
          "versionNumber": {
            "description": "Omit for the list of versions. Give one to get THAT version’s full graph, in exactly the shape `campaign_set_graph` accepts.",
            "type": "integer",
            "exclusiveMinimum": 0,
            "maximum": 9007199254740991
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "checkpoint_delete",
      "title": "Withdraw a checkpoint",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "initiativeId",
          "type": "string",
          "required": true,
          "description": "The initiative the checkpoint belongs to."
        },
        {
          "name": "checkpointId",
          "type": "string",
          "required": true,
          "description": "The checkpoint id (`ckpt_…`), as `initiative_get` returned it in the history."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "initiativeId": {
            "type": "string",
            "description": "The initiative the checkpoint belongs to."
          },
          "checkpointId": {
            "type": "string",
            "description": "The checkpoint id (`ckpt_…`), as `initiative_get` returned it in the history."
          }
        },
        "required": [
          "initiativeId",
          "checkpointId"
        ]
      }
    },
    {
      "name": "checkpoint_edit",
      "title": "Correct a checkpoint",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "body",
          "type": "string",
          "required": false,
          "description": "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`."
        },
        {
          "name": "health",
          "type": "\"on_track\" | \"at_risk\" | \"off_track\"",
          "required": false,
          "description": "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."
        },
        {
          "name": "initiativeId",
          "type": "string",
          "required": true,
          "description": "The initiative the checkpoint belongs to."
        },
        {
          "name": "checkpointId",
          "type": "string",
          "required": true,
          "description": "The checkpoint id (`ckpt_…`), as `initiative_get` returned it in the history."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 20000,
            "description": "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": {
            "type": "string",
            "enum": [
              "on_track",
              "at_risk",
              "off_track"
            ],
            "description": "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": {
            "type": "string",
            "description": "The initiative the checkpoint belongs to."
          },
          "checkpointId": {
            "type": "string",
            "description": "The checkpoint id (`ckpt_…`), as `initiative_get` returned it in the history."
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "initiativeId",
          "checkpointId"
        ]
      }
    },
    {
      "name": "checkpoint_publish",
      "title": "Publish a checkpoint on an initiative",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "body",
          "type": "string",
          "required": true,
          "description": "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`."
        },
        {
          "name": "health",
          "type": "\"on_track\" | \"at_risk\" | \"off_track\"",
          "required": true,
          "description": "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."
        },
        {
          "name": "initiativeId",
          "type": "string",
          "required": true,
          "description": "The initiative being judged (`init_…`)."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "body": {
            "type": "string",
            "minLength": 1,
            "maxLength": 20000,
            "description": "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": {
            "type": "string",
            "enum": [
              "on_track",
              "at_risk",
              "off_track"
            ],
            "description": "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": {
            "type": "string",
            "description": "The initiative being judged (`init_…`)."
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "body",
          "health",
          "initiativeId"
        ]
      }
    },
    {
      "name": "initiative_delete",
      "title": "Withdraw a go-to-market initiative",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "initiativeId",
          "type": "string",
          "required": true,
          "description": "The initiative to remove, by id (`init_…`)."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "initiativeId": {
            "type": "string",
            "description": "The initiative to remove, by id (`init_…`)."
          }
        },
        "required": [
          "initiativeId"
        ]
      }
    },
    {
      "name": "initiative_get",
      "title": "Read one go-to-market initiative",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "initiativeId",
          "type": "string",
          "required": true,
          "description": "The initiative to read, by id (`init_…`)."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "initiativeId": {
            "type": "string",
            "description": "The initiative to read, by id (`init_…`)."
          }
        },
        "required": [
          "initiativeId"
        ]
      }
    },
    {
      "name": "initiative_list",
      "title": "List the go-to-market initiatives",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "status",
          "type": "\"draft\" | \"active\" | \"completed\" | \"canceled\"",
          "required": false,
          "description": "Return only initiatives in this state. Omit for all of them, including completed and canceled ones."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "status": {
            "description": "Return only initiatives in this state. Omit for all of them, including completed and canceled ones.",
            "type": "string",
            "enum": [
              "draft",
              "active",
              "completed",
              "canceled"
            ]
          }
        }
      }
    },
    {
      "name": "initiative_set",
      "title": "Create or edit a go-to-market initiative",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "name",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "body",
          "type": "string | null",
          "required": false,
          "description": "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."
        },
        {
          "name": "status",
          "type": "\"draft\" | \"active\" | \"completed\" | \"canceled\"",
          "required": false,
          "description": "`draft` while it is being shaped, `active` once it is being worked."
        },
        {
          "name": "priority",
          "type": "integer | null",
          "required": false,
          "description": "0 is highest. Lower sorts first, matching every other priority in Kairon."
        },
        {
          "name": "startDate",
          "type": "string | null",
          "required": false,
          "description": "`YYYY-MM-DD`, this century. An initiative starts on a date, not at a moment."
        },
        {
          "name": "endDate",
          "type": "string | null",
          "required": false,
          "description": "`YYYY-MM-DD`, this century. When the initiative should have paid off, or be called. Must not be before `startDate`."
        },
        {
          "name": "initiativeId",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "owner",
          "type": "object | null",
          "required": false,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "name": {
            "description": "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.",
            "type": "string",
            "minLength": 1,
            "maxLength": 200
          },
          "body": {
            "description": "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.",
            "anyOf": [
              {
                "type": "string",
                "maxLength": 20000
              },
              {
                "type": "null"
              }
            ]
          },
          "status": {
            "description": "`draft` while it is being shaped, `active` once it is being worked.",
            "type": "string",
            "enum": [
              "draft",
              "active",
              "completed",
              "canceled"
            ]
          },
          "priority": {
            "description": "0 is highest. Lower sorts first, matching every other priority in Kairon.",
            "anyOf": [
              {
                "type": "integer",
                "minimum": 0,
                "maximum": 3
              },
              {
                "type": "null"
              }
            ]
          },
          "startDate": {
            "description": "`YYYY-MM-DD`, this century. An initiative starts on a date, not at a moment.",
            "anyOf": [
              {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              {
                "type": "null"
              }
            ]
          },
          "endDate": {
            "description": "`YYYY-MM-DD`, this century. When the initiative should have paid off, or be called. Must not be before `startDate`.",
            "anyOf": [
              {
                "type": "string",
                "pattern": "^\\d{4}-\\d{2}-\\d{2}$"
              },
              {
                "type": "null"
              }
            ]
          },
          "initiativeId": {
            "description": "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.",
            "type": "string"
          },
          "owner": {
            "description": "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.",
            "anyOf": [
              {
                "anyOf": [
                  {
                    "type": "object",
                    "properties": {
                      "email": {
                        "type": "string",
                        "minLength": 1,
                        "maxLength": 320,
                        "description": "The owner's email, when you do not have their `userId`. Must be a member of this org."
                      }
                    },
                    "required": [
                      "email"
                    ]
                  },
                  {
                    "type": "object",
                    "properties": {
                      "userId": {
                        "type": "string",
                        "minLength": 1
                      }
                    },
                    "required": [
                      "userId"
                    ]
                  }
                ]
              },
              {
                "type": "null"
              }
            ]
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        }
      }
    },
    {
      "name": "insights_report",
      "title": "How the whole account is performing",
      "description": "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.",
      "cluster": "campaigns",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "from",
          "type": "string",
          "required": true,
          "description": "First day of the window, `YYYY-MM-DD`, inclusive."
        },
        {
          "name": "to",
          "type": "string",
          "required": true,
          "description": "Last day of the window, `YYYY-MM-DD`, inclusive. Must be on or after `from`, and the window must not exceed 366 days."
        },
        {
          "name": "userId",
          "type": "string",
          "required": false,
          "description": "Narrow to one seat owner. Omit for every seat in the organization."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "from": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "First day of the window, `YYYY-MM-DD`, inclusive."
          },
          "to": {
            "type": "string",
            "pattern": "^\\d{4}-\\d{2}-\\d{2}$",
            "description": "Last day of the window, `YYYY-MM-DD`, inclusive. Must be on or after `from`, and the window must not exceed 366 days."
          },
          "userId": {
            "description": "Narrow to one seat owner. Omit for every seat in the organization.",
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "from",
          "to"
        ]
      }
    },
    {
      "name": "chat_list",
      "title": "List conversations",
      "description": "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.",
      "cluster": "inbox-outreach",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "seatId",
          "type": "string",
          "required": false,
          "description": "One teammate's seat id, from `seat_list`. Defaults to your own."
        },
        {
          "name": "scope",
          "type": "\"mine\" | \"org\"",
          "required": false,
          "description": "'mine' (default) is your own seat; 'org' is every seat in the organization."
        },
        {
          "name": "unread",
          "type": "boolean",
          "required": false,
          "description": "True returns only chats with unread messages."
        },
        {
          "name": "answered",
          "type": "\"yes\" | \"no\"",
          "required": false,
          "description": "'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."
        },
        {
          "name": "leadsOnly",
          "type": "boolean",
          "required": false,
          "description": "Only chats already linked to a lead."
        },
        {
          "name": "needsHuman",
          "type": "boolean",
          "required": false,
          "description": "Only chats where Kairon handed the conversation back and is waiting on you."
        },
        {
          "name": "classification",
          "type": "\"interested\" | \"objection\" | \"neutral\" | \"not_interested\"",
          "required": false,
          "description": "What their last inbound message meant — not a Pipeline stage. Combine with answered: \"no\" for interested replies you have not answered."
        },
        {
          "name": "seen",
          "type": "boolean",
          "required": false,
          "description": "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."
        },
        {
          "name": "q",
          "type": "string",
          "required": false,
          "description": "Case-insensitive match on the counterpart's NAME."
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "seatId": {
            "description": "One teammate's seat id, from `seat_list`. Defaults to your own.",
            "type": "string",
            "maxLength": 64
          },
          "scope": {
            "description": "'mine' (default) is your own seat; 'org' is every seat in the organization.",
            "type": "string",
            "enum": [
              "mine",
              "org"
            ]
          },
          "unread": {
            "description": "True returns only chats with unread messages.",
            "type": "boolean"
          },
          "answered": {
            "description": "'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.",
            "type": "string",
            "enum": [
              "yes",
              "no"
            ]
          },
          "leadsOnly": {
            "description": "Only chats already linked to a lead.",
            "type": "boolean"
          },
          "needsHuman": {
            "description": "Only chats where Kairon handed the conversation back and is waiting on you.",
            "type": "boolean"
          },
          "classification": {
            "description": "What their last inbound message meant — not a Pipeline stage. Combine with answered: \"no\" for interested replies you have not answered.",
            "type": "string",
            "enum": [
              "interested",
              "objection",
              "neutral",
              "not_interested"
            ]
          },
          "seen": {
            "description": "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.",
            "type": "boolean"
          },
          "q": {
            "description": "Case-insensitive match on the counterpart's NAME.",
            "type": "string",
            "maxLength": 200
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        }
      }
    },
    {
      "name": "chat_messages",
      "title": "Read a conversation",
      "description": "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.",
      "cluster": "inbox-outreach",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "chatId",
          "type": "string",
          "required": true
        },
        {
          "name": "cursor",
          "type": "string",
          "required": false
        },
        {
          "name": "limit",
          "type": "integer",
          "required": false
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "cursor": {
            "type": "string",
            "maxLength": 65536
          },
          "limit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 100
          }
        },
        "required": [
          "chatId"
        ]
      }
    },
    {
      "name": "inmail_send",
      "title": "Send an InMail",
      "description": "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.",
      "cluster": "inbox-outreach",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "target",
          "type": "object",
          "required": true,
          "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
        },
        {
          "name": "subject",
          "type": "string",
          "required": false,
          "description": "The InMail subject line — the one thing that decides whether it is opened."
        },
        {
          "name": "text",
          "type": "string",
          "required": true,
          "description": "The InMail body, as it will be sent. Over MCP, em dashes and curly quotes become plain punctuation; nothing else rewrites it."
        },
        {
          "name": "attachments",
          "type": "object[]",
          "required": false
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "target": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "url",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "A LinkedIn profile or company URL."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "public_identifier",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The vanity slug (e.g. \"jane-doe\") or company universal name."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "provider_id",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "Unipile/LinkedIn internal id (e.g. \"ACoAAB…\" or the Sales-Nav \"ACwAAB…\")."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              }
            ],
            "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
          },
          "subject": {
            "description": "The InMail subject line — the one thing that decides whether it is opened.",
            "type": "string",
            "maxLength": 200
          },
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 8000,
            "description": "The InMail body, as it will be sent. Over MCP, em dashes and curly quotes become plain punctuation; nothing else rewrites it."
          },
          "attachments": {
            "maxItems": 5,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "fileName": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 255,
                      "description": "The file name, with its extension — what the recipient sees."
                    },
                    "mimeType": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 255,
                      "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                    },
                    "dataBase64": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 4194304,
                      "description": "The file itself, base64-encoded. Use this OR `url`, never both."
                    }
                  },
                  "required": [
                    "fileName",
                    "mimeType",
                    "dataBase64"
                  ],
                  "additionalProperties": false
                },
                {
                  "type": "object",
                  "properties": {
                    "fileName": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 255,
                      "description": "The file name, with its extension — what the recipient sees."
                    },
                    "mimeType": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 255,
                      "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                    },
                    "url": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2048,
                      "format": "uri",
                      "description": "An https address we fetch server-side. Use this OR `dataBase64`, never both. Reach for it when you cannot produce base64."
                    }
                  },
                  "required": [
                    "fileName",
                    "mimeType",
                    "url"
                  ],
                  "additionalProperties": false
                }
              ]
            }
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "target",
          "text"
        ]
      }
    },
    {
      "name": "invite_send",
      "title": "Send a connection invitation",
      "description": "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.",
      "cluster": "inbox-outreach",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "target",
          "type": "object",
          "required": true,
          "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
        },
        {
          "name": "note",
          "type": "string",
          "required": false,
          "description": "The connection-request note, up to 300 characters. Omit it entirely for a note-less invite, which is valid and often accepts better."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "target": {
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "url",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "A LinkedIn profile or company URL."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "public_identifier",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The vanity slug (e.g. \"jane-doe\") or company universal name."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "provider_id",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "Unipile/LinkedIn internal id (e.g. \"ACoAAB…\" or the Sales-Nav \"ACwAAB…\")."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              }
            ],
            "description": "Who this acts on — one LinkedIn person or company, named by URL, vanity slug, or provider id."
          },
          "note": {
            "description": "The connection-request note, up to 300 characters. Omit it entirely for a note-less invite, which is valid and often accepts better.",
            "type": "string",
            "maxLength": 300
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "target"
        ]
      }
    },
    {
      "name": "message_delete",
      "title": "Delete (retract) a sent message",
      "description": "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.",
      "cluster": "inbox-outreach",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "chatId",
          "type": "string",
          "required": true
        },
        {
          "name": "messageId",
          "type": "string",
          "required": true,
          "description": "The message's `id` from chat_messages."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "messageId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "The message's `id` from chat_messages."
          }
        },
        "required": [
          "chatId",
          "messageId"
        ]
      }
    },
    {
      "name": "message_edit",
      "title": "Edit a sent message",
      "description": "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.",
      "cluster": "inbox-outreach",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "chatId",
          "type": "string",
          "required": true
        },
        {
          "name": "messageId",
          "type": "string",
          "required": true,
          "description": "The message's `id` from chat_messages."
        },
        {
          "name": "text",
          "type": "string",
          "required": true,
          "description": "The replacement body. It overwrites the message for the recipient too."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "chatId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "messageId": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "The message's `id` from chat_messages."
          },
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 8000,
            "description": "The replacement body. It overwrites the message for the recipient too."
          }
        },
        "required": [
          "chatId",
          "messageId",
          "text"
        ]
      }
    },
    {
      "name": "message_send",
      "title": "Send a direct message",
      "description": "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.",
      "cluster": "inbox-outreach",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "chatId",
          "type": "string",
          "required": false,
          "description": "Reply in this existing conversation (from chat_list). Use this, or `target`."
        },
        {
          "name": "target",
          "type": "object",
          "required": false,
          "description": "Open a NEW conversation with this person. Use this, or `chatId`."
        },
        {
          "name": "text",
          "type": "string",
          "required": true,
          "description": "The message body, as it will be sent. Em dashes and curly quotes become plain punctuation; nothing else rewrites it."
        },
        {
          "name": "attachments",
          "type": "object[]",
          "required": false
        },
        {
          "name": "cc",
          "type": "string[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "clientToken",
          "type": "string",
          "required": false,
          "description": "Your own idempotency key. Send the SAME token when retrying after a lost response and the message is not sent twice."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "chatId": {
            "description": "Reply in this existing conversation (from chat_list). Use this, or `target`.",
            "type": "string",
            "minLength": 1,
            "maxLength": 64
          },
          "target": {
            "description": "Open a NEW conversation with this person. Use this, or `chatId`.",
            "oneOf": [
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "url",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "A LinkedIn profile or company URL."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "public_identifier",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "The vanity slug (e.g. \"jane-doe\") or company universal name."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              },
              {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "const": "provider_id",
                    "description": "Which form `value` takes. Exactly one form per call — pick the one you actually have rather than reshaping it."
                  },
                  "value": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 512,
                    "description": "Unipile/LinkedIn internal id (e.g. \"ACoAAB…\" or the Sales-Nav \"ACwAAB…\")."
                  },
                  "kind": {
                    "description": "What `value` names, when already known — set for you on targets read off a list row. Leave unset when addressing something by hand; `linkedin_fetch`'s own `kind` overrides it. Ignored on a URL, which already says what it points at.",
                    "type": "string",
                    "enum": [
                      "profile",
                      "company"
                    ]
                  }
                },
                "required": [
                  "type",
                  "value"
                ]
              }
            ]
          },
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 8000,
            "description": "The message body, as it will be sent. Em dashes and curly quotes become plain punctuation; nothing else rewrites it."
          },
          "attachments": {
            "maxItems": 5,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "fileName": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 255,
                      "description": "The file name, with its extension — what the recipient sees."
                    },
                    "mimeType": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 255,
                      "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                    },
                    "dataBase64": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 4194304,
                      "description": "The file itself, base64-encoded. Use this OR `url`, never both."
                    }
                  },
                  "required": [
                    "fileName",
                    "mimeType",
                    "dataBase64"
                  ],
                  "additionalProperties": false
                },
                {
                  "type": "object",
                  "properties": {
                    "fileName": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 255,
                      "description": "The file name, with its extension — what the recipient sees."
                    },
                    "mimeType": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 255,
                      "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                    },
                    "url": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 2048,
                      "format": "uri",
                      "description": "An https address we fetch server-side. Use this OR `dataBase64`, never both. Reach for it when you cannot produce base64."
                    }
                  },
                  "required": [
                    "fileName",
                    "mimeType",
                    "url"
                  ],
                  "additionalProperties": false
                }
              ]
            }
          },
          "cc": {
            "description": "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.",
            "maxItems": 5,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 320,
              "format": "email",
              "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$"
            }
          },
          "clientToken": {
            "description": "Your own idempotency key. Send the SAME token when retrying after a lost response and the message is not sent twice.",
            "type": "string",
            "minLength": 1,
            "maxLength": 128
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "text"
        ]
      }
    },
    {
      "name": "identity_get",
      "title": "Who you are acting as",
      "description": "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.",
      "cluster": "acting-as",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "impersonate_list",
      "title": "List who you can act as",
      "description": "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.",
      "cluster": "acting-as",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "search",
          "type": "string",
          "required": false,
          "description": "Filter by name, email address or organization name."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "search": {
            "description": "Filter by name, email address or organization name.",
            "type": "string"
          }
        }
      }
    },
    {
      "name": "impersonate_start",
      "title": "Act as another user",
      "description": "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`.",
      "cluster": "acting-as",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "user",
          "type": "string",
          "required": true,
          "description": "The email address or user id to act as."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "user": {
            "type": "string",
            "minLength": 1,
            "description": "The email address or user id to act as."
          }
        },
        "required": [
          "user"
        ]
      }
    },
    {
      "name": "impersonate_stop",
      "title": "Stop acting as another user",
      "description": "Go back to being yourself. Succeeds whether or not you were acting as anyone, and reports which it was.",
      "cluster": "acting-as",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "seat_list",
      "title": "List the seats you can send from",
      "description": "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.",
      "cluster": "acting-as",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "allowance_get",
      "title": "Read the organization's AI balance",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "config_get",
      "title": "Read the operator's own material",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "sections",
          "type": "\"competitorUrls\" | \"sourceUrls\" | \"valueProposition\" | \"leadMagnet\" | \"learnings\"[]",
          "required": false,
          "description": "Which sections to read. Omit for all five."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "sections": {
            "description": "Which sections to read. Omit for all five.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "competitorUrls",
                "sourceUrls",
                "valueProposition",
                "leadMagnet",
                "learnings"
              ]
            }
          }
        }
      }
    },
    {
      "name": "config_set",
      "title": "Save the operator's own material",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "competitorUrls",
          "type": "string[]",
          "required": false,
          "description": "Replaces every competitor URL. Each must be a LinkedIn URL."
        },
        {
          "name": "sourceUrls",
          "type": "string[]",
          "required": false,
          "description": "Replaces every source URL. Any http(s) URL."
        },
        {
          "name": "valueProposition",
          "type": "string",
          "required": false,
          "description": "Replaces the organization's value proposition. An empty string clears it."
        },
        {
          "name": "leadMagnet",
          "type": "string",
          "required": false,
          "description": "Replaces the organization's lead magnet — what it gives a prospect to earn a reply. An empty string clears it."
        },
        {
          "name": "learnings",
          "type": "string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "competitorUrls": {
            "description": "Replaces every competitor URL. Each must be a LinkedIn URL.",
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 2048,
              "format": "uri"
            }
          },
          "sourceUrls": {
            "description": "Replaces every source URL. Any http(s) URL.",
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "string",
              "maxLength": 2048,
              "format": "uri"
            }
          },
          "valueProposition": {
            "description": "Replaces the organization's value proposition. An empty string clears it.",
            "type": "string",
            "maxLength": 5000
          },
          "leadMagnet": {
            "description": "Replaces the organization's lead magnet — what it gives a prospect to earn a reply. An empty string clears it.",
            "type": "string",
            "maxLength": 5000
          },
          "learnings": {
            "description": "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.",
            "type": "string",
            "maxLength": 20000
          }
        }
      }
    },
    {
      "name": "doc_get",
      "title": "Read one product doc",
      "description": "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\".",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "slug",
          "type": "string",
          "required": true,
          "description": "Which doc to read — a `slug` from `doc_list`, e.g. \"campaigns\"."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "slug": {
            "type": "string",
            "minLength": 1,
            "maxLength": 64,
            "description": "Which doc to read — a `slug` from `doc_list`, e.g. \"campaigns\"."
          }
        },
        "required": [
          "slug"
        ]
      }
    },
    {
      "name": "doc_list",
      "title": "List the product docs",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "email_domain_buy",
      "title": "Buy the sending domain",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "domain",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "mailboxes",
          "type": "object[]",
          "required": true,
          "description": "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."
        },
        {
          "name": "forwardingDomain",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "agreedTotal",
          "type": "number",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "minLength": 4,
            "maxLength": 253,
            "pattern": "^[a-z0-9][a-z0-9-]*(\\.[a-z0-9-]+)*\\.(com|org)$",
            "description": "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": {
            "minItems": 1,
            "maxItems": 5,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "emailAddressPrefix": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 64,
                  "description": "The part before the `@` — `ada` becomes `ada@the-domain.com`."
                },
                "firstName": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 100,
                  "description": "The mailbox owner's first name, as it appears in the From line of every email sent from it."
                },
                "lastName": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 100,
                  "description": "The mailbox owner's last name."
                }
              },
              "required": [
                "emailAddressPrefix",
                "firstName",
                "lastName"
              ]
            },
            "description": "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": {
            "description": "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.",
            "type": "string"
          },
          "agreedTotal": {
            "type": "number",
            "minimum": 0,
            "maximum": 10000,
            "description": "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."
          }
        },
        "required": [
          "domain",
          "mailboxes",
          "agreedTotal"
        ]
      }
    },
    {
      "name": "email_domain_check",
      "title": "Check a sending domain’s DNS records",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "domain",
          "type": "string",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "minLength": 4,
            "maxLength": 253,
            "pattern": "^[a-z0-9][a-z0-9-]*(\\.[a-z0-9-]+)+$",
            "description": "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."
          }
        },
        "required": [
          "domain"
        ]
      }
    },
    {
      "name": "email_domain_quote",
      "title": "What would this sending domain cost?",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "domain",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "mailboxes",
          "type": "object[]",
          "required": true,
          "description": "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."
        },
        {
          "name": "forwardingDomain",
          "type": "string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "minLength": 4,
            "maxLength": 253,
            "pattern": "^[a-z0-9][a-z0-9-]*(\\.[a-z0-9-]+)*\\.(com|org)$",
            "description": "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": {
            "minItems": 1,
            "maxItems": 5,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "emailAddressPrefix": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 64,
                  "description": "The part before the `@` — `ada` becomes `ada@the-domain.com`."
                },
                "firstName": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 100,
                  "description": "The mailbox owner's first name, as it appears in the From line of every email sent from it."
                },
                "lastName": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 100,
                  "description": "The mailbox owner's last name."
                }
              },
              "required": [
                "emailAddressPrefix",
                "firstName",
                "lastName"
              ]
            },
            "description": "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": {
            "description": "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.",
            "type": "string"
          }
        },
        "required": [
          "domain",
          "mailboxes"
        ]
      }
    },
    {
      "name": "email_domain_search",
      "title": "Is this sending domain available?",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "domain",
          "type": "string",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "minLength": 4,
            "maxLength": 253,
            "pattern": "^[a-z0-9][a-z0-9-]*(\\.[a-z0-9-]+)*\\.(com|org)$",
            "description": "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."
          }
        },
        "required": [
          "domain"
        ]
      }
    },
    {
      "name": "email_format_set",
      "title": "Send email as plain text or HTML",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "format",
          "type": "\"text\" | \"html\"",
          "required": true,
          "description": "`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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "format": {
            "type": "string",
            "enum": [
              "text",
              "html"
            ],
            "description": "`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."
          }
        },
        "required": [
          "format"
        ]
      }
    },
    {
      "name": "email_mailbox_add",
      "title": "Add a mailbox the customer already owns",
      "description": "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`.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "email",
          "type": "string",
          "required": true,
          "description": "The full address of a mailbox the customer already owns, like `ada@acme.com`."
        },
        {
          "name": "firstName",
          "type": "string",
          "required": true,
          "description": "The mailbox owner's first name — it appears in the From line of every send."
        },
        {
          "name": "lastName",
          "type": "string",
          "required": true,
          "description": "The mailbox owner's last name."
        },
        {
          "name": "provider",
          "type": "\"google\" | \"microsoft\" | \"custom\"",
          "required": true,
          "description": "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."
        },
        {
          "name": "password",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "smtpHost",
          "type": "string",
          "required": false,
          "description": "Outgoing mail server, `custom` provider only (Google and Microsoft are known)."
        },
        {
          "name": "smtpPort",
          "type": "integer",
          "required": false,
          "description": "Outgoing mail port, `custom` provider only — usually 465 or 587."
        },
        {
          "name": "imapHost",
          "type": "string",
          "required": false,
          "description": "Incoming mail server, `custom` provider only."
        },
        {
          "name": "imapPort",
          "type": "integer",
          "required": false,
          "description": "Incoming mail port, `custom` provider only — usually 993."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "maxLength": 254,
            "format": "email",
            "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
            "description": "The full address of a mailbox the customer already owns, like `ada@acme.com`."
          },
          "firstName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "The mailbox owner's first name — it appears in the From line of every send."
          },
          "lastName": {
            "type": "string",
            "minLength": 1,
            "maxLength": 100,
            "description": "The mailbox owner's last name."
          },
          "provider": {
            "type": "string",
            "enum": [
              "google",
              "microsoft",
              "custom"
            ],
            "description": "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": {
            "type": "string",
            "minLength": 1,
            "maxLength": 500,
            "description": "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": {
            "description": "Outgoing mail server, `custom` provider only (Google and Microsoft are known).",
            "type": "string",
            "maxLength": 253
          },
          "smtpPort": {
            "description": "Outgoing mail port, `custom` provider only — usually 465 or 587.",
            "type": "integer",
            "minimum": 1,
            "maximum": 65535
          },
          "imapHost": {
            "description": "Incoming mail server, `custom` provider only.",
            "type": "string",
            "maxLength": 253
          },
          "imapPort": {
            "description": "Incoming mail port, `custom` provider only — usually 993.",
            "type": "integer",
            "minimum": 1,
            "maximum": 65535
          }
        },
        "required": [
          "email",
          "firstName",
          "lastName",
          "provider",
          "password"
        ]
      }
    },
    {
      "name": "email_mailbox_limit_set",
      "title": "Set a mailbox’s daily sending limit",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "email",
          "type": "string",
          "required": true,
          "description": "The mailbox, as `email_setup_get` lists it under its domain."
        },
        {
          "name": "dailyLimit",
          "type": "integer",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
            "description": "The mailbox, as `email_setup_get` lists it under its domain."
          },
          "dailyLimit": {
            "type": "integer",
            "minimum": 1,
            "maximum": 30,
            "description": "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."
          }
        },
        "required": [
          "email",
          "dailyLimit"
        ]
      }
    },
    {
      "name": "email_mailbox_signature_set",
      "title": "Set a mailbox’s email signature",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "email",
          "type": "string",
          "required": true,
          "description": "The mailbox, as `email_setup_get` lists it under its domain."
        },
        {
          "name": "signature",
          "type": "string",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "pattern": "^(?!\\.)(?!.*\\.\\.)([A-Za-z0-9_'+\\-\\.]*)[A-Za-z0-9_+-]@([A-Za-z0-9][A-Za-z0-9\\-]*\\.)+[A-Za-z]{2,}$",
            "description": "The mailbox, as `email_setup_get` lists it under its domain."
          },
          "signature": {
            "type": "string",
            "maxLength": 1000,
            "description": "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."
          }
        },
        "required": [
          "email",
          "signature"
        ]
      }
    },
    {
      "name": "email_plan_request",
      "title": "Ask Kairon to set up email sending",
      "description": "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`.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "email_setup_get",
      "title": "Read the email sending setup",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "email_warmup_start",
      "title": "Start warmup on a domain’s mailboxes",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": true,
        "openWorld": true
      },
      "params": [
        {
          "name": "domain",
          "type": "string",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "domain": {
            "type": "string",
            "minLength": 4,
            "maxLength": 253,
            "pattern": "^[a-z0-9][a-z0-9-]*(\\.[a-z0-9-]+)+$",
            "description": "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."
          }
        },
        "required": [
          "domain"
        ]
      }
    },
    {
      "name": "feedback_report",
      "title": "Report a problem back to Kairon",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "problem",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "priority",
          "type": "\"urgent\" | \"high\" | \"medium\" | \"low\"",
          "required": true,
          "description": "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`."
        },
        {
          "name": "tools",
          "type": "object[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "quote",
          "type": "string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "problem": {
            "type": "string",
            "minLength": 20,
            "maxLength": 1500,
            "description": "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": {
            "type": "string",
            "enum": [
              "urgent",
              "high",
              "medium",
              "low"
            ],
            "description": "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": {
            "description": "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.",
            "maxItems": 10,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "description": "The Kairon tool name, e.g. `campaign_create`."
                },
                "error": {
                  "description": "The error text it returned, verbatim and untrimmed if short.",
                  "type": "string",
                  "maxLength": 500
                }
              },
              "required": [
                "name"
              ]
            }
          },
          "quote": {
            "description": "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.",
            "type": "string",
            "maxLength": 500
          }
        },
        "required": [
          "problem",
          "priority"
        ]
      }
    },
    {
      "name": "framework_get",
      "title": "Read go-to-market axioms",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "slug",
          "type": "string",
          "required": false,
          "description": "Which axiom to read — a `slug` from `framework_list`. An area name reads the whole area, like `area`."
        },
        {
          "name": "area",
          "type": "\"belief-system\" | \"targeting\" | \"outreach\" | \"content\" | \"loop\"",
          "required": false,
          "description": "Read every axiom of one area at once instead of one by `slug`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "slug": {
            "description": "Which axiom to read — a `slug` from `framework_list`. An area name reads the whole area, like `area`.",
            "type": "string",
            "minLength": 1,
            "maxLength": 96
          },
          "area": {
            "description": "Read every axiom of one area at once instead of one by `slug`.",
            "type": "string",
            "enum": [
              "belief-system",
              "targeting",
              "outreach",
              "content",
              "loop"
            ]
          }
        }
      }
    },
    {
      "name": "framework_list",
      "title": "List the go-to-market axioms",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "message_style_get",
      "title": "Read how the Author writes messages, in their own words",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "message_style_set",
      "title": "Save how the Author writes messages",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "text",
          "type": "string",
          "required": true,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "maxLength": 8000,
            "description": "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."
          }
        },
        "required": [
          "text"
        ]
      }
    },
    {
      "name": "skill_get",
      "title": "Read one skill",
      "description": "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`.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "name",
          "type": "string",
          "required": true,
          "description": "Which playbook to fetch — a `name` from `skill_list`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 80,
            "description": "Which playbook to fetch — a `name` from `skill_list`."
          }
        },
        "required": [
          "name"
        ]
      }
    },
    {
      "name": "skill_list",
      "title": "List the available skills",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "webhook_delete",
      "title": "Take a webhook endpoint down",
      "description": "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\" }`.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "webhook_deliveries",
      "title": "Read what an endpoint was sent, and what it answered",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "endpointId",
          "type": "string",
          "required": true,
          "description": "The endpoint whose recent deliveries to read."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "endpointId": {
            "type": "string",
            "minLength": 1,
            "description": "The endpoint whose recent deliveries to read."
          }
        },
        "required": [
          "endpointId"
        ]
      }
    },
    {
      "name": "webhook_list",
      "title": "Read the organization's webhook endpoints",
      "description": "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' }`.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "webhook_set",
      "title": "Register or change a webhook endpoint",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": false,
          "description": "Omit to register a new endpoint. Present to change that one."
        },
        {
          "name": "url",
          "type": "string",
          "required": false,
          "description": "Where to POST. Must be https and must resolve to a public address. Required when creating."
        },
        {
          "name": "description",
          "type": "string",
          "required": false,
          "description": "The operator's own label for it, e.g. 'Our HubSpot sync'."
        },
        {
          "name": "enabledEvents",
          "type": "\"lead.stage.changed\"[]",
          "required": false,
          "description": "Which event types to send. Defaults to all of them when creating."
        },
        {
          "name": "status",
          "type": "\"enabled\" | \"disabled_by_user\"",
          "required": false,
          "description": "Turn it off without deleting it, or back on. Re-enabling clears its failure streak."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "description": "Omit to register a new endpoint. Present to change that one.",
            "type": "string",
            "minLength": 1
          },
          "url": {
            "description": "Where to POST. Must be https and must resolve to a public address. Required when creating.",
            "type": "string",
            "maxLength": 2000,
            "format": "uri"
          },
          "description": {
            "description": "The operator's own label for it, e.g. 'Our HubSpot sync'.",
            "type": "string",
            "maxLength": 120
          },
          "enabledEvents": {
            "description": "Which event types to send. Defaults to all of them when creating.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "lead.stage.changed"
              ]
            }
          },
          "status": {
            "description": "Turn it off without deleting it, or back on. Re-enabling clears its failure streak.",
            "type": "string",
            "enum": [
              "enabled",
              "disabled_by_user"
            ]
          }
        }
      }
    },
    {
      "name": "webhook_test",
      "title": "Send a test event to a webhook endpoint",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "id",
          "type": "string",
          "required": true,
          "description": "The endpoint to send the sample event to."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "minLength": 1,
            "description": "The endpoint to send the sample event to."
          }
        },
        "required": [
          "id"
        ]
      }
    },
    {
      "name": "workspace_rename",
      "title": "Rename the workspace",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "name",
          "type": "string",
          "required": true,
          "description": "The workspace's new display name."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "name": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120,
            "description": "The workspace's new display name."
          }
        },
        "required": [
          "name"
        ]
      }
    },
    {
      "name": "writing_check_judge",
      "title": "Judge a draft against the rules, without sending it",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": false,
        "idempotent": false,
        "openWorld": false
      },
      "params": [
        {
          "name": "draft",
          "type": "string",
          "required": true,
          "description": "The exact text to judge, as it would go out."
        },
        {
          "name": "surface",
          "type": "\"post\" | \"comment\"",
          "required": true,
          "description": "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`."
        },
        {
          "name": "context",
          "type": "string",
          "required": false,
          "description": "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."
        },
        {
          "name": "template",
          "type": "string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "draft": {
            "type": "string",
            "minLength": 1,
            "maxLength": 20000,
            "description": "The exact text to judge, as it would go out."
          },
          "surface": {
            "type": "string",
            "enum": [
              "post",
              "comment"
            ],
            "description": "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": {
            "description": "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.",
            "type": "string",
            "maxLength": 20000
          },
          "template": {
            "description": "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.",
            "type": "string",
            "maxLength": 20000
          }
        },
        "required": [
          "draft",
          "surface"
        ]
      }
    },
    {
      "name": "writing_check_list",
      "title": "Read the rules every draft is held to",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "sections",
          "type": "\"general\" | \"post\" | \"comment\"[]",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "sections": {
            "description": "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.",
            "minItems": 1,
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "general",
                "post",
                "comment"
              ]
            }
          }
        }
      }
    },
    {
      "name": "writing_check_set",
      "title": "Save the Author's own rules for one section",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "section",
          "type": "\"general\" | \"post\" | \"comment\"",
          "required": true,
          "description": "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`."
        },
        {
          "name": "checks",
          "type": "object[]",
          "required": true,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "section": {
            "type": "string",
            "enum": [
              "general",
              "post",
              "comment"
            ],
            "description": "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": {
            "maxItems": 20,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 128
                },
                "description": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 2000,
                  "description": "The rule itself, phrased so a draft can be judged against it and either passes or does not — \"No em dashes anywhere\" works. A note about how the writing should feel gives nothing to test, so it silently never fires."
                }
              },
              "required": [
                "description"
              ]
            },
            "description": "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": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "section",
          "checks"
        ]
      }
    },
    {
      "name": "writing_check_shared_set",
      "title": "Switch one of Kairon's own rules on or off",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "key",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "enabled",
          "type": "boolean",
          "required": true,
          "description": "`false` switches Kairon's rule off for this Author; `true` switches it back on."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "key": {
            "type": "string",
            "minLength": 1,
            "description": "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": {
            "type": "boolean",
            "description": "`false` switches Kairon's rule off for this Author; `true` switches it back on."
          },
          "readReceipts": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "key",
          "enabled"
        ]
      }
    },
    {
      "name": "writing_style_get",
      "title": "Read how the Author comments, in their own words",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "writing_style_set",
      "title": "Save how the Author comments",
      "description": "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.",
      "cluster": "craft",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": true,
        "openWorld": false
      },
      "params": [
        {
          "name": "text",
          "type": "string",
          "required": true,
          "description": "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."
        },
        {
          "name": "readReceipts",
          "type": "string[] | string",
          "required": false,
          "description": "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."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "maxLength": 8000,
            "description": "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": {
            "description": "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.",
            "anyOf": [
              {
                "maxItems": 10,
                "type": "array",
                "items": {
                  "type": "string",
                  "maxLength": 512
                }
              },
              {
                "type": "string",
                "maxLength": 6000
              }
            ]
          }
        },
        "required": [
          "text"
        ]
      }
    },
    {
      "name": "post_media_upload",
      "title": "Upload files for a LinkedIn post",
      "description": "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.",
      "cluster": "presence",
      "annotations": {
        "readOnly": true,
        "destructive": false,
        "idempotent": true,
        "openWorld": false
      },
      "params": [],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {}
      }
    },
    {
      "name": "post_publish",
      "title": "Publish a LinkedIn post",
      "description": "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.",
      "cluster": "presence",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "text",
          "type": "string",
          "required": true,
          "description": "The post as it will read on LinkedIn, at most 3000 characters."
        },
        {
          "name": "media",
          "type": "object[]",
          "required": false,
          "description": "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."
        },
        {
          "name": "mentions",
          "type": "object[]",
          "required": false,
          "description": "People or companies to tag. Write `{{0}}`, `{{1}}`… in `text` where each goes."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "minLength": 1,
            "maxLength": 3000,
            "description": "The post as it will read on LinkedIn, at most 3000 characters."
          },
          "media": {
            "description": "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.",
            "maxItems": 20,
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string",
                      "maxLength": 512,
                      "pattern": "^post-media\\/.*",
                      "description": "A file id the upload box reported (`post_media_upload`)."
                    }
                  },
                  "required": [
                    "id"
                  ]
                },
                {
                  "anyOf": [
                    {
                      "type": "object",
                      "properties": {
                        "fileName": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 255,
                          "description": "The file name, with its extension — what the recipient sees."
                        },
                        "mimeType": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 255,
                          "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                        },
                        "dataBase64": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 4194304,
                          "description": "The file itself, base64-encoded. Use this OR `url`, never both."
                        }
                      },
                      "required": [
                        "fileName",
                        "mimeType",
                        "dataBase64"
                      ],
                      "additionalProperties": false
                    },
                    {
                      "type": "object",
                      "properties": {
                        "fileName": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 255,
                          "description": "The file name, with its extension — what the recipient sees."
                        },
                        "mimeType": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 255,
                          "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                        },
                        "url": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 2048,
                          "format": "uri",
                          "description": "An https address we fetch server-side. Use this OR `dataBase64`, never both. Reach for it when you cannot produce base64."
                        }
                      },
                      "required": [
                        "fileName",
                        "mimeType",
                        "url"
                      ],
                      "additionalProperties": false
                    }
                  ]
                }
              ]
            }
          },
          "mentions": {
            "description": "People or companies to tag. Write `{{0}}`, `{{1}}`… in `text` where each goes.",
            "maxItems": 50,
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 100,
                  "description": "The name as it shows in the post."
                },
                "profileId": {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 100,
                  "description": "The LinkedIn provider id `linkedin_fetch` returns: `ACo…` for a person, digits for a company."
                },
                "isCompany": {
                  "description": "true when the tag is a company page.",
                  "type": "boolean"
                }
              },
              "required": [
                "name",
                "profileId"
              ],
              "additionalProperties": false
            }
          }
        },
        "required": [
          "text"
        ]
      }
    },
    {
      "name": "profile_edit",
      "title": "Edit your own LinkedIn profile",
      "description": "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.",
      "cluster": "presence",
      "annotations": {
        "readOnly": false,
        "destructive": true,
        "idempotent": false,
        "openWorld": true
      },
      "params": [
        {
          "name": "headline",
          "type": "string",
          "required": false,
          "description": "The line under your name on LinkedIn. Omit to leave it as it is; there is no way to clear it from here."
        },
        {
          "name": "about",
          "type": "string",
          "required": false,
          "description": "The whole About section, replaced outright — send the full text you want, not an addition to it. Omit to leave it as it is."
        },
        {
          "name": "picture",
          "type": "object",
          "required": false,
          "description": "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."
        },
        {
          "name": "banner",
          "type": "object",
          "required": false,
          "description": "A new background banner, the wide image behind the top of the profile. Same two sources and same image types as `picture`."
        }
      ],
      "inputSchema": {
        "$schema": "https://json-schema.org/draft/2020-12/schema",
        "type": "object",
        "properties": {
          "headline": {
            "description": "The line under your name on LinkedIn. Omit to leave it as it is; there is no way to clear it from here.",
            "type": "string",
            "minLength": 1,
            "maxLength": 220
          },
          "about": {
            "description": "The whole About section, replaced outright — send the full text you want, not an addition to it. Omit to leave it as it is.",
            "type": "string",
            "minLength": 1,
            "maxLength": 2600
          },
          "picture": {
            "description": "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.",
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "fileName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "The file name, with its extension — what the recipient sees."
                  },
                  "mimeType": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                  },
                  "dataBase64": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4194304,
                    "description": "The file itself, base64-encoded. Use this OR `url`, never both."
                  }
                },
                "required": [
                  "fileName",
                  "mimeType",
                  "dataBase64"
                ],
                "additionalProperties": false
              },
              {
                "type": "object",
                "properties": {
                  "fileName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "The file name, with its extension — what the recipient sees."
                  },
                  "mimeType": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                  },
                  "url": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2048,
                    "format": "uri",
                    "description": "An https address we fetch server-side. Use this OR `dataBase64`, never both. Reach for it when you cannot produce base64."
                  }
                },
                "required": [
                  "fileName",
                  "mimeType",
                  "url"
                ],
                "additionalProperties": false
              }
            ]
          },
          "banner": {
            "description": "A new background banner, the wide image behind the top of the profile. Same two sources and same image types as `picture`.",
            "anyOf": [
              {
                "type": "object",
                "properties": {
                  "fileName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "The file name, with its extension — what the recipient sees."
                  },
                  "mimeType": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                  },
                  "dataBase64": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 4194304,
                    "description": "The file itself, base64-encoded. Use this OR `url`, never both."
                  }
                },
                "required": [
                  "fileName",
                  "mimeType",
                  "dataBase64"
                ],
                "additionalProperties": false
              },
              {
                "type": "object",
                "properties": {
                  "fileName": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "The file name, with its extension — what the recipient sees."
                  },
                  "mimeType": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 255,
                    "description": "The file's MIME type, e.g. `image/png` or `application/pdf`."
                  },
                  "url": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 2048,
                    "format": "uri",
                    "description": "An https address we fetch server-side. Use this OR `dataBase64`, never both. Reach for it when you cannot produce base64."
                  }
                },
                "required": [
                  "fileName",
                  "mimeType",
                  "url"
                ],
                "additionalProperties": false
              }
            ]
          }
        }
      }
    }
  ]
}
