> ## Documentation Index
> Fetch the complete documentation index at: https://www.landbase.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# linkedin-campaigns

> Create and launch outbound LinkedIn campaigns from an imported audience

Create, personalize, and launch outbound LinkedIn campaigns on top of an
imported audience. The flow mirrors [`email-campaigns`](https://www.landbase.com/docs/reference/email-campaigns) —
`create → audience → messages → coverage → launch` — with LinkedIn-specific
deltas: steps run from order `0` (the connection request), messages are
body-only, and there's an extra `sendability` report. For a guided walkthrough
see [How to launch a campaign](https://www.landbase.com/docs/guides/how-to/launch-a-campaign.md).

> [!NOTE]
>
> `linkedin-campaigns` is a **platform-mode** command. Before running it:
>
> 1. [`landbase-cli auth login`](https://www.landbase.com/docs/reference/auth) — authenticate the session.
> 2. [`landbase-cli account set`](https://www.landbase.com/docs/reference/account) — pick and persist the target account.
> 3. [`landbase-cli contacts-import start`](https://www.landbase.com/docs/reference/contacts-import) — import
>    contacts; this produces the **tag** the campaign targets.
> 4. A **connected LinkedIn account**, configured in the web app — required to
>    launch. Launch sends from the campaign owner's connected LinkedIn
>    account(s) — yours, or the assigned user's if the campaign is assigned.
>
> Without an active account the command exits with `linkedin-campaigns requires
>     an account id...`; pass `--account=<id>` per call as an escape hatch.

## linkedin-campaigns create

Create a campaign draft. Reads a JSON body describing the audience and the
message sequence, and returns `{ campaignId, status }`. A **template** campaign
carries one shared message per message-bearing step; a **personalized** campaign
carries a skeleton sequence whose per-contact copy you upload later with
`messages`.

**Usage**

```bash theme={null}
landbase-cli linkedin-campaigns create --json <path|->
```

**--json** `string` *required*

Path to a JSON file with the campaign definition, or `-` to read it from
stdin. See [`create --json` config](#create-json-config) below for the body shape.

Create is instant and returns only `{ campaignId, status }`; counts come from
`status` after launch.

```json theme={null}
{ "campaignId": 456, "status": "draft" }
```

### `create --json` config

| Field | Type | Notes |
| - | - | - |
| `schemaVersion` | integer | Required. Currently `1`. |
| `name` | string | Required. Campaign name (no control characters). |
| `goal` | string | Optional. Campaign goal (free text). |
| `audience` | object | Required. `{ type: "tag", tagName \| tagId }`. |
| `sequence` | object | Required. `{ steps: [...] }` — at least one step. |
| `messaging` | object | Optional. `{ mode: "template" \| "personalized" }`. Defaults to `"template"`. |
| `assignedUserId` | string | Optional. `identityId` from [`account users`](https://www.landbase.com/docs/reference/account#account-users) — the assigned user becomes the campaign **owner** and **their connected LinkedIn account(s) send**. Omit to own the campaign yourself. |

`audience` names its tag by **exactly one** of `tagName` (preferred — the
human-readable name `contacts-import` shows you) or `tagId` (the numeric id). If
a name matches more than one tag the call fails with `tag_name_ambiguous`;
re-issue with the explicit `tagId`.

> [!NOTE]
>
> **Assigning to a teammate** requires the account **Owner** role
> (`assignment_not_allowed` otherwise); anyone may self-assign.
> [`account users`](https://www.landbase.com/docs/reference/account#account-users) already lists only who
> you can assign to. The assigned user must be an active member with the
> LinkedIn channel permission and a connected LinkedIn account
> (`hasConnectedLinkedin: true`) — validated at create/assign and re-checked at
> launch. See [assign](#linkedin-campaigns-assign) to re-assign a draft later.

Each `sequence.steps[]` entry:

| Field | Type | Notes |
| - | - | - |
| `order` | integer | `0` = the connection request; `1..N` = follow-up messages. Contiguous. |
| `delayDays` | integer | Days to wait before this step. **Exactly one** of `delayDays`/`minutesDelay`. `order` 1 must be `delayDays ≥ 1`. |
| `minutesDelay` | integer | Minutes to wait. Only allowed after the first message (`order ≥ 2`); must be `≥ 1`. |
| `isMessageInclude` | boolean | Whether this step sends a message. |
| `message` | object | `{ body }`. **Required in template mode** on every message-bearing step; omitted in personalized mode. |
| `name` | string | Optional step name. |
| `goal` | string | Optional step goal. |
| `isVoiceGeneration` | boolean | Optional. |

**Example** (template mode)

```bash theme={null}
cat > campaign.json <<'JSON'
{
  "schemaVersion": 1,
  "name": "LinkedIn — Q2 leads",
  "audience": { "type": "tag", "tagName": "Q2 Leads" },
  "sequence": {
    "steps": [
      { "order": 0, "delayDays": 0, "isMessageInclude": true,
        "message": { "body": "Hi — we work with teams like yours on ..." } },
      { "order": 1, "delayDays": 2, "isMessageInclude": true,
        "message": { "body": "Thanks for connecting! ..." } }
    ]
  }
}
JSON
landbase-cli linkedin-campaigns create --json campaign.json
# → { "campaignId": 456, "status": "draft" }
```

***

## linkedin-campaigns audience

Export the campaign's resolved contacts as **one wide CSV** so you (or an AI
agent) can write one personalized message per contact, per message-bearing step.
The file has the contact-context columns plus an empty `message_<order>` cell for
each message-bearing order. Fill those cells **in place** and resubmit the same
file with `messages`. Used only for personalized campaigns.

**Usage**

```bash theme={null}
landbase-cli linkedin-campaigns audience --campaign <id> [--out <file>] [--limit N]
```

**--campaign** `integer` *required*

The campaign (draft) id returned by `create`.

**--out** `string`

Write the CSV to this file and print a JSON summary to stdout. Without it, the
raw CSV is written to stdout.

**--limit** `integer`

Cap the number of exported contacts (positive integer).

CSV columns: `linkedin_url, first_name, last_name, title, company_name,
company_website, industry`, then `message_<order>` per message-bearing order.
`linkedin_url` is the join key and must stay exactly as exported; each
`message_<order>` is ≤ 5000 chars. `message_0` is the connection-request note
(sent with the invite); `message_1..` are follow-ups.

With `--out`, the CSV is written to the file and stdout is a JSON summary;
without `--out`, the raw CSV is written to stdout (not JSON):

```json theme={null}
{
  "campaignId": 456,
  "tagId": 15,
  "orders": [0, 1],
  "total": 240,
  "out": "audience.csv",
  "columns": [
    "linkedin_url", "first_name", "last_name", "title", "company_name",
    "company_website", "industry", "message_0", "message_1"
  ]
}
```

`orders` are the message-bearing step orders that drive the `message_<order>`
columns.

***

## linkedin-campaigns messages

Upload the filled wide CSV to the campaign. The back-end validates the file,
matches rows by `linkedin_url`, and ingests them asynchronously. Without
`--watch` the command returns the ingest-start response immediately; with
`--watch` it polls to a terminal phase and prints the final ingest status.

**Usage**

```bash theme={null}
landbase-cli linkedin-campaigns messages --campaign <id> --csv <file> [--watch]
```

**--campaign** `integer` *required*

The campaign id to upload messages for.

**--csv** `string` *required*

Path to the filled messages CSV (the same file exported by `audience`).

**Flags**

| Flag | Default | Description |
| - | - | - |
| `--watch` | — | Poll the ingest until a terminal phase (`completed`/`partial`/`failed`), then print the final status. Can block a minute or more. |
| `--account=<id>` | — | Override the active account for a single command |
| `--verbose` | — | Print request/response details to stderr |

Re-uploading a fixed file is a full replace. Terminal ingest phases:
`completed`, `partial`, `failed`.

The start response (without `--watch`):

```json theme={null}
{ "ingestId": 91, "campaignId": 456, "status": "queued", "phase": "queued" }
```

With `--watch`, the final status once terminal — the `coverage.samples` carry
LinkedIn URLs and ordered bodies:

```json theme={null}
{
  "ingestId": 91,
  "campaignId": 456,
  "status": "done",
  "phase": "completed",
  "progress": { "totalRows": 240, "processedRows": 240, "percent": 100 },
  "stats": { "rawRowCount": 240, "validRowCount": 240, "invalidRowCount": 0 },
  "coverage": {
    "stepCount": 2,
    "matched": 240,
    "covered": 240,
    "uncovered": [],
    "invalid": [],
    "samples": [
      { "linkedinUrl": "https://www.linkedin.com/in/sarahchen",
        "messages": [ { "order": 0, "body": "..." }, { "order": 1, "body": "..." } ] }
    ]
  },
  "errorLog": null
}
```

`coverage` is `null` while the ingest is still in flight.

***

## linkedin-campaigns coverage

Inspect message coverage before launching — which matched contacts have a
complete message set. Bare, it returns the coverage report; with
`--show <linkedinUrl>` it returns that one contact's stored per-step messages.

**Usage**

```bash theme={null}
landbase-cli linkedin-campaigns coverage --campaign <id> [--show <linkedinUrl>]
```

**--campaign** `integer` *required*

The campaign id to report coverage for.

**--show** `string`

A contact's LinkedIn URL. Returns that contact's stored messages instead of the
report.

> [!NOTE]
>
> `coverage` and `sendability` are **draft-only** reports. After launch they
> return `409 coverage_unavailable` / `sendability_unavailable`.

Bare coverage report — `matched` is the resolved audience size, `covered` the
contacts with a complete message set, `uncovered` a sample still missing them:

```json theme={null}
{
  "campaignId": 456,
  "stepCount": 2,
  "matched": 240,
  "covered": 240,
  "uncovered": [],
  "samples": [
    { "linkedinUrl": "https://www.linkedin.com/in/sarahchen",
      "messages": [ { "order": 0, "body": "..." } ] }
  ]
}
```

With `--show <linkedinUrl>` — one contact's ordered messages (`null` if none yet):

```json theme={null}
{
  "campaignId": 456,
  "linkedinUrl": "https://www.linkedin.com/in/sarahchen",
  "messages": [ { "order": 0, "body": "..." }, { "order": 1, "body": "..." } ]
}
```

***

## linkedin-campaigns sendability

Explain why the sendable audience is smaller than the tag: totals plus a
per-reason breakdown. `sendable` is what the campaign will message; `excluded`
is the count of tagged contacts not currently sendable. The `contacts*` reason
counts are independent diagnostics that may overlap and need not sum to
`excluded`.

**Usage**

```bash theme={null}
landbase-cli linkedin-campaigns sendability --campaign <id>
```

**--campaign** `integer` *required*

The campaign (draft) id.

Draft-only (see the note under `coverage`). `sendable` is what the campaign will
message (`== total`); `excluded` = `tagged − sendable`. The `contacts*` reason
counts are optional diagnostics that may overlap and need not sum to `excluded`:

```json theme={null}
{
  "campaignId": 456,
  "tagId": 15,
  "sendability": {
    "tagged": 300,
    "sendable": 240,
    "excluded": 60,
    "contactsWithoutLinkedinUrl": 42,
    "contactsInDnc": 5,
    "contactsAlreadyInCampaign": 13
  }
}
```

***

## linkedin-campaigns assign

Re-assign a **draft** campaign to another account user. The assigned user
becomes the campaign owner and **their connected LinkedIn account(s) send**.
Works on drafts only — once launched, assignment is final
(`campaign_not_reassignable`).

**Usage**

```bash theme={null}
landbase-cli linkedin-campaigns assign --campaign <id> --user <identityId>
```

**--campaign** `integer` *required*

The draft campaign id.

**--user** `string` *required*

The `identityId` of the user to assign, from
[`account users`](https://www.landbase.com/docs/reference/account#account-users).

Assigning to a teammate requires the account **Owner** role
(`assignment_not_allowed` otherwise); anyone may self-assign. The assigned user
must have the LinkedIn channel permission and a connected LinkedIn account
(`hasConnectedLinkedin: true` in `account users`).

Returns the campaign with its new owner:

```json theme={null}
{
  "campaignId": 456,
  "status": "draft",
  "assignedUser": {
    "identityId": "idn_01H...",
    "firstName": "Sarah",
    "lastName": "Chen",
    "email": "sarah@acme.com"
  }
}
```

***

## linkedin-campaigns launch

Launch a draft. Takes no body — the campaign enrolls its audience and hands off
to the send pipeline, sending from the campaign owner's connected LinkedIn
account(s). **Launch is blocked** (`campaign_coverage_incomplete`) until every
matched contact has a complete message set. With `--watch` the command polls
the status (up to 10 times, 10s apart) until it reaches a terminal state
(target: `scheduled`).

**Usage**

```bash theme={null}
landbase-cli linkedin-campaigns launch <campaignId> [--yes] [--watch]
```

**campaignId** `integer` *required*

The draft campaign id to launch.

**Flags**

| Flag | Default | Description |
| - | - | - |
| `--yes` | — | Confirm launching a campaign assigned to someone other than its creator |
| `--watch` | — | Poll status (max 10 × 10s) until terminal; print the final status |
| `--account=<id>` | — | Override the active account for a single command |

> [!NOTE]
>
> **Assigned-launch confirmation.** Launching a campaign assigned to someone
> **other than its creator** requires `--yes` — without it the CLI exits `1`
> with `CONFIRMATION_REQUIRED`, because after launch the assignment is final
> and the assignee's LinkedIn account(s) send. Unassigned (or self-assigned)
> campaigns launch unchanged, no `--yes` needed. The check is best-effort: if
> the pre-launch status fetch fails, the CLI prints a warning to stderr and
> proceeds rather than blocking the launch.

> [!WARNING]
>
> **Launch ≠ sends.** Reaching `scheduled` means the audience is enrolled and
> handed to the send pipeline — invites and messages then go out on a throttled
> cadence bounded by LinkedIn rate limits and per-account caps, **not** all at
> once. Track real progress with `status`.

Returns `{ campaignId, status }`:

```json theme={null}
{ "campaignId": 456, "status": "scheduled" }
```

With `--watch`, stdout is the final
[`status`](https://www.landbase.com/docs/reference/output-schemas#linkedin-campaigns-status) object instead.

***

## linkedin-campaigns status

Status for one campaign — type, lifecycle, and allocated/sent stats.

**Usage**

```bash theme={null}
landbase-cli linkedin-campaigns status <campaignId>
```

**campaignId** `integer` *required*

The campaign id.

Returns `{ campaignId, name?, status, campaignType?, stats? }`. See
[output schemas](https://www.landbase.com/docs/reference/output-schemas.md#linkedin-campaigns-status).

***

## linkedin-campaigns list

List the account's campaigns, newest first.

**Usage**

```bash theme={null}
landbase-cli linkedin-campaigns list
```

Returns `{ data: [...], totalCount }`. See
[output schemas](https://www.landbase.com/docs/reference/output-schemas.md#linkedin-campaigns-list).

## Errors

| Code | Meaning |
| - | - |
| `INVALID_INPUT` | Missing/invalid `--json`, a non-numeric campaign id, a bad `--limit`, or a malformed body (e.g. not exactly one of `tagId`/`tagName`; a step with neither/both of `delayDays`/`minutesDelay`; `minutesDelay` before order 2; order 1 with `delayDays < 1`; a message-bearing step missing its body in template mode). |
| `AUTH_REQUIRED` | Run `landbase-cli auth login` first. |
| `AUTH_FAILED` | The platform session expired (7-day TTL) — re-login. |
| `NOT_FOUND` | `tag_not_found`, `campaign_not_found`, or `assigned_user_not_found`. |
| `API_ERROR` | `campaign_not_launchable`, `tag_name_ambiguous` (pass the explicit `tagId`), `no_contacts_matched`, `campaign_coverage_incomplete` (fix the CSV), `campaign_not_personalized`, `campaign_creator_mismatch` (only the draft creator can launch), `campaign_not_reassignable` (assignment is final after launch), `assignment_not_allowed` (non-Owners may only self-assign — drop `assignedUserId` / pass your own id, or ask an account Owner), `assigned_user_not_in_account` / `linkedin_not_connected` / `linkedin_channel_permission_required` (pick another user via [`account users`](https://www.landbase.com/docs/reference/account#account-users)), or `sendability_unavailable` / `coverage_unavailable` (those reports are draft-only). A launch with no connected LinkedIn account surfaces "No connected LinkedIn accounts". |
| `CONFIRMATION_REQUIRED` | The campaign is assigned to someone other than its creator — re-run `launch` with `--yes` to confirm. |
| `TIMEOUT` | `launch --watch` / `messages --watch` exceeded their poll budget while still non-terminal. Resume with `status` / `messages`. |

## Related

* [How to launch a campaign](https://www.landbase.com/docs/guides/how-to/launch-a-campaign.md)
* [email-campaigns command reference](https://www.landbase.com/docs/reference/email-campaigns.md)
* [contacts-import command reference](https://www.landbase.com/docs/reference/contacts-import.md)
* [account command reference](https://www.landbase.com/docs/reference/account.md)
* [Output schemas: status & list](https://www.landbase.com/docs/reference/output-schemas.md#linkedin-campaigns-status)
* [Error codes](https://www.landbase.com/docs/reference/error-codes.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.