---
name: linbox-connections
description: >-
  Queues LinkedIn connection invites through the Linbox MCP server —
  send, accept, and withdraw requests for single or batch targets, and finds
  1st-degree connections via linbox_search_persona (is_friend=true). Use when a
  request involves LinkedIn connections, invites, connection requests, accepting
  or withdrawing invites, checking who is already connected, or when calling
  linbox_send_connection_request, linbox_accept_connection_request, or
  linbox_withdraw_connection_request.
contract_version: "3.0"
---

# Linbox Connections

Queue-only connection actions on the Linbox LinkedIn MCP server. The three
connection tools never run Playwright inline — they always enqueue a
`connection_request` task for the worker and return a `task_id` (plus `batch_id`
for 2+ targets). Poll with `linbox_get_task` / `linbox_get_batch`.

Read the root [Linbox skill](../SKILL.md) first for the shared auth, identifier,
and response contract. This skill covers only the connection group.

## When to use

Use this skill when the request is about LinkedIn connections through Linbox:

- sending a connection invite (`linbox_send_connection_request`)
- accepting an incoming invite (`linbox_accept_connection_request`)
- withdrawing a pending outgoing invite (`linbox_withdraw_connection_request`)
- checking who is already a 1st-degree connection before inviting
  (`linbox_search_persona` with `is_friend=true`)
- polling the outcome of a queued connection task or batch

To check for new 1st-degree connections you haven't reviewed yet, call `linbox_whats_new`
first; `ack=true` marks every matching connection seen, not just the previewed ones.

## Prerequisites

- A Linbox MCP token scoped to one LinkedIn `account_id` (see root skill). Never
  pass an account id; the token scopes it.
- An **active LinkedIn session** for the account. Without it, every connection
  tool returns `status=blocked`, `meta.reason=account_session_not_active` and
  enqueues nothing.

## Tools

| Tool | Action | Targets |
|---|---|---|
| `linbox_send_connection_request(profile_url?, persona_id?, note?)` | `send` | single or batch |
| `linbox_accept_connection_request(profile_url?, persona_id?)` | `accept` | single or batch |
| `linbox_withdraw_connection_request(profile_url?, persona_id?)` | `withdraw` | single or batch |
| `linbox_search_persona(name?, linkedin_url?, tags?, is_friend?, campaign_id?, cursor?, limit_personas=30)` | check friends | cursor paginated |
| `linbox_get_task(task_id)` | poll one task | — |
| `linbox_get_batch(batch_id)` | poll a batch | — |

### Targeting: `profile_url` vs `persona_id`

All three connection tools accept `profile_url` and/or `persona_id`, each either a
single string or a list:

- **`profile_url`** — a LinkedIn URL or `public_id`. Use when you only have the
  LinkedIn link and no Linbox persona yet.
- **`persona_id`** — the Linbox UUID (`personas.id`), e.g. from
  `linbox_search_persona` or thread/inbox reads. Prefer this when available; it
  resolves without re-parsing a URL.
- At least one target is required, or the tool returns `Error: At least one
  profile_url or persona_id is required`.
- **Batch** = 2+ resolved targets in one call (lists, or mixing `profile_url` and
  `persona_id`). Duplicate targets resolving to the same profile are collapsed.
- `note` applies only to `send`; `accept` and `withdraw` ignore it.

## Workflow

1. **Check first (recommended for `send`).** Call `linbox_search_persona` with
   `is_friend=true` (optionally `name` / `linkedin_url` / `tags`) to see whether
   the target is already a 1st-degree connection. This lists connections imported
   by `sync_connections` from the DB — no live LinkedIn fetch. Paginate with
   `cursor` while `page.has_more=true`.
2. **Call the connection tool** with `profile_url` and/or `persona_id`. Pass a
   list for a batch.
3. **Read the per-target `status`** (see below). `already_connected` means no task
   was created; `pending` reuses an existing task.
4. **Capture ids.** Single target → `task_id`. Batch (2+) → `batch_id` plus one
   `task_id` per item.
5. **Poll to terminal.** `linbox_get_task(task_id)` for a single target;
   `linbox_get_batch(batch_id)` for a batch. Poll after `scheduled_in` /
   `scheduled_for`.

### Status outcomes (per target)

| `status` | Meaning | Task created? |
|---|---|---|
| `queued` | Enqueued; worker may run now (limit gate allowed it) | yes |
| `queued_deferred` | Enqueued with a future `due_date` (limit / work-hours gate, incl. the write pace shared with `linbox_send_message`) | yes |
| `pending` | A `todo`/`in_progress` `connection_request` task already exists for the same target + `action`; the existing `task_id` is returned | reused |
| `already_connected` | `persona_accounts.is_friend=true`; nothing to do | no |
| `blocked` | No active session (`meta.reason=account_session_not_active`) | no |

### Single vs batch responses

- **Single target** → `McpConnectionRequestResponse`: `action`, `status`,
  `profile_url`, `persona_id`, `task_id`, `task_status`, `scheduled_for` /
  `scheduled_for_ago`, `already_connected`, `meta.reason`. No `batch_id`.
- **2+ targets** → `McpConnectionBatchResponse`: `batch_id`, `total`, the counters
  `queued` / `queued_deferred` / `pending` / `already_connected` / `blocked`, and
  `items[]` (one `McpConnectionRequestResponse` per target). Every item shares the
  same `batch_id`.

### Polling a batch

`linbox_get_batch(batch_id)` aggregates all tasks sharing `tasks.batch_id`:

- Counts: `total`, `todo`, `in_progress`, `done`, `failed`, plus `items[]`.
- `overall_status`: `in_progress` (any running, or a done+failed mix) → `todo`
  (still queued) → `partial_failed` (some `done` and some `failed`) → `failed`
  (all failed) → `done` (all done); an empty/unknown batch → `not_found`.
- On a failed item, read its `output.error` / `output.error_type`.

### Invite outcome (`send`, once the task is `done`)

A `done` task is not proof an invite went out — read `output`:

| `output.status` | Meaning |
|---|---|
| `sent` | Connect was clicked and **Pending is visible** on the profile (`verified_pending=true`) |
| `already_pending` | Pending was already visible before clicking — an earlier invite is live (`verified_pending=true`). Not a reliable duplicate detector: on profiles that hide Pending in the overflow menu, a repeat send returns `sent` instead |
| `already_connected` | Profile shows a 1st-degree badge |
| `not_sent` | Nothing was sent — read `output.blocked_reason` (below) |

`not_sent` reasons, grouped by what to do next:

- **LinkedIn refused — back off for this account.** `invite_weekly_limit`: the
  account hit LinkedIn's *own* weekly invitation limit. Retrying sooner fails the
  same way; stop sending invites for this account until LinkedIn's weekly window
  resets.
- **Terminal for this profile — do not retry.** `invitations_not_accepted` (the
  member is not accepting invitations), `email_verification_required` (LinkedIn
  demands the target's email first), `connect_button_not_found` (no Connect, no
  overflow entry, no invite URL — this profile cannot be invited).
- **Retry only after fixing the cause.** `note_too_long` (shorten the note),
  `connect_click_failed`, `invite_modal_failed`, `pending_not_verified`.

## Rules

- Connection tools are **queue-only**: they always enqueue (or reuse/skip) and
  never scrape inline, never fall back to cache.
- Always require an active session; otherwise `blocked` +
  `account_session_not_active`, no task.
- **Invites share a write budget with DMs.** Invites and `linbox_send_message` are paced
  together (a combined interval plus hourly/daily caps, on top of the per-tool limit), so an
  invite queued right after a DM (or another invite) comes back `queued_deferred` with a
  later `scheduled_for`. That is normal — poll `linbox_get_task` / `linbox_get_batch`; never
  re-queue to get an earlier slot.
- `already_connected` short-circuits `send` — do not create a task for existing
  1st-degree connections. Prefer a `linbox_search_persona(is_friend=true)` check
  first to avoid redundant calls.
- Duplicate target + `action` returns the existing `pending` task; do not expect a
  new `task_id`. Reuse the returned one.
- A `done` `send` task is not a sent invite. Only `output.verified_pending=true` proves
  a live Pending invite; treat `not_sent` as "nothing went out" and retry only after
  fixing the cause (e.g. shorten an over-long note).
- `blocked_reason=invite_weekly_limit` is **LinkedIn's** restriction, not a Linbox
  limit deferral — Linbox will keep queueing invites normally, so it is on the agent
  to stop sending for this account until LinkedIn's weekly window resets.
- `batch_id` appears only for 2+ targets; a single target returns just `task_id`.
- `note` is only meaningful for `send`.
- A validation error is a plain string `"Error: ..."`, not JSON.

## Examples

**Check, then invite one person**

```
linbox_search_persona({"linkedin_url": "https://www.linkedin.com/in/johndoe/", "is_friend": true})
  # if personas[] returns the target → already connected, skip the invite
linbox_send_connection_request({"profile_url": "https://www.linkedin.com/in/johndoe/", "note": "Great to connect!"})
  # status=queued|queued_deferred → capture task_id
linbox_get_task("<task_id>")   # done -> check output.status + output.verified_pending
```

**Batch invite by persona_id**

```
linbox_send_connection_request({"persona_id": ["<uuid-a>", "<uuid-b>", "<uuid-c>"]})
  # 3 targets → batch_id + items[]; note per-item status
linbox_get_batch("<batch_id>")
  # overall_status: in_progress → todo → partial_failed → failed → done
```

**Accept incoming, withdraw a stale invite**

```
linbox_accept_connection_request({"profile_url": "https://www.linkedin.com/in/janedoe/"})
linbox_withdraw_connection_request({"persona_id": "<uuid>"})
  # each single target → task_id; poll with linbox_get_task
```

**List 1st-degree connections (paginated)**

```
linbox_search_persona({"is_friend": true, "limit_personas": 50})
  # while page.has_more: repeat with cursor=page.next_cursor
```

## Related skills

- [Linbox root](../SKILL.md) — auth, identifiers, meta/task/guidance, cursor
  pagination, task polling (global contract).
- [Messaging](../messaging/SKILL.md) — inbox, threads, `linbox_send_message`
  (which can queue a connection first via `queued_connection_first`).
- [Enrichment](../enrichment/SKILL.md) — `linbox_scrap_profile` to resolve a
  persona and its `is_friend` / `connection_degree` before inviting.
- [Tags](../tags/SKILL.md) — tag personas to organize connection targets.
- [Posts](../posts/SKILL.md) — find invite targets among the actors of a post.
