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

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

  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.
</Note>

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

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

`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]`.
</Tip>

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

<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 (redirect it to a file yourself).
</ParamField>

<ParamField path="--limit" type="integer">
  Cap the number of exported contacts (positive integer). Useful for a quick
  sample before generating copy for the whole audience.
</ParamField>

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

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

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

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

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

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

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

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

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

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](/docs/reference/output-schemas#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](/docs/reference/output-schemas#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`, or `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), or `campaign_creator_mismatch` (only the draft creator can launch). |
| `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)
* [linkedin-campaigns command reference](/docs/reference/linkedin-campaigns)
* [contacts-import command reference](/docs/reference/contacts-import)
* [account command reference](/docs/reference/account)
* [Output schemas: status & list](/docs/reference/output-schemas#email-campaigns-status)
* [Error codes](/docs/reference/error-codes)
