# Broadcasts

> WhatsApp campaigns over the flow API — a campaign is a flow, the four statuses, the audience as a segment, the schedule in the bot's zone, and the template catalog you cannot write to.

Page: https://sdk.chatfuel.com/docs/modules/broadcasts
Markdown: https://sdk.chatfuel.com/docs/modules/broadcasts.md

## What it is [#what-it-is]

There is no campaign entity on the Chatfuel API. A campaign is a **flow** holding two blocks: an
entry point that carries the audience and the schedule — a `WhatsAppOneTimeNotificationBlock`
for "send now", a `WhatsAppScheduledMessageBlock` for "later" and "on a repeat" — connected to a
`WhatsAppTemplateBlock` that carries the message. One pair mutation creates both blocks and the
connection; every setter afterwards is keyed by one of the three ids on that pair. The module
reads every flow on the bot, keeps the ones shaped like that, and derives a campaign record from
each. The dashboard's flow builder shows the same flows.

Nothing in the API writes a WhatsApp template. Templates are written and submitted in WhatsApp
Manager and read here; the Templates tab is that catalog with each template's Meta status, a
refresh that asks Meta for the latest, and a door to WhatsApp Manager.

## Installed as [#installed-as]

|             |                                             |
| ----------- | ------------------------------------------- |
| Wizard id   | `broadcasts`                                |
| Install it  | `npx @chatfuel/wizard --modules broadcasts` |
| Selected by | Default — `--yes` installs it               |
| Requires    | Nothing, beyond the implicit `core`         |
| Recommends  | `contacts`, `channels`                      |
| Skill       | `chatfuel-broadcasts`                       |

Neither `hidden` nor `railHidden`; it sits in the rail's growth group beside Publishing. The two
recommendations are the audience's two ends: `contacts` is where the attributes an audience is
built from are edited, and `channels` is where the WhatsApp number this module sends from gets
connected — without a number the module shows the connect state on every screen.

## Routes and views [#routes-and-views]

The view is a path segment; everything else is a query parameter. The campaign list is the
default, so it has no segment of its own.

| Path                           | What it shows                                                           |
| ------------------------------ | ----------------------------------------------------------------------- |
| `/broadcasts`                  | The campaign list, with a panel beside it for the campaign `?c=` names. |
| `/broadcasts/compose?c=<flow>` | The composer over one draft: name, message, audience, schedule, review. |
| `/broadcasts/templates`        | The catalog on the bot's number, with Meta status.                      |

| Parameter | What it does                                                                                                    |
| --------- | --------------------------------------------------------------------------------------------------------------- |
| `c`       | The campaign — a flow id — whose panel or composer is open.                                                     |
| `step`    | Composer only: `name`, `message`, `audience`, `schedule` or `review`. Absent means the first step not yet done. |
| `status`  | List only: `draft`, `scheduled`, `sending` or `sent`. Absent means every status.                                |
| `q`       | The search box, on the list and on the catalog.                                                                 |
| `t`       | Catalog only: the template whose preview is open.                                                               |

An unknown value falls back in silence, and defaults are left out of what is written. `⌘K`
opens the command palette anywhere in the module and `?` the keyboard sheet.

## The model [#the-model]

**Four statuses, derived on every read.** The entry point's element carries a `status`
(`Draft`, `Live`, `Paused`, `Finished`) and the block an `isEntryPointEnabled` bit, and the
campaign's status is read off both against the clock:

| Kind      | Element             | Bit                               | Campaign                                     |
| --------- | ------------------- | --------------------------------- | -------------------------------------------- |
| One-time  | `Draft` or `Paused` | —                                 | draft                                        |
| One-time  | `Live`              | —                                 | sending                                      |
| One-time  | `Finished`          | —                                 | sent                                         |
| Scheduled | `Finished`          | —                                 | sent                                         |
| Scheduled | any                 | off                               | draft                                        |
| Scheduled | any                 | on, first send ahead or repeating | scheduled                                    |
| Scheduled | any                 | on, once and the time has passed  | sending, until the next read says `Finished` |

There is no paused state: `blockDisableEntryPoint` puts a scheduled element back to `Draft`, so
"take off the schedule" makes a draft with everything kept. A one-time pair is born with its bit
on and the bit cannot be turned off — the element's `status` is what counts. A row that is
sending is re-read every ten seconds until it is not; a one-shot that is sending by the clock
alone is re-read for an hour past its time, and Refresh after that.

**Arming is silent when it fails.** `blockEnableEntryPoint` re-validates the campaign and, when
any verdict stands, answers the flow unchanged with the bit still off and no error. The module
reads the bit back and shows the verdicts rather than assuming. Verdicts are data —
`blockElements[].errors[].code`, snake\_case, recomputed on every write — and each is printed on
the review step under the step that fixes it. A thrown refusal sits at
`extensions.errors[0].extensions.code`, one level down through the router; an unregistered one
arrives as a bare `InternalServerError`, which is what every write to a live schedule answers.
So the module disarms before it writes a time, and arms again on confirm.

**Times are instants; weekdays are UTC.** The first send goes up as an ISO instant and comes
back as a `Z` string. A repeat's weekdays are stored normalised to UTC, so a Monday 01:00
campaign on a bot three hours east of Greenwich is stored as Sunday. The module shifts the list
by the bot zone's day offset at the first send on the way out and on the way in, and sends the
corrected list with every first-send-time write while the stored list is non-empty. The order of
writes is the server's: repeat type, then its list, then the first send time last. The zone is
the bot's; an operator's own zone is the fallback when the bot has none.

**The audience is a segment.** The entry point holds a `SegmentInput` with client-minted UUIDs;
"everyone" is a segment with no filters and is valid as it stands. The recipient count is
`contactsTotalCount(platforms: [whatsapp], segment)` with the very same segment. The builder
writes two levels — the segment's own filters and one nested segment per filter slot — and reads
three, keeping whatever it cannot show (a tag, a stored segment, a date strategy, a third level)
as passthrough that is re-sent as read. Writes and counts are debounced together and the count
is epoch-guarded, so a figure never describes a filter that has since changed.

<Callout type="warn">
  **An unknown attribute name in a template parameter creates the attribute.** `{{Attribute
    name}}` in a blank becomes a reference the server parses; a name the bot does not have is
  accepted and a custom attribute is created for it, with no API to delete one. The composer's
  Insert field offers only `botAttributes(platforms: [whatsapp])`, and text typed by hand is sent
  as text. In an audience filter an unknown name is harmless — it selects nobody.
</Callout>

**A block never changes type, and there is no clone.** "Send now" on a scheduled draft creates
the other pair on the same flow, replays the template and the audience onto it, and deletes the
old entry point; the old template block cannot be deleted and stays in the flow, unconnected.
Duplicate is a new flow with the source replayed onto it — template, every filled blank, media
by file id, the segment with fresh ids, the repeat type and its list, never the first send time.
Neither is one operation, and a partial failure keeps what was made and lists what is missing.

**There are no delivery figures.** `WhatsAppTemplateBlock.stats` answers a server error on
every read, so the module never selects it. The only count the API has is
`sentToContactsCount` on a finished one-time element, and it is sometimes null.

## Configuration [#configuration]

| Variable                     | Notes                                           |
| ---------------------------- | ----------------------------------------------- |
| `CHATFUEL_TOKEN`             | Secret. Server-side only.                       |
| `VITE_CHATFUEL_WORKSPACE_ID` | The workspace you picked during the wizard run. |
| `CHATFUEL_API_BASE`          | Defaults to `https://panel.chatfuel.com`.       |

Nothing of its own. The number it sends from is connected in the [Channels](/docs/modules/channels)
module, and the module asks for the template catalog only once the bot has one.

## Permissions [#permissions]

| Object   | Action | Required for                                                                                                                                                              |
| -------- | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Flows`  | `View` | Listing campaigns — every campaign is a flow, read through the bot's flow groups.                                                                                         |
| `Flows`  | `Edit` | Creating, filling, scheduling, sending, taking off the schedule and deleting. Every write is a flow-builder block mutation, and the server checks this, not Broadcasting. |
| `Bot`    | `View` | The template catalog, the bot's time zone and the connected number.                                                                                                       |
| `People` | `View` | The attribute catalog behind the audience builder and Insert field, and the recipient count.                                                                              |

## Limits [#limits]

**The five time setters refuse while the campaign is armed**, with a bare server error that
names nothing; the module disarms first. **A one-time campaign that has started is immutable**:
send and segment both answer `WhatsAppOneTimeBroadcastAlreadyStarted`, and the module treats
that as "already sent" and re-reads. **The catalog ignores paging** and answers the whole list.
&#x2A;*Never call `deleteFlowGroup`** — it deletes every flow in the group, campaigns included.

The module's own ceilings:

|                                                    |                                                            |
| -------------------------------------------------- | ---------------------------------------------------------- |
| Days between repeats                               | 1 to 365 — the server refuses above 1000 with a bare error |
| Dates in a repeat                                  | 500                                                        |
| Copy code                                          | 15 characters                                              |
| Text blank                                         | 1,024 characters                                           |
| Link parameter                                     | 2,083 characters                                           |
| Conditions in an audience                          | 20                                                         |
| Groups in an audience                              | 10                                                         |
| Campaign name                                      | 120 characters                                             |
| Wait before a filter change is written and counted | 400 ms                                                     |
| A sending row re-read                              | every 10 seconds, for an hour past a one-shot's time       |
| Catalog read after a Meta refresh                  | at 3 s and again at 10 s                                   |

<Cards>
  <Card title="Broadcasts, as a product" href="/modules/broadcasts" description="What your clients send with it, the three decisions, and where a campaign is — from the person sending it." />

  <Card title="Contacts" href="/docs/modules/contacts" description="Recommended alongside this one: the attributes an audience is built from, and where they are edited." />

  <Card title="Channels" href="/docs/modules/channels" description="Recommended alongside this one: where the WhatsApp number this module sends from gets connected." />

  <Card title="Your agent's notes" href="/docs/concepts/agent-skills" description="The wizard installs .claude/skills/chatfuel-broadcasts/ (Codex: .agents/skills/chatfuel-broadcasts/) — references/campaign-model.md walks a campaign from nothing to sent, schedule.md has the weekday arithmetic, audience.md the segment, templates.md the blanks and the WhatsApp Manager hand-off." />
</Cards>
