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

# How to launch a campaign

> Start an outbound email or LinkedIn campaign with the CLI — import contacts, create a draft, write per-contact copy, and launch.

# How to launch a campaign

This guide takes you from a signed-in session to a live outbound campaign. It
covers **both** channels — email and LinkedIn — which share the same shape:
import contacts to get a tag, create a draft against that tag, write the copy,
then launch.

The main flow uses **personalized mode** — one message written per contact,
which is how most campaigns run and what lands best. If you'd rather send one
shared message per step, the shorter
[template-mode path](#simpler-template-mode) is at the end.

<Note>
  Campaigns are **platform-mode** commands. You'll need, in order:

  1. [`landbase-cli auth login`](/docs/reference/auth) — a signed-in session.
  2. [`landbase-cli account set`](/docs/reference/account) — an active account.
  3. A **tag** of contacts (step 2 below).
  4. A connected sending channel — a **sending inbox** (email) or a **connected
     LinkedIn account** (LinkedIn), both configured in the web app.
</Note>

Throughout, `<channel>` is either `email` or `linkedin` — the subcommands are
identical unless noted.

***

## 1. Sign in and pick the account

```bash theme={null}
landbase-cli auth login
landbase-cli account set
```

`account set` shows a picker and saves your choice, so it's a one-time step per
machine. Full walkthrough:
[How to select and onboard an account](/docs/how-to/select-and-onboard-account).

***

## 2. Import contacts to get the tag

A campaign always targets a **tag**, and the tag comes from importing contacts —
so this is a required pre-campaign step, not an optional one. Start an import and
note the `tagName`/`tagId` it produces:

```bash theme={null}
echo '{"datasetId":"<dataset-id>","tagName":"Q2 outreach"}' \
  | landbase-cli contacts-import start --json - --watch
```

`contacts-import status <importId>` surfaces both `tagName` and `tagId` once the
import is terminal — that name (or id) is what the campaign's `audience` refers
to. Full walkthrough: [How to import contacts](/docs/how-to/import-contacts) and the
[contacts-import reference](/docs/reference/contacts-import).

<Tip>
  Prefer `tagName` in the campaign body — it's the human-readable name the import
  shows you. Use the numeric `tagId` only if a name is ambiguous (matches more
  than one tag).
</Tip>

***

## 3. Connect the sending channel

Sending inboxes and LinkedIn accounts are connected **in the web app**, not
through the CLI:

* **Email** — connect at least one sending inbox on the account.
* **LinkedIn** — connect at least one LinkedIn account. Launch sends from every
  connected LinkedIn account.

You can create a draft without this, but `launch` needs a connected channel.

***

## 4. Create a draft (personalized mode)

Write a campaign definition that targets your tag with
`"messaging": { "mode": "personalized" }`. In personalized mode the steps are a
**skeleton** — order and timing only, **no** inline `message` — because you'll
write one message per contact in step 6. The body differs slightly per channel:

<CodeGroup>
  ```json Email (campaign.json) theme={null}
  {
    "schemaVersion": 1,
    "name": "Q2 outreach",
    "audience": { "type": "tag", "tagName": "Q2 outreach" },
    "messaging": { "mode": "personalized" },
    "sequence": {
      "steps": [
        { "order": 1, "channel": "email", "daysDelay": 0 },
        { "order": 2, "channel": "email", "daysDelay": 3 }
      ]
    }
  }
  ```

  ```json LinkedIn (campaign.json) theme={null}
  {
    "schemaVersion": 1,
    "name": "LinkedIn — Q2 leads",
    "audience": { "type": "tag", "tagName": "Q2 outreach" },
    "messaging": { "mode": "personalized" },
    "sequence": {
      "steps": [
        { "order": 0, "delayDays": 0, "isMessageInclude": true },
        { "order": 1, "delayDays": 2, "isMessageInclude": true }
      ]
    }
  }
  ```
</CodeGroup>

```bash theme={null}
landbase-cli <channel>-campaigns create --json campaign.json
```

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

Note the `campaignId` — you'll use it for every step below. Key differences
between the two bodies: email steps start at `order: 1` with `channel: "email"`
and `daysDelay`; LinkedIn steps start at `order: 0` (the connection request), use
`isMessageInclude`, and take `delayDays` **or** `minutesDelay`. Full field
reference: [email-campaigns](/docs/reference/email-campaigns#create-json-config) /
[linkedin-campaigns](/docs/reference/linkedin-campaigns#create-json-config).

***

## 5. Export the audience

Export the campaign's resolved contacts as **one wide CSV** — contact-context
columns plus empty message cells for each step:

```bash theme={null}
landbase-cli <channel>-campaigns audience --campaign 123 --out audience.csv
```

You'll fill the message cells in the next step. There is exactly one file: fill
it **in place** and resubmit the same file — don't create a second CSV.

***

## 6. Write the messages

First pull seller-side context so the copy has a value prop to lean on, then fill
one personalized message per contact, per step, directly in `audience.csv`:

```bash theme={null}
landbase-cli onboarding get
#    Email:    fill subject_1/body_1 … subject_N/body_N per row
#    LinkedIn: fill message_<order> per row (message_0 = the connection-request note)
```

The join key (`email` for email, `linkedin_url` for LinkedIn) is already filled
and must stay exactly as exported. Write genuinely per-contact copy that
references each contact's role, company, and industry — don't reuse a templated
sentence across rows.

<Tip>
  Richer account onboarding produces sharper copy. If `onboarding get` shows
  sparse `aboutCompany` / `aboutAudience` / `aboutCompetition`, fill it in with
  [`onboarding`](/docs/reference/onboarding) before writing.
</Tip>

***

## 7. Upload the messages

Upload the filled CSV. The back-end matches rows by the join key and ingests them
asynchronously; `--watch` polls until the ingest is terminal:

```bash theme={null}
landbase-cli <channel>-campaigns messages --campaign 123 --csv audience.csv --watch
```

Re-uploading a fixed file is a full replace, so you can iterate until coverage is
complete.

***

## 8. Check coverage, then launch

Spot-check that contacts have their messages, then launch:

```bash theme={null}
landbase-cli <channel>-campaigns coverage --campaign 123 --show <email|linkedinUrl>
landbase-cli <channel>-campaigns launch 123 --watch
```

`launch --watch` polls the status (up to 10 times, 10s apart) until it reaches
`scheduled`.

<Warning>
  **Launch ≠ sends.** Reaching `scheduled` means the audience is enrolled and
  handed to the send scheduler — not that messages have gone out. Sends then
  happen on a throttled cadence: email is gated by inbox caps and the account
  send window (default 06:00–15:00 PT); LinkedIn is gated by rate limits and
  per-account caps. Launch is also **blocked until every matched contact has a
  message** — if `coverage` shows gaps, fix those rows and re-upload (step 7)
  before launching.
</Warning>

<Tip>
  `messages --watch` and `launch --watch` can each block for a minute or more.
  When driving from a coding agent, run the watched call as a sub-agent so it
  doesn't tie up your main session.
</Tip>

***

## 9. Track it

```bash theme={null}
landbase-cli <channel>-campaigns status 123
```

The delivery funnel, exclusions, and timing estimates appear here as the
scheduler works through the audience.

***

## Simpler: template mode

If you don't need per-contact copy, **template mode** (the default) carries one
shared message per step inline in the create body — no CSV, no upload. Skip
steps 5–7: create with an inline `message` on each step, then launch.

<CodeGroup>
  ```json Email (campaign.json) theme={null}
  {
    "schemaVersion": 1,
    "name": "Q2 outreach",
    "audience": { "type": "tag", "tagName": "Q2 outreach" },
    "sequence": {
      "steps": [
        { "order": 1, "channel": "email", "daysDelay": 0,
          "message": { "subject": "Quick question, [Recipient's Name]", "body": "..." } },
        { "order": 2, "channel": "email", "daysDelay": 3,
          "message": { "subject": "Following up", "body": "..." } }
      ]
    }
  }
  ```

  ```json LinkedIn (campaign.json) theme={null}
  {
    "schemaVersion": 1,
    "name": "LinkedIn — Q2 leads",
    "audience": { "type": "tag", "tagName": "Q2 outreach" },
    "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! ..." } }
      ]
    }
  }
  ```
</CodeGroup>

```bash theme={null}
landbase-cli <channel>-campaigns create --json campaign.json
landbase-cli <channel>-campaigns launch 123 --watch
```

In template mode, email message bodies may use only these bracketed variables:
`[Recipient's Name]`, `[Recipient's Company Name]`, `[Your Company]`, `[Opener]`.

***

## If something fails

* **`campaign_coverage_incomplete` on launch** — a personalized campaign still
  has contacts without a complete message set. Run `coverage`, fix those rows in
  the CSV, re-upload with `messages`, then re-launch.
* **`tag_name_ambiguous`** — the `tagName` matches more than one tag. Re-issue
  `create` with the explicit `tagId`.
* **`AUTH_FAILED`** — the session expired (7-day TTL). Re-run `auth login`; see
  [troubleshoot authentication](/docs/how-to/troubleshoot-auth).
* **No sends after `scheduled`** — check the sending channel is connected and,
  for email, that you're inside the send window.

## Related

* [email-campaigns command reference](/docs/reference/email-campaigns)
* [linkedin-campaigns command reference](/docs/reference/linkedin-campaigns)
* [How to import contacts](/docs/how-to/import-contacts)
* [How to select and onboard an account](/docs/how-to/select-and-onboard-account)
* [Output schemas](/docs/reference/output-schemas#email-campaigns-status)
