> ## 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`](/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](/docs/how-to/launch-a-campaign).

<Note>
  `linkedin-campaigns` is a **platform-mode** command. Before running it:

  1. [`landbase-cli auth login`](/docs/reference/auth) — authenticate the session.
  2. [`landbase-cli account set`](/docs/reference/account) — pick and persist the target account.
  3. [`landbase-cli contacts-import start`](/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 every connected LinkedIn account on the account.

  Without an active account the command exits with `linkedin-campaigns requires
      an account id...`; pass `--account=<id>` per call as an escape hatch.
</Note>

## 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|->
```

<ParamField path="--json" type="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.
</ParamField>

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"`. |

`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`.

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]
```

<ParamField path="--campaign" type="integer" required>
  The campaign (draft) id returned by `create`.
</ParamField>

<ParamField path="--out" type="string">
  Write the CSV to this file and print a JSON summary to stdout. Without it, the
  raw CSV is written to stdout.
</ParamField>

<ParamField path="--limit" type="integer">
  Cap the number of exported contacts (positive integer).
</ParamField>

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]
```

<ParamField path="--campaign" type="integer" required>
  The campaign id to upload messages for.
</ParamField>

<ParamField path="--csv" type="string" required>
  Path to the filled messages CSV (the same file exported by `audience`).
</ParamField>

**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>]
```

<ParamField path="--campaign" type="integer" required>
  The campaign id to report coverage for.
</ParamField>

<ParamField path="--show" type="string">
  A contact's LinkedIn URL. Returns that contact's stored messages instead of the
  report.
</ParamField>

<Note>
  `coverage` and `sendability` are **draft-only** reports. After launch they
  return `409 coverage_unavailable` / `sendability_unavailable`.
</Note>

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>
```

<ParamField path="--campaign" type="integer" required>
  The campaign (draft) id.
</ParamField>

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 launch

Launch a draft. Takes no body — the campaign enrolls its audience and hands off
to the send pipeline, sending from every connected LinkedIn account. **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> [--watch]
```

<ParamField path="campaignId" type="integer" required>
  The draft campaign id to launch.
</ParamField>

**Flags**

| Flag             | Default | Description                                                       |
| ---------------- | ------- | ----------------------------------------------------------------- |
| `--watch`        | —       | Poll status (max 10 × 10s) until terminal; print the final status |
| `--account=<id>` | —       | Override the active account for a single command                  |

<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`.
</Warning>

Returns `{ campaignId, status }`:

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

With `--watch`, stdout is the final
[`status`](/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>
```

<ParamField path="campaignId" type="integer" required>
  The campaign id.
</ParamField>

Returns `{ campaignId, name?, status, campaignType?, stats? }`. See
[output schemas](/docs/reference/output-schemas#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](/docs/reference/output-schemas#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` or `campaign_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), or `sendability_unavailable` / `coverage_unavailable` (those reports are draft-only). A launch with no connected LinkedIn account surfaces "No connected LinkedIn accounts". |
| `TIMEOUT`       | `launch --watch` / `messages --watch` exceeded their poll budget while still non-terminal. Resume with `status` / `messages`.                                                                                                                                                                                                                                                                                                |

## Related

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