---
title: line_item_create
description: 'Add a row to a campaign plan: one thing being bought, with the envelope of an intent.'
---

Adds a **Line Item** to a Campaign Plan — one thing being bought, at a granularity coarser than an ad set and deliberately not one-to-one with a campaign.

## Input

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `campaign_plan_name` | `string` | yes | The plan this line belongs to. |
| `channel` | `enum` | yes | `meta`, `tiktok`, `snapchat`, `google_search`, `google_pmax`, `google_display`, `youtube`, `programmatic_display`, `linkedin`, `x`. |
| `objective` | `enum` | yes | `awareness`, `engagement`, `leads`, `sales`, `traffic`, `app_promotion`. |
| `budget` | `number` | no | Whole units of the **plan's** currency. |
| `start_date` / `end_date` | `YYYY-MM-DD` | no | Omit to inherit the plan's. |
| `countries` | `string[]` | no | ISO 3166-1 alpha-2. |
| `age_min` / `age_max` | `integer` | no | Each independently optional. |
| `gender` | `enum` | no | `all`, `men`, `women`. Omit for undecided. |
| `advertiser_id` | `acc_…` | no | The ad account this line runs through. |
| `persona_name` | `string` | no | A persona slug, looked up on the brand the plan is for. |

## Example

```ts
line_item_create({
  campaign_plan_name: 'q4-acquisition',
  channel: 'meta',
  objective: 'sales',
  budget: 40_000, // €40,000.00 in total, because the plan ends 31 Dec
  countries: ['FR', 'BE'],
  age_min: 25,
  age_max: 54,
});
// → { lineItem: { id: 'lni_…', unit: 'total', executable: true, … },
//     campaignPlanStatus: 'draft' }
```

:::info[A line holds only what is constant across everything it spawns]

One Line Item may create any number of provider objects, and you choose the shape. That is why there is no optimization goal here: two ad sets born of one line can legitimately want different ones.

:::

:::tip[The unit is derived, never sent]

If the line has an effective end date — its own, or the plan's — the budget is a **total** for that period. With no end date it is a **daily** rate. Read `unit` back from the response rather than assuming.

:::

:::warning[A persona explains the audience; it never supplies it]

The countries, age range and gender are what execute. A persona is prose about a human, for whoever reads the plan and writes the copy — it carries no geography, so it could never produce a targetable ad set on its own.

:::

Requires the `campaign_plan:write` scope.

## Reference

Add a Line Item to a Campaign Plan: one thing being bought, coarser than an ad set and deliberately not one-to-one with a campaign. It carries the envelope of an intent, never an execution script — one line may create any number of provider objects, and it holds only what is constant across all of them, which is why there is no optimization goal here. Only the channel and the objective are required. The budget is in whole units of the plan's currency, and whether it reads as a total or a daily rate follows from the effective end date. A persona explains the audience; the countries, age range and gender are what actually execute. Adding a line returns an approved plan to draft.

### Input

| Argument | Type | Required | Description |
| --- | --- | --- | --- |
| `advertiser_id` | string | no | The ad account, from list_advertisers. It sits on a Line Item rather than the plan because an account belongs to one provider, while a plan spans channels. |
| `age_max` | integer, 0 to 120 | no |  |
| `age_min` | integer, 0 to 120 | no |  |
| `budget` | number, at least 0 | no | Whole units of the plan's currency (40.5 is 40.50). Whether it reads as a total or a daily rate follows from the effective end date — never send a unit. |
| `campaign_plan_name` | string | yes | The Campaign Plan slug — its stable per-organization handle. |
| `channel` | one of `meta`, `tiktok`, `snapchat`, `google_search`, `google_pmax`, `google_display`, `youtube`, `programmatic_display`, `linkedin`, `x` | yes | What is being bought, in planning vocabulary rather than provider names. Only meta can be executed today; the rest are authorable and inert. |
| `countries` | array of (string, 2 characters) | no | ISO 3166-1 alpha-2 codes, e.g. ["FR", "BE"]. An empty list is a normal drafting state. |
| `end_date` | string | no | Omit to inherit the plan's. Present — here or inherited — makes the budget a total for the period; absent makes it a daily rate. |
| `gender` | one of `all`, `men`, `women` | no | Omit for undecided, which executes the same as "all". |
| `objective` | one of `awareness`, `engagement`, `leads`, `sales`, `traffic`, `app_promotion` | yes |  |
| `persona_name` | string | no | A persona slug. It is looked up on the brand this plan is for — a Line Item may only name one of those — so there is no brand to pass. It **explains** the audience for whoever reads the plan; the countries, age range and gender above are what actually execute. |
| `start_date` | string | no | Omit to inherit the plan's. |

### Failure codes

A failed call has `isError` set, and `structuredContent.error` holds one of these codes. [Errors](/mcp/errors) describes the shape of a failed call.

- `not_found`
- `invalid_countries`
- `invalid_age_range`
- `invalid_amount`
- `invalid_window`
- `persona_not_in_brand`
- `advertiser_not_owned`
- `forbidden`
- `invalid_request`
- `internal_error`

### Scope

The token must hold `campaign_plan:write`. [Auth & scopes](/mcp/auth) lists each scope.

### Annotations

A client reads these hints. A hint that the tool does not declare has the default value of the MCP specification.

- **Writes.** The tool can change data.
- **Destructive.** The tool can make a change that you cannot undo. A client can ask you to confirm before it calls the tool.
- **Not idempotent.** A second call with the same arguments can change more.
- **Closed world.** The tool reads and writes the data of AdCrunch only.
