Integration
Unipile
Unipile LinkedIn automation plugin for OpenClaw β messaging, connections, profile reads, Sales Navigator search, with hard-enforced daily quotas and human-like pacing.
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