← Back to Plugins
Integration

Unipile

CassiaResearch By CassiaResearch 👁 8 views ▲ 0 votes

Unipile LinkedIn automation plugin for OpenClaw β€” messaging, connections, profile reads, Sales Navigator search, with hard-enforced daily quotas and human-like pacing.

GitHub

Install

openclaw plugins install openclaw-unipile

Configuration Example

{
  "plugins": {
    "openclaw-unipile": {
      "enabled": true,
      "dsn": "https://apiXX.unipile.com:443XX",
      "apiKey": "...",
      "accountId": "...",
      "accountTier": "sales_navigator"
    }
  }
}

README

# Unipile LinkedIn plugin for OpenClaw

Wraps the [`unipile-node-sdk`](https://www.npmjs.com/package/unipile-node-sdk) to give the agent LinkedIn reach β€” messaging, connections, profile lookup, and Sales Navigator search β€” against a single, already-connected LinkedIn account.

**Defaults to Sales Navigator** for search and profile reads. Enforces daily, weekly, and monthly quotas per LinkedIn account, minimum call spacing, working-hours windows, per-tool polling cooldowns, and jitter. All outbound Unipile calls are serialized through an async mutex so no two actions can fire concurrently.

## Install

```bash
openclaw plugins install openclaw-unipile \
  --marketplace https://github.com/CassiaResearch/openclaw-marketplace
```

## Configure

Either set env vars on the gateway process:

```
UNIPILE_DSN=https://apiXX.unipile.com:443XX
UNIPILE_API_KEY=...
UNIPILE_ACCOUNT_ID=...
```

or put the values in `~/.openclaw/openclaw.json`:

```json
{
  "plugins": {
    "openclaw-unipile": {
      "enabled": true,
      "dsn": "https://apiXX.unipile.com:443XX",
      "apiKey": "...",
      "accountId": "...",
      "accountTier": "sales_navigator"
    }
  }
}
```

All three credentials are required. Without them the plugin logs a warning and disables itself β€” it doesn't crash the gateway.

## Tools

| Tool                                   | Category         | Notes                                                                                                                  |
| -------------------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------- |
| `linkedin_get_own_profile`             | default          | β€”                                                                                                                      |
| `linkedin_get_profile`                 | profile_read     | Sales Nav API by default                                                                                               |
| `linkedin_get_company`                 | profile_read     | β€”                                                                                                                      |
| `linkedin_search`                      | search_results   | Sales Nav API by default; accepts a browser URL or keywords/filters/category; cost = results returned                  |
| `linkedin_search_parameters`           | default          | Resolves LOCATION/INDUSTRY/COMPANY/SKILL/SCHOOL/LANGUAGE/SERVICE names β†’ LinkedIn IDs for use in `filters`             |
| `linkedin_list_chats`                  | cached_read      | Unipile-cached, bypasses all guardrails                                                                                |
| `linkedin_list_chat_messages`          | cached_read      | Unipile-cached, bypasses all guardrails                                                                                |
| `linkedin_list_messages_from_attendee` | cached_read      | All messages with one attendee across every thread; Unipile-cached                                                     |
| `linkedin_send_message`                | message_write    | Working-hours gated                                                                                                    |
| `linkedin_start_chat`                  | message_write    | Working-hours gated                                                                                                    |
| `linkedin_send_invitation`             | invitation_write | Working-hours gated, β‰₯90 s spacing, ≀300-char message                                                                  |
| `linkedin_list_invitations_sent`       | relation_poll    | 4 h cooldown (per-tool)                                                                                                |
| `linkedin_list_invitations_received`   | relation_poll    | 4 h cooldown (per-tool)                                                                                                |
| `linkedin_handle_invitation`           | invitation_write | Working-hours gated                                                                                                    |
| `linkedin_cancel_invitation_sent`      | default          | β€”                                                                                                                      |
| `linkedin_list_relations`              | relation_poll    | 4 h cooldown (per-tool)                                                                                                |
| `linkedin_usage_report`                | cached_read      | Returns current per-category usage, remaining budgets, cooldowns, and working-hours status. Read-only, no budget cost. |

`accountId` is never a tool parameter β€” the plugin injects the configured one on every call.

## Guardrails

All guardrails are hard blocks: the tool returns a readable error and does not hit Unipile.

### Daily / weekly / monthly quotas (per LinkedIn account)

| Category               |                          Day | Week |  Month | Notes                                                                                                                                   |
| ---------------------- | ---------------------------: | ---: | -----: | --------------------------------------------------------------------------------------------------------------------------------------- |
| Invitations            |                           80 |  200 |    600 | Paid-account defaults. LinkedIn caps ~200/week at the protocol level. For free accounts with a note, set `invitationsPerMonth: 5`.      |
| Profile reads          | 100 (Γ—2 Sales Nav/Recruiter) |    β€” |  3 000 | β€”                                                                                                                                       |
| Search results fetched |    2 500 (1 000 for classic) |    β€” | 50 000 | Cost = number of results returned, not number of calls.                                                                                 |
| Messages / InMails     |                          100 |    β€” |  2 000 | For InMails (`linkedin_start_chat` with `inmail=true`), lower `messagesPerMonth` to ~800 to match LinkedIn's free-InMail monthly quota. |
| Other                  |                          100 |    β€” |  2 000 | Default bucket for everything else.                                                                                                     |

Windows are rolling, not calendar-based.

### Pacing

Per-category behavior β€” not everything has the same guardrails:

| Category                                                     | Mutex (serialize)                                                             | Jitter | Gate (budget / working hours / cooldown)                              |
| ------------------------------------------------------------ | ----------------------------------------------------------------------------- | ------ | --------------------------------------------------------------------- |
| `invitation_write`, `message_write`                          | **yes** β€” writes share one mutex; no two writes ever in flight simultaneously | yes    | yes                                                                   |
| `profile_read`, `search_results`, `relation_poll`, `default` | no β€” concurrent reads allowed                                                 | yes    | yes                                                                   |
| `cached_read` (chat/message reads)                           | no                                                                            | no     | no β€” Unipile serves these from its own cache; they never hit LinkedIn |

Details:

- **Jitter**: 400–1500 ms random delay before every outbound call that hits LinkedIn.
- **Minimum spacing**: β‰₯90 s between consecutive invitations.
- **Polling cooldown**: 4 h between calls to _each_ `relation_poll` tool (tracked per tool, not per category). Calling `list_relations` does not reset the cooldown on `list_invitations_received`.
- **Working hours**: writes (invitations, messages) blocked outside 09:00–18:00 in your configured timezone, and outside the configured working days (default Mon–Fri). Reads are always allowed.
- **429 / 500 handling**: counted as 5Γ— cost against the bucket and forces a longer pause.

The mutex is writes-only by design: LinkedIn's automation detection fingerprints concurrent writes from a single account, but concurrent reads are fine (and the daily/weekly/monthly caps already limit read volume).

Counters persist at `~/.openclaw/unipile/<accountId>/usage.json`. Shape:

- `aggregates.daily[date][category] = { calls, penalty }` β€” real calls vs. 429/500 penalty inflation, split so you can tell them apart.
- `aggregates.perTool[date][toolName] = count` β€” per-tool breakdown for the day.
- `lastCallAt[category]`, `lastCooldownAt[cooldownKey]` β€” ISO 8601, readable without the plugin.
- `events[]` β€” ring buffer of the most recent usage events (default 500, configurable via `telemetry.eventRingSize`). Each is `{ t, tool, cat, cost, result, durationMs?, errorStatus?, reason? }`. Most-recent-first.
- Top-level `createdAt`, `updatedAt`, `accountId`, `accountTier` for diagnostics.

History is retained indefinitely (no pruning). Writes are debounced (~1 s coalesce) and flushed on gateway shutdown via `registerService`. Corrupt / unreadable files are logged and the counter starts fresh; the daily limits still apply within the session. Schema is versioned (`version: 1`) for future migrations.

The plugin is designed for a single-gateway deployment. Two gateways pointed at the same `accountId` would race on the file and lose increments.

Everything above is configurable via `limits`, `pacing`, and `workingHours` in plugin config.

## Error handling

Errors returned by Unipile are mapped to agent-readable messages based on their `errors/*` type slug

... (truncated)
integration

Comments

Sign in to leave a comment

Loading comments...