---
name: linbox
description: >-
  Operate the Linbox LinkedIn MCP server — check what's new, read inbox/threads, read one
  cached profile/post/company, search threads, personas and posts, manage tags, run
  multi-step outreach campaigns, and queue every LinkedIn action (messages, invites,
  scrapes, likes, comments) with the single enqueue tool linbox_create_task. Use whenever
  a request involves LinkedIn messaging, inbox, DMs, connections/invites, LinkedIn tags,
  posts/feed, outreach campaigns/sequences, or profile/company scraping through Linbox, or
  when calling any linbox_* MCP tool. This root skill holds the global contract (auth,
  identifiers, create_task types and params, entity reads vs task reads, meta/task/guidance,
  cursor pagination, task polling) and routes to topic skills.
contract_version: "5.2"
---

# Linbox

Thin router and global contract for the Linbox LinkedIn MCP server (`/mcp`, FastMCP).
All `linbox_*` tools share one auth model, one identifier vocabulary, and one response
contract. Read this first, then jump to the topic skill for the task.

The shape of the API is three groups:

- **One enqueue tool** — `linbox_create_task(type, params)`. Every LinkedIn side effect
  (message, invite, scrape, like, comment) goes through it. There is no other write path.
- **Entity reads** — `linbox_get_profile`, `linbox_get_post`, `linbox_get_company`,
  `linbox_get_thread`, `linbox_get_inbox`, and the `linbox_search_*` tools. They serve the
  cache and never need a `task_id`; on a miss the three single-entity gets queue the scrape
  for you.
- **Task reads** — `linbox_get_task` (one task or a whole batch) and `linbox_list_tasks`.

## When to use

Use this skill when a request touches LinkedIn through Linbox, including:

- asking what's new / what needs attention across messages, feed posts, connections,
  campaign replies, and campaign leads waiting on the agent
- reading the inbox or a specific conversation/thread
- reading one person's profile, one post, or one company page from the cache
- searching threads, people (personas), or posts
- listing, creating, or attaching/removing tags
- sending a message, invite, accept, or withdraw
- scraping a profile, company, the feed, or one post; liking or commenting on a post
- setting up or running a multi-step outreach campaign / sequence, enrolling personas
- polling a queued task or batch

If the task is narrow, go straight to the matching topic skill under
[Related skills](#related-skills). Always apply the global rules below regardless of tool.

## Prerequisites

- A Linbox MCP token scoped to exactly one LinkedIn `account_id`. The token binds the
  account, so no separate account parameter is ever passed.
- The MCP client is connected to `<api-host>/mcp` with auth configured (see below).

### Auth contract

Authenticate with **one** of:

- **Bearer header (preferred):** `Authorization: Bearer <mcp_token>`
- **Query param fallback (clients without custom headers):** `<api-host>/mcp?token=<mcp_token>`

If both are present, the header wins. Prefer the header — `?token=` can leak into access
logs, browser history, and referrers.

### Response format

Optional header `X-Linbox-Response-Format: markdown | json` (default `markdown`). Set
`json` for programmatic parsing; every field name below matches the JSON shape.

## Tools

| Tool | Group | Purpose |
|---|---|---|
| `linbox_whats_new` | core | Cache-only digest: unread messages, new feed posts, new connections, campaign replies |
| `linbox_create_task` | core | **The only enqueue tool** — queue any LinkedIn action by `type` + `params` |
| `linbox_get_task` | core | What one task did, by `task_id`, or a batch view by `batch_id` |
| `linbox_list_tasks` | core | Recent tasks this agent can enqueue (cursor paginated) |
| `linbox_get_inbox` | messaging | List cached threads; enqueues a background refresh |
| `linbox_get_thread` | messaging | Cached thread + messages; marks read; enqueues a refresh |
| `linbox_search_thread` | messaging | Search threads in DB (cursor paginated) |
| `linbox_search_persona` | messaging | Search personas / list friends; `campaign_id` scopes to a campaign's enrollees, `post_url` or `activity_id` lists a post's actors with `roles` and each commenter's `comment` (cursor paginated) |
| `linbox_get_profile` | enrichment | One profile of any age + `current_companies`; a miss queues a short scrape |
| `linbox_get_company` | enrichment | One company (global, not per account); a miss queues a scrape |
| `linbox_get_post` | posts | One post: text, author, url, actions, our comment, tags; a `post_url` miss queues a scrape |
| `linbox_search_post` | posts | Search cached posts (cursor paginated) |
| `linbox_list_tags` | tags | List account tags with description/color/count |
| `linbox_upsert_tag` | tags | Create tag or update metadata (no attachment) |
| `linbox_add_persona_tags` / `linbox_remove_persona_tags` | tags | Attach/detach tags on a persona |
| `linbox_add_thread_tags` / `linbox_remove_thread_tags` | tags | Attach/detach tags on a thread |
| `linbox_add_post_tags` / `linbox_remove_post_tags` | tags | Attach/detach tags on a cached post |
| `linbox_list_campaigns` | campaigns | List campaigns (`status`, `steps_count`, invite cap) |
| `linbox_upsert_campaign` | campaigns | Create/update a campaign and its `steps` (`message`, `scrap_profile`, `like_recent`, `wait_agent`) |
| `linbox_set_campaign_status` | campaigns | `draft` / `active` / `paused` / `done` |
| `linbox_enroll_campaign` | campaigns | Enroll personas by `profile_urls` / `persona_ids` |
| `linbox_list_campaign_personas` | campaigns | Inspect enrollment state; `waiting_agent` / `step_ids` filters (cursor paginated) |
| `linbox_update_campaign_persona` | campaigns | Only per-enrollment write: merge `texts` / `vars`, complete a `wait_agent` step, stop/skip/reactivate |

### Identifiers

| Concept | Identifiers | Notes |
|---|---|---|
| Thread | `thread_id`, `linkedin_thread_ref`, `profile_url` | `thread_id` = Linbox UUID (`threads.id`); `linkedin_thread_ref` = thread URL or `thread_source_id`. If both are passed, `thread_id` wins and the ref is validated. `linbox_get_thread` also accepts `profile_url` to open a person's thread (cache miss → `thread=null` + `meta.reason="thread_not_in_cache"`, not an error). Responses also expose `thread_source_id`. |
| Person | `persona_id`, `profile_url`, `linkedin_url` | `persona_id` = Linbox UUID; `profile_url`/`linkedin_url` = LinkedIn URL or `public_id`. Responses also expose `partner_public_id`. |
| Post | `post_url`, `activity_id` | `post_url` = `linkedin.com/posts/...` or `linkedin.com/feed/update/urn:li:{activity\|ugcPost\|share}:{id}` (normalized before dedup); `activity_id` = Linbox UUID (`activities.id`) from `linbox_search_post`. Post tools require the post to be cached for the account. |
| Company | `company_url` | LinkedIn company URL. Companies are **global**, shared across accounts. |
| Async work | `task_id`, `batch_id` | `task_id` = one queued task (`tasks.id`); `batch_id` = shared id for a 2+ target call. Both are read with `linbox_get_task`. |

## `linbox_create_task(type, params)`

Enqueue-only — it never runs the browser inline and never returns entity bodies. The
signature never grows: everything type-specific lives in `params`.

| `type` | `params` |
|---|---|
| `send_message` | `text`, plus one of `thread_id`, `linkedin_thread_ref`, `profile_url`, `persona_id` |
| `connect` | one of `profile_url`, `persona_id` (string or list); optional `text` = invite note |
| `accept_connection` | one of `profile_url`, `persona_id` (string or list); no `text` |
| `withdraw_connection` | one of `profile_url`, `persona_id` (string or list); no `text` |
| `scrap_profile` | one of `profile_url`, `persona_id` (string or list); optional `sections`, `full`, `short`, `activities_limit`, `activities_since_days` |
| `scrap_company` | `company_url` (string or list) |
| `scrap_feed` | optional `limit`, integer 1–200, default 30 |
| `search` | `query` (≤200 chars); optional `limit` = posts per run, 1–20 (omit = account cap: 10 low/on_demand, 20 high; above cap = error, not queued), `entities` (default `["posts"]`; only `posts` is implemented — others are an error, not queued) |
| `scrap_post` | `post_url`; optional `sections` (`comments`, `reactions`; `null`/`[]` = body only), `comments_limit`, `comments_sort` (`relevant` \| `recent`), `reactions_limit` |
| `like_post` | `post_url` |
| `comment_on_post` | `post_url`, `text` |

An unknown `type`, a missing required key, or an **extra** key is a validation error
(`Error: type=<name> requires ...`). Nothing is silently ignored.

**Response:** `type`, `status`, `batch_id` (**only** when 2+ targets were queued),
`tasks[]`, `guidance`, `meta`. Each `tasks[]` item carries `task_id`, `task_key`,
`task_status`, `scheduled_for(_ago)`, `estimated_done_for`, `estimated_done_in`, `reason`,
and the target (`profile_url` / `persona_id` / `post_url` / `thread_id` / `company_url`).
Entity bodies are never here — read them with a get/search tool.

**`status` values:** `queued`, `queued_deferred` (limit or work-hours gate), `pending`
(identical task already queued — reuse its `task_id`), `cache_hit` (`scrap_profile` on a
fresh cache: nothing queued, no quota spent), `blocked` (no active session, nothing
queued), `partial` (mixed per-target outcomes in a batch). `send_message` adds
`checking_profile` and `queued_connection_first`, plus `already_connected` for `connect`.

## Entity via get/search, task via get_task

This is the central rule of the contract. A task tells you *what happened*; the entity
lives behind its own read tool.

| Queued `type` | Read the entity with |
|---|---|
| `scrap_profile` | `linbox_get_profile` |
| `scrap_company` | `linbox_get_company` |
| `scrap_feed`, `search` | `linbox_search_post` |
| `scrap_post` | `linbox_get_post` (+ `linbox_search_persona(post_url=...)` for comments/reactions) |
| `like_post`, `comment_on_post` | `linbox_get_post` |
| `send_message` | `linbox_get_thread` |
| `connect`, `accept_connection`, `withdraw_connection` | `linbox_search_persona` |

`linbox_get_task` returns this pointer as `next`, so you never have to guess.

### Entity reads

`linbox_get_profile(profile_url | persona_id)`, `linbox_get_post(post_url | activity_id)`,
and `linbox_get_company(company_url)` share one contract:

Just call the get — don't `linbox_create_task` first.

- **Cached → use it, at any age.** `meta.reason="cached"`, `task=null`; the age is
  `meta.as_of` / `meta.as_of_ago`. Old data never triggers a refresh. Want newer or deeper
  data? That is an explicit `linbox_create_task`.
- **Not cached → the get queues a default scrape** and returns it on `task`: poll
  `task.task_id` with `linbox_get_task`, then call the same get again. Defaults: profile =
  short `scrap_profile`, post = body only, company = `scrap_company`. An unfinished task for
  the same URL is returned instead of a new one (`meta.reason="pending_task"`).
- **Nothing queued** (`profile` / `post` / `company` = `null`, `task=null`) — branch on
  `meta.reason`, don't retry the read: `no_scrape_url` (`linbox_get_post(activity_id)` miss
  — pass `post_url`), `account_session_not_active` (reconnect the account).
- `guidance` states the age or the task to poll — follow it.

Threads stay on `linbox_get_inbox` / `linbox_get_thread`. `linbox_search_persona`,
`linbox_search_post`, and `linbox_search_thread` stay list/filter tools —
`linbox_search_persona` is **not** a replacement for `linbox_get_profile`.

## Workflow

1. **Start open-ended requests with `linbox_whats_new`** ("what's new", "anything to do",
   start of a work session) to see where the work is before picking a group.
2. **Classify the request** into a group: messaging, tags, connections, enrichment,
   posts, or campaigns.
3. **Open the topic skill** for that group ([Related skills](#related-skills)) for the
   `params` details, edge cases, and decision flows.
4. **Read before write.** Get the right identifier from a read/search tool before queueing
   a `send_message` or `connect`.
5. **Check freshness** on every read via `meta` (see below); don't assume data is live.
6. **Compare skill contract version** before relying on this file (see Skill version sync).
7. **After `linbox_create_task`, capture** `task_id` (and `batch_id` when batched), poll
   `linbox_get_task` until terminal, then read the entity with the tool `next` names.

### Skill version sync

Every MCP response includes `meta.contract_version` (`MAJOR.MINOR`). Compare it to the
`contract_version` in this skill's frontmatter.

- **Remote MAJOR is higher than local — stop.** Do not call any further `linbox_*` tool.
  Tools have been removed or renamed in ways these skills describe incorrectly, so
  anything you do next is guesswork. Tell the user to re-download the skills from the hub
  (`/skills` or `linbox-skills.zip`), then continue with the new pack.
- **Only remote MINOR is higher** — re-download is recommended; your local copy may miss
  new guidance but still works.
- **Local matches or is newer on MAJOR** — proceed with the installed skills.

### Cache-first reads vs enqueue-only writes

- **Reads** always answer from the Linbox DB — `meta.data_source` is `cache`. LinkedIn is
  never scraped inline.
- `linbox_get_inbox` and `linbox_get_thread` may **enqueue a background refresh**
  (subject to limits and an active session) and return it as a pollable `task` alongside
  the cached data. `linbox_get_profile`, `linbox_get_post`, and `linbox_get_company`
  enqueue **only on a cache miss** (see Entity reads). `linbox_whats_new`,
  `linbox_list_tasks`, and every `linbox_search_*` never enqueue.
- **Writes** never fall back to cache and never run Playwright inline. `linbox_create_task`
  returns queued work, or `status="blocked"` + `meta.reason` when the session is inactive.

### What's new digest

`linbox_whats_new(ack=False, limit_per_section=10)` is the session entrypoint. It is
**cache-only** and enqueues nothing — it never scrapes and never queues a refresh.

Returns `summary` (one line), `guidance`, and five sections — `messages`, `posts`
(feed posts within a 7-day `horizon_days`), `connections`, `campaigns` (replies that
stopped a campaign enrollment), `campaign_waiting` (leads parked by a `wait_agent`
campaign step) — each with `count`, `capped`, a preview `items[]` (`limit_per_section`
clamp 1–10), and `next` naming the tool to widen with. Use `next`, not the preview,
when you need the full set.

- **`ack=true` clears all matching posts, connections, and campaign replies**, not just
  the previewed items.
- **Messages are never acked here.** An unread thread clears only when you open it with
  `linbox_get_thread`.
- **`campaign_waiting` is live and never acked.** Its items are aggregates
  (`campaign_id`, `campaign_name`, `count`); the count drops only once you resolve the
  enrollments via `linbox_update_campaign_persona` (see
  [Campaigns](campaigns/SKILL.md)).
- `campaigns` items carry `campaign_persona_id`, `campaign_id`, `campaign_name`,
  `persona_id`, `profile_url`, `replied_at` — open the conversation before acking. A
  reply shows up here once it is in the cache (message persist or the next campaign
  tick), not when that enrollment's next follow-up is due.
- To pull fresh data, call the refresh paths yourself: `linbox_get_inbox`,
  `linbox_create_task(type="scrap_feed")`, `linbox_search_persona(is_friend=true)`.

The same one-line summary appears as `meta.whats_new_hint` on poll and read tools
(`linbox_get_task`, `linbox_get_inbox`, `linbox_get_thread`, `linbox_search_thread`,
`linbox_search_post`, `linbox_search_persona`). It is omitted / `null` when all counts are
zero — treat it as a nudge to call `linbox_whats_new`, not as data to act on directly.

### meta / task / guidance

Every response carries `meta`:

- `data_source`: `live | cache`
- `contract_version`: `MAJOR.MINOR` of the live MCP contract (see Skill version sync)
- `fetched_at`, `fetched_ago`, `as_of`, `as_of_ago`
- `reason`: why live was skipped, e.g. `per_hour`, `per_day`, `outside_work_hours`,
  `account_session_not_active`, `browser_service_unavailable`
- `whats_new_hint`: optional one-line digest summary (see What's new digest)

`linbox_get_inbox`, `linbox_get_thread`, `linbox_get_profile`, `linbox_get_post`, and
`linbox_get_company` may also return:

- `task` (`McpEnqueuedTask | null`): `task_id`, `task_key`, `task_status`,
  `scheduled_for`, `scheduled_in`, `estimated_done_for`, `estimated_done_in`, `reason` —
  present when a refresh was enqueued.
- `guidance`: one human-readable string describing the next step; when `task` is set it
  names the `task_id` to poll and states the expected-done ETA.

Rule: don't tell the user "can't"; read `guidance` and `task` for the concrete next step.

### Cursor pagination

`linbox_search_thread`, `linbox_search_persona`, `linbox_search_post`, and
`linbox_list_tasks` use an opaque **keyset cursor** (not offset):

- **Input:** `cursor` — omit on the first call; pass the previous `page.next_cursor` after.
  An invalid token returns `Error: ...`.
- **Output block `page`:** `limit` (clamp 1–100; 1–50 for `linbox_list_tasks`), `returned`,
  `has_more`, `next_cursor` (`null` when `has_more=false`).
- **Loop:** while `page.has_more`, repeat with the same filters and `cursor=page.next_cursor`.

`linbox_list_campaign_personas` paginates the same way but returns `has_more`,
`next_cursor`, and `returned` at the top level instead of inside a `page` block.

### Task polling

`linbox_create_task` returns `task_id` per target and `batch_id` for 2+ targets.

- **`linbox_get_task(task_id=...)`** → what that task did: `task_status`
  (`todo | in_progress | done | failed`), `scheduled_for(_ago)`, `started_at`,
  `finished_at`, related ids (`thread_id`, `persona_id`, `profile_url`, `batch_id`),
  `task_payload`, `output` (the worker result), and `next` — the get/search that reads the
  entity. System tasks (`scrap_inbox`, `scrap_thread`, …) are readable by `task_id` too.
- **`linbox_get_task(batch_id=...)`** → the batch view: counts (`total`, `todo`,
  `in_progress`, `done`, `failed`), `overall_status`, and per-task `items[]`. Pass
  **exactly one** of `task_id` / `batch_id`.
- `overall_status`: `in_progress` (any running or a done/failed mix) → `todo` (queued) →
  `failed` (all failed) → `done` (all done); empty batch → `not_found`.
- Poll after `scheduled_in` / `scheduled_for`; on `failed` read `output.error` /
  `output.error_type` / `output.reason`; a future `scheduled_for` means a limit or
  work-hours gate.
- **Transient browser errors are retried server-side** on every LinkedIn browser task
  (scrapes, message sends, invites, likes, comments). A Playwright timeout or proxy blip
  requeues the same task: the poll shows `task_status="todo"` with `output.attempt`,
  `output.retriable=true`, and the last `error` — that is **not** a failure, keep polling
  and never re-enqueue. After 3 attempts it settles as `failed` with
  `output.retries_exhausted=true`. Hard failures (bad input, lost session) fail
  immediately and carry no `attempt`.
- `output.reason="linkedin_session_logged_out"` means the LinkedIn session hit a
  login/checkpoint/authwall page and the account was deactivated — the user must
  reconnect before any further work.

**`linbox_list_tasks(type=None, status=None, limit=20, cursor=None)`** lists only the keys
an agent can enqueue — `send_message`, `connection_request`, `scrap_profile`,
`scrap_company`, `scrap_feed`, `search`, `scrap_post`, `like_post`, `comment_on_post`. System keys
(`scrap_inbox`, `scrap_thread`, `scrap_thread_backfill`, `sync_connections`,
`deliver_webhook`, `mark_thread_read`, `check_and_scrap_account`) never appear. `type` is
an agent type (`connect` matches invites only, not accepts or withdraws), `status` is
`todo | in_progress | done | failed`, `limit` is 1–50 (default 20), `cursor` is a keyset.
Items carry `campaign_id` when the task came from a campaign. It lists tasks; what a task
did is `linbox_get_task`, and the entity is a get/search.

## Rules

- Before invoking tools, compare local `contract_version` (frontmatter) with
  `meta.contract_version`; **stop and re-download** when remote MAJOR is higher.
- **One enqueue tool.** Every LinkedIn side effect is `linbox_create_task(type, params)`.
  The old per-action tools (`linbox_send_message`, `linbox_scrap_profile`,
  `linbox_like_post`, `linbox_get_batch`, …) are gone with no aliases — calling them is an
  error, not a fallback.
- **Entity via get/search, task via `linbox_get_task`.** Never expect a profile, post,
  company, or message body in a `linbox_create_task` or `linbox_get_task` response.
- A get tool returning `null` means "not in cache", not a failure. If `task` is set, poll
  it and call the get again; report `meta.as_of` when freshness matters.
- Prefer the Bearer header over `?token=`.
- Never pass an account id; the token already scopes the account.
- A validation error is a plain string `"Error: ..."`, not JSON — handle both shapes.
- HTTP `403 Account is inactive` means the account is deactivated (e.g. its LinkedIn
  session was lost) and **every** tool will fail until the user reconnects it via the
  Linbox extension/dashboard. Don't retry or route around it — report that reconnect is
  needed; check a recent failed task's `output.reason` to confirm a logout episode.
- **The account working window is work days + work hours** (default Mon–Fri in the
  account timezone). Every queued LinkedIn task is gated by it: work enqueued outside the
  window defers with `outside_work_hours` and a `scheduled_for` on the next work day —
  expected, not a failure. Never promise a weekend send unless the account's work days
  include `sat`/`sun`.
- **Messages and invites share one write budget.** `send_message` and the connection types
  are paced together (a combined minimum interval with jitter, plus hourly/daily caps) on
  top of each type's own limits, so a DM and an invite cannot go out back-to-back — the
  second is deferred to a later slot. `status="queued_deferred"` with a distant
  `scheduled_for` is the normal outcome, not a failure: poll `linbox_get_task`, then
  re-read the entity. **Never retry a write in a loop** — a repeat call only adds another
  queued task, and there is no flag to bypass limits. Likes, comments, and thread-read
  marks are outside this bucket.
- **`scrap_profile` has two independent daily budgets.** Short scrapes (`short=true`,
  `sections=[]`, first page only) and full scrapes (`full=true` or any activity /
  `current_company` section) are metered separately — 150 short vs 50 full per day by
  default — with a shared minimum interval, so running out of one does not block the other.
  A deferred scrape names its bucket in `output.limit_mode`. Prefer `short` for bulk
  enrichment; see [Enrichment](enrichment/SKILL.md).
- Only pass `ack=true` to `linbox_whats_new` after actually reviewing
  posts/connections/campaign replies — it clears every matching item, not just the
  preview, and cannot be undone.
- Campaign tools are config-only: they write campaigns/enrollments in the DB and return
  no `task_id`. Sending happens later via a server-side tick under the limit gate, and
  only while the campaign is `active`.
- `batch_id` appears only for 2+ targets in one call; a single target returns just
  `task_id`. Both are read with `linbox_get_task`.
- `connect` returns `already_connected` (no task) when the persona is already a
  1st-degree connection, and `pending` reuses the existing task for a duplicate
  target + action.
- Keep this root concise; consult the topic skill before invoking a group's tools.

## Examples

**Open a work session**

```
linbox_whats_new()                         # messages/posts/connections/campaigns counts
  # drill into a section with its `next`, e.g. linbox_search_thread(reply_status="unread")
linbox_whats_new({"ack": true})            # only after reviewing non-message sections
```

**Read a conversation by tag**

```
linbox_list_tags()                         # pick a real tag name
linbox_search_thread({"tags": ["lead"], "reply_status": "unread"})
  # take threads[].id; if page.has_more, repeat with cursor=page.next_cursor
linbox_get_thread({"thread_id": "<uuid>"}) # full messages[]
```

**Queue a send, poll it, read the result**

```
linbox_create_task({"type": "send_message",
                    "params": {"thread_id": "<uuid>", "text": "..."}})
  # tasks[0].task_id; status queued | queued_deferred | blocked
linbox_get_task({"task_id": "<task_id>"})  # poll to done/failed; `next` = linbox_get_thread
linbox_get_thread({"thread_id": "<uuid>"}) # the message itself lives here
```

**Read a person (the get queues the scrape on a miss)**

```
linbox_get_profile({"profile_url": "https://www.linkedin.com/in/janedoe/"})
  # profile present -> use it; note meta.as_of_ago (no scrape was queued)
  # profile=null + task -> linbox_get_task({"task_id": task.task_id}) until done,
  #   then linbox_get_profile again
  # need newer/deeper data than the cache -> linbox_create_task(type="scrap_profile")
```

## Related skills

Topic skills (one level deep, same tree):

- [Messaging](messaging/SKILL.md) — inbox, threads, search, `send_message`.
- [Tags](tags/SKILL.md) — `linbox_list_tags`, `linbox_upsert_tag`, add/remove persona,
  thread & post tags, tag-based search (OR semantics, persona→thread inheritance).
- [Connections](connections/SKILL.md) — `connect` / `accept_connection` /
  `withdraw_connection`, single & batch, `already_connected`, pending reuse.
- [Enrichment](enrichment/SKILL.md) — `linbox_get_profile` / `linbox_get_company` reads and
  the `scrap_profile` / `scrap_company` types (`sections`, freshness, budgets).
- [Posts](posts/SKILL.md) — `linbox_get_post`, `linbox_search_post`, the `scrap_feed` /
  `scrap_post` / `like_post` / `comment_on_post` types, post tags, and
  `linbox_search_persona(post_url=...)` for a post's actors.
- [Campaigns](campaigns/SKILL.md) — multi-step outreach sequences: campaign steps,
  enrollment, per-persona texts, `wait_agent` handoffs, and how replies stop an
  enrollment.

For end-to-end recipes, see the playbooks index at [`playbooks/SKILL.md`](../playbooks/SKILL.md).
