# Campaigns

Build an audience, draft a broadcast, and launch or schedule it. `launch_campaign` sends immediately.

## Workflow

1. `list_etiquetas` — tags and contact counts.
2. `preview_audience` — size and sample, nothing saved.
3. `create_audience` — persist the spec. Binds to `phone_number_id` or the project default; `create_broadcast_draft` inherits that binding.
4. `list_templates` / `list_template_variables` — pick an approved template and its variables.
5. `create_broadcast_draft` — draft only. Pass `scheduled_at` to schedule instead of sending now.
6. `launch_campaign` — sends now. Confirm the recipient count with the operator first.
7. `list_broadcasts` / `get_broadcast` — status, counts, and `failure_breakdown`.

## Audience

`create_audience` spec shapes:

- `{type:'respond_io_filter', filters:[...]}`
- `{type:'static_list', contacts:[{phone, attributes?}, ...]}`

Greeting templates (`Hola {{1}}`) need `attributes.first_name` on each static-list contact, or the phone must already be named in the CRM. Over MCP the freeze returns `BROADCAST_FROZEN_VARIABLE` at `create_broadcast_draft`, including when `scheduled_at` is set.

`preview_audience` returns `name_coverage.unnamed`: how many contacts would fall back to "Cliente" on a greeting template. Fix those before scheduling. For a static list, `sample.first_name` is what the send will use (spec attributes, then CRM).

## Greeting freeze

A greeting that collapses to one value is refused at draft time, including when `scheduled_at` is set. Persisted texts:

«Variable {{1}} colapsó al mismo valor para N destinatarios.»

«Variable {{1}} está fijada a un valor constante.»

The tool code is `BROADCAST_FROZEN_VARIABLE`. A failed greeting is not relaunched: recreate the list (real names) and create a new draft.

## Scheduling

`scheduled_at` is ISO-8601 with a timezone offset, at least 1 minute in the future and at most 1 year out. A bad value is `SCHEDULED_AT_INVALID`. To send now, omit `scheduled_at` and call `launch_campaign` only after the operator confirms the count.

`create_broadcast_draft` with `scheduled_at` is a real, scheduled send. Confirm the recipient count and time with the operator before that call.

## Fleet vs line routing

`routing_mode` and `fleet_id` live on the broadcast (`get_broadcast`). `assign_owners` writes the contact book for one line (`phone_number_id` is the Frepi UUID from `list_phone_numbers`). It does not send. Contacts with no owner stay `held_no_owner` — they are not guessed onto a line.

- **Line:** the audience is bound to the line (`create_audience` with its `phone_number_id`), or the draft's `phone_number_id` matches the audience's line.
- **Fleet:** `assign_owners(phone_number_id, audience_id)`, then `create_broadcast_draft` without `phone_number_id`. Children skip the audience binding; unassigned contacts stay `held_no_owner`.

## Fleet preview

Call `get_fill_preview` and show the operator the filled rows. Only after the operator approves them, call `get_fill_preview` again with `preview_ok=true`. Then confirm the count before `launch_campaign`.

## Launch

`launch_campaign` sends now. It is draft-only: a completed or failed broadcast is not relaunched. Confirm the recipient count first. `BROADCAST_NOT_LAUNCHABLE` means this draft cannot be sent (wrong state, or the launch gate refused it). Create a new draft after fixing the cause.

## Funnel

`recipient_counts` keys: `pending`, `sent`, `delivered`, `read`, `failed`, `cancelled`. `will_send` is pending plus sent. `failure_breakdown` is the top failure buckets: `code`, `title`, `count`, `help_topic`. Call `bi_help` with that `help_topic`.
