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

# email-campaigns

> Create and launch outbound email campaigns from an imported audience

Create, personalize, and launch outbound email campaigns on top of an imported
audience. The normal flow is `create → audience → messages → coverage → launch`,
but a template-mode campaign can go straight from `create` to `launch`. For a
guided, end-to-end walkthrough see
[How to launch a campaign](https://www.landbase.com/docs/guides/how-to/launch-a-campaign.md).

> [!NOTE]
>
> `email-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.
>
> Sending inboxes are configured separately on the account **in the web app**,
> not through the CLI. Without an active account the command exits with
> `email-campaigns requires an account id...`; pass `--account=<id>` per call as
> an escape hatch.

## email-campaigns create

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

**Usage**

```bash theme={null}
landbase-cli email-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 }`; audience and
exclusion counts come from `status` after launch.

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

### `create --json` config

| Field | Type | Notes |
| - | - | - |
| `schemaVersion` | integer | Required. Currently `1`. |
| `name` | string | Required. Campaign name (no control characters). |
| `audience` | object | Required. `{ type: "tag", tagName \| tagId, excludeContactedDays?, excludeType?, limitPerCompany? }`. |
| `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 inboxes 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`. Optional filters: `excludeContactedDays`
(skip contacts touched in the last N days), `excludeType` (`"contacted"` or
`"replied"`), and `limitPerCompany`.

Each `sequence.steps[]` entry:

| Field | Type | Notes |
| - | - | - |
| `order` | integer | Step order, contiguous from `1`. |
| `channel` | string | `"email"`. |
| `daysDelay` | integer | Days to wait before this step. |
| `message` | object | `{ subject, body }`. **Required in template mode**, omitted in personalized mode. |
| `initiateThread` | boolean | Optional. Start a new thread instead of replying in-thread. |

> [!TIP]
>
> In **template mode**, message bodies may use only these bracketed variables
> (anything else is rejected): `[Recipient's Name]`,
> `[Recipient's Company Name]`, `[Your Company]`, `[Opener]`.

> [!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 email
> channel permission and at least one active inbox (`hasActiveInboxes: true`) —
> validated at create/assign and re-checked at launch. See
> [assign](#email-campaigns-assign) to re-assign a draft later.

**Example** (template mode)

```bash theme={null}
cat > campaign.json <<'JSON'
{
  "schemaVersion": 1,
  "name": "Q2 outreach",
  "audience": { "type": "tag", "tagName": "Q2 Leads" },
  "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
landbase-cli email-campaigns create --json campaign.json
# → { "campaignId": 123, "status": "draft" }
```

***

## email-campaigns audience

Export the campaign's resolved, enriched contacts as **one wide CSV** so you (or
an AI agent) can write one personalized message per contact, per step. The file
has the contact-context columns plus empty `subject_i`/`body_i` pairs for each
step. Fill those cells **in place** and resubmit the same file with `messages` —
do not create a second CSV. Used only for personalized campaigns.

**Usage**

```bash theme={null}
landbase-cli email-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 (redirect it to a file yourself).

**--limit** `integer`

Cap the number of exported contacts (positive integer). Useful for a quick
sample before generating copy for the whole audience.

CSV columns: `email, first_name, last_name, title, company_name,
company_website, industry, employee_range, linkedin_url`, then `subject_1,
body_1, …, subject_N, body_N` for the N steps. `email` is the join key and must
stay exactly as exported; `subject_i` ≤ 1000 chars, `body_i` ≤ 10000 chars.

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": 123,
  "tagId": 15,
  "stepCount": 2,
  "total": 240,
  "out": "audience.csv",
  "columns": [
    "email", "first_name", "last_name", "title", "company_name",
    "company_website", "industry", "employee_range", "linkedin_url",
    "subject_1", "body_1", "subject_2", "body_2"
  ]
}
```

***

## email-campaigns messages

Upload the filled wide CSV to the campaign. The back-end validates the file,
matches rows by email, and ingests them asynchronously (same upload-and-poll
shape as [`contacts-import`](https://www.landbase.com/docs/reference/contacts-import)). Without `--watch` the
command returns the ingest-start response immediately; with `--watch` it polls
to a terminal phase and prints the final ingest status (including a coverage
summary).

**Usage**

```bash theme={null}
landbase-cli email-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 (idempotent). Terminal ingest
phases: `completed`, `partial`, `failed`.

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

```json theme={null}
{ "ingestId": 88, "campaignId": 123, "status": "queued", "phase": "queued" }
```

With `--watch`, the final status once terminal — including a coverage summary:

```json theme={null}
{
  "ingestId": 88,
  "campaignId": 123,
  "status": "done",
  "phase": "completed",
  "progress": { "totalRows": 240, "processedRows": 240, "percent": 100 },
  "stats": { "rawRowCount": 240, "validRowCount": 238, "invalidRowCount": 2 },
  "coverage": {
    "stepCount": 2,
    "matched": 240,
    "covered": 238,
    "uncovered": ["late@acme.com"],
    "invalid": ["bad-row@acme.com"],
    "samples": [
      { "email": "sarah@acme.com",
        "messages": [ { "subject": "...", "body": "..." } ] }
    ]
  },
  "errorLog": null
}
```

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

***

## email-campaigns coverage

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

**Usage**

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

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

The campaign id to report coverage for.

**--show** `string`

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

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": 123,
  "stepCount": 2,
  "matched": 240,
  "covered": 238,
  "uncovered": ["late@acme.com"],
  "samples": [
    { "email": "sarah@acme.com",
      "messages": [ { "subject": "...", "body": "..." } ] }
  ]
}
```

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

```json theme={null}
{
  "campaignId": 123,
  "email": "sarah@acme.com",
  "messages": [ { "subject": "...", "body": "..." } ]
}
```

***

## email-campaigns assign

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

**Usage**

```bash theme={null}
landbase-cli email-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 email channel permission and at least one active inbox
(`hasActiveInboxes: true` in `account users`).

Returns the campaign with its new owner:

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

***

## email-campaigns launch

Launch a draft. Takes no body — the campaign enrolls its audience and hands off
to the send scheduler. **Launch is blocked** (`campaign_coverage_incomplete`)
until every matched contact has a complete message set; on a blocked launch the
CLI prints which contacts are still uncovered. 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 email-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 inboxes 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.** `launch` returns quickly with `processing_messages` and
> reaches `scheduled` shortly after — that means the audience is enrolled and
> handed to the scheduler, **not** that emails have gone out. Actual sends are
> further gated by inbox status and daily caps, the account send window (default
> 06:00–15:00 PT), and contact verification. Track real progress with `status`.

Returns `{ campaignId, status }` — `status` reaches `scheduled` shortly after:

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

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

***

## email-campaigns status

Composed status for one campaign — lifecycle plus the delivery funnel,
exclusions, and timing estimates.

**Usage**

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

**campaignId** `integer` *required*

The campaign id.

Returns `{ campaignId, name?, status, stats?, outreach?, excluded?, timing? }`.
Audience and exclusion counts are produced by the scheduler **after** launch —
read them here, not from the `create`/`launch` responses. See
[output schemas](https://www.landbase.com/docs/reference/output-schemas.md#email-campaigns-status).

***

## email-campaigns list

List the account's campaigns, newest first.

**Usage**

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

Returns `{ data: [...], totalCount }`. See
[output schemas](https://www.landbase.com/docs/reference/output-schemas.md#email-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 missing its message 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`, `user_not_found`, or `assigned_user_not_found`. |
| `API_ERROR` | `campaign_not_launchable` (wrong current status), `tag_name_ambiguous` (pass the explicit `tagId`), `no_contacts_matched`, `campaign_coverage_incomplete` (fix the CSV and re-upload), `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), or `assigned_user_not_in_account` / `assigned_user_no_active_inboxes` / `email_channel_permission_required` (pick another user via [`account users`](https://www.landbase.com/docs/reference/account#account-users)). |
| `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)
* [linkedin-campaigns command reference](https://www.landbase.com/docs/reference/linkedin-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#email-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.