> ## 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`](https://www.landbase.com/docs/reference/cli/auth) — a signed-in session.
> 2. [`landbase-cli account set`](https://www.landbase.com/docs/reference/cli/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.

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](https://www.landbase.com/docs/guides/how-to/select-and-onboard-account.md).

***

## 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](https://www.landbase.com/docs/guides/how-to/import-contacts.md) and the
[contacts-import reference](https://www.landbase.com/docs/reference/cli/contacts-import.md).

> [!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).

***

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

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

```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](https://www.landbase.com/docs/reference/cli/email-campaigns.md#create-json-config) /
[linkedin-campaigns](https://www.landbase.com/docs/reference/cli/linkedin-campaigns.md#create-json-config).

### Optional: assign the campaign to a teammate

By default you own the campaign and your inboxes / LinkedIn account send. To
have a teammate's channel send instead, assign the campaign to them — either at
create time (add `"assignedUserId": "<identityId>"` to the body above) or on
the draft afterwards:

```bash theme={null}
landbase-cli account users
landbase-cli <channel>-campaigns assign --campaign 123 --user <identityId>
```

[`account users`](https://www.landbase.com/docs/reference/cli/account#account-users) lists who you can assign to
(account Owners see every member; everyone else only themselves) with per-channel
readiness flags — pick a user with `hasActiveInboxes: true` (email) or
`hasConnectedLinkedin: true` (LinkedIn). Assignment works on **drafts only**:
once launched, it's final.

***

## 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`](https://www.landbase.com/docs/reference/cli/onboarding) before writing.

***

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

If the campaign is assigned to someone other than its creator, `launch` exits
with `CONFIRMATION_REQUIRED` — re-run it with `--yes` to confirm that the
assignee's channel will send and that the assignment becomes final.

> [!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.

> [!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.

***

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

```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! ..." } }
    ]
  }
}
```

```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`.
* **`CONFIRMATION_REQUIRED` on launch** — the campaign is assigned to someone
  other than its creator. Re-run `launch` with `--yes` to confirm.
* **`assignment_not_allowed`** — only account Owners can assign to a teammate;
  everyone else may only self-assign. Drop `assignedUserId` or ask an Owner.
* **`AUTH_FAILED`** — the session expired (7-day TTL). Re-run `auth login`; see
  [troubleshoot authentication](https://www.landbase.com/docs/guides/how-to/troubleshoot-auth.md).
* **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](https://www.landbase.com/docs/reference/cli/email-campaigns.md)
* [linkedin-campaigns command reference](https://www.landbase.com/docs/reference/cli/linkedin-campaigns.md)
* [How to import contacts](https://www.landbase.com/docs/guides/how-to/import-contacts.md)
* [How to select and onboard an account](https://www.landbase.com/docs/guides/how-to/select-and-onboard-account.md)
* [Output schemas](https://www.landbase.com/docs/reference/cli/output-schemas.md#email-campaigns-status)


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