---
title: "Audience messages"
url: "/scheduled-tasks/audience-messages"
description: "Message everyone in a data table who matches your filters — one message per row, no AI processing."
productArea: scheduled-tasks
audience: ["business-user"]
tags: ["scheduled-tasks", "data-tables"]
lastReviewed: 2026-08-27
---

# Audience messages (/scheduled-tasks/audience-messages)





Most scheduled tasks message one contact. An **Audience Message** messages everyone in a
[data table](/data-tables) who matches your filters — one message per matching row, on WhatsApp
or Telegram.

Create one from the agent's **Scheduled Tasks** tab, the same place as any other task, by
choosing the **Audience Message** action.

## How it differs from a single-contact task [#how-it-differs-from-a-single-contact-task]

|                             | Agent message                                      | Audience message                |
| --------------------------- | -------------------------------------------------- | ------------------------------- |
| Who receives it             | One contact                                        | Every row matching your filters |
| The agent runs              | Yes — it writes the reply                          | No — you write the message      |
| Recipients must be contacts | Yes                                                | No, a table row is enough       |
| Billing                     | One [interaction](/reference/interaction-counting) | One interaction per recipient   |

Because no model runs, an audience message says exactly what you wrote. It is a way to reach a
list, not a way to hold a conversation — though if a recipient replies, the agent picks up the
conversation the message opened, with the campaign for context, rather than starting cold.

There is no "payment reminder" or "birthday" feature. Those are a filter and a template, and
building them is the point.

## Choosing who receives it [#choosing-who-receives-it]

Pick a **Data table**, then add **Filters**. Everyone in the table who matches them all will be
messaged. Only tables the agent has read permission on are offered — see
[permissions](/data-tables#permissions) if the table you want is missing.

Filter values can use date tokens, resolved against the task's timezone on the day it runs:

| Token             | Resolves to                                               |
| ----------------- | --------------------------------------------------------- |
| `{{today}}`       | The run date, as `YYYY-MM-DD`                             |
| `{{today+7d}}`    | Seven days after the run date                             |
| `{{today-3d}}`    | Three days before the run date                            |
| `{{today:MM-DD}}` | Month and day only — for anniversaries, ignoring the year |
| `{{today:YYYY}}`  | The year only                                             |

So "invoices due in a week" is `due_date` **is** `{{today+7d}}`, and "birthdays today" is
`birthday` **is** `{{today:MM-DD}}` against a column holding `MM-DD`.

## Which column can be addressed [#which-column-can-be-addressed]

**Send to** picks the column holding each recipient's address. Only two column types can be
messaged, and which one depends on the channel.

| Channel  | Column type that works |
| -------- | ---------------------- |
| WhatsApp | WhatsApp ID            |
| Telegram | Telegram ID            |

<Callout type="warn">
  A **Phone** column does not work for WhatsApp. The two are different identities even when the
  number looks the same, and a phone column is reserved for a future SMS channel. A **Telegram
  handle** cannot be messaged either — it can authorise someone who messages your agent, but the
  Telegram Bot API cannot start a conversation from an @username. An **Email** column has no channel
  at all.
</Callout>

If your table stores numbers in a phone column, add a WhatsApp ID column and populate it with the
digits-only form. See [field types](/reference/data-field-types).

Rows that match your filters but have nothing in the **Send to** column are skipped, and counted
separately in the preview.

### Addressing through a linked table [#addressing-through-a-linked-table]

The address does not have to live on the table you are filtering. If your table has a
[relation](/reference/data-field-types) column linking each row to one record in another table —
a **Payments** table where every payment links to a **Contacts** record — **Send to** also offers
that table's columns, shown as **Contact → WhatsApp ID**.

That is what lets you filter payments and message people: the filters run on the payments row,
the message goes to the number on the linked contact. You do not have to copy the number onto
every payment, so it stays correct when the customer changes it.

Two limits:

* The relation must link to **one** record, not many. A relation that holds several records names
  no single recipient, so it is not offered.
* **Filters still run on the table you picked.** You cannot filter on a linked table's columns —
  filter on Payments, address through Contacts.

A payment whose relation is empty, or points at a record that has since been deleted, is skipped
and counted separately in the preview from rows whose contact simply has no WhatsApp ID. The two
have different fixes: link the record, or fill in the number.

A table with no usable column — its own or through a link — cannot be an audience, and the editor
says so rather than letting you save.

## Writing the message [#writing-the-message]

On **WhatsApp** you must select an approved template. Meta only allows a business to start a
conversation with one, and there is no guarantee the 24-hour window is open when the task runs.
See [templates and quality](/channels/whatsapp#templates-and-quality).

On **Telegram** you write the message text directly.

Template parameters can be filled three ways:

| Parameter type       | What it fills in                        |
| -------------------- | --------------------------------------- |
| **Static text**      | The same value for everyone             |
| **Column**           | A value read off each matching row      |
| **Recipient number** | The number the message is being sent to |

**Column** is what makes the message personal — bind a parameter to `first_name` or
`amount_due` and every recipient gets their own. There is no contact name or agent name token
here, because an audience has no single contact behind it.

Columns on a linked table are offered here too, on the same terms as **Send to** above — so a
payment reminder can greet people by the name held on their contact record without copying it
onto every payment. In Telegram message text they are written `{{contact.first_name}}`.

## Attaching an image [#attaching-an-image]

An audience message can carry one image, sent as a **single message** with your text as its
caption — not a photo followed by a separate bubble. Everyone in the audience gets the same
image; it cannot vary per row.

Only images **shared with all conversations** can be attached. Open the file in
[Assets](/knowledge/assets), choose **Share with all conversations**, and give the agent permission to
send it. Files belonging to one customer — anything they uploaded in a chat, or an image the
agent generated for them — are deliberately not offered: an audience message has no single
customer behind it, so there is nothing to check a private file against.

On **Telegram** you can attach an image to any message. Keep the text under **1,024 characters**
— past that, the remainder arrives as a second message.

On **WhatsApp** the image travels in the template's header, so it only works with a template
that Meta approved **with an Image header**. Create one in WhatsApp Manager with header type
**Image**; the Image field appears once you select it, and is then required. A template's layout
is fixed when Meta approves it, so an image cannot be added to a text-only template.

<Callout type="warn">
  A message with an image counts as **two interactions** per recipient instead of one. A 500-person
  birthday campaign with a photo uses 1,000. See [interaction
  counting](/reference/interaction-counting).
</Callout>

## Checking who matches [#checking-who-matches]

Select **Check who matches** before saving. It reports how many people will be messaged, lists
the first few with their addresses partly hidden, and tells you how many matching rows were
skipped — separately for rows with no address and rows not linked to a record in the table being
messaged.

The preview stores nothing and sends nothing. Use it — a mistyped filter is invisible until the
messages have gone out.

## Updating the row after sending [#updating-the-row-after-sending]

**After sending, update** lists the columns to write back to once the message has gone out.
Add as many as you need — the rows stack the same way filters do.

This matters more than it looks. A recurring task has to change something about the rows it
just messaged, or it matches the same people again on the next run.

Each row picks a column and what to write into it. What is on offer depends on the column:

| Write           | Available on                       | What it writes                                  |
| --------------- | ---------------------------------- | ----------------------------------------------- |
| the send time   | date, date-and-time, text          | The moment the message actually went out        |
| set to          | any column you can type a value in | A fixed value you choose                        |
| move forward by | date                               | The row's own date, plus *N* months             |
| add to it       | number, currency                   | The row's own number, plus (or minus) an amount |

### Record that they were reached [#record-that-they-were-reached]

Write **the send time** into a column, so a task filtered on "reminder sent **is empty**" stops
matching that row.

### Move a date forward to the next cycle [#move-a-date-forward-to-the-next-cycle]

**Move forward by *N* months** sets the column to its own current value plus that many months,
instead of the send time. This is the recurring-due-date shape: a subscription renewal, an
appointment, an insurance premium.

Say policies carry a `next_premium_due_date`. One task covers every quarterly policy:

| Setting               | Value                                            |
| --------------------- | ------------------------------------------------ |
| Filter                | `next_premium_due_date` **is** `{{today+7d}}`    |
| Filter                | `premium_frequency` **is** `Quarterly`           |
| Filter                | `policy_status` **is** `Inforce`                 |
| After sending, update | `next_premium_due_date`, forward by **3** months |

Each run messages whoever is due in a week and rolls their date to the next quarter, so the
following quarter is picked up with no further setup. Author one task per frequency — monthly
forward by 1, semi-annual by 6, annual by 12 — each filtered to its own frequency value.

<Callout type="info">
  Month-ends clamp rather than overflow: a due date on 31 January moves to 28 February (29 in a leap
  year), not 2 March. A date that was clamped returns to its original day as soon as the target
  month is long enough, so a policy due on the 31st stays on the 31st.
</Callout>

Only **date** columns can be moved forward. Date-and-time and text columns can still be stamped
with the send time. If a row's date column is empty or unreadable, that row is still messaged
but that column is not written — writing the send time there would silently re-date the record
to today. The rest of the row's write-backs still apply.

### Multi-step follow-ups [#multi-step-follow-ups]

Stacking is what makes a follow-up sequence possible: a column records which step someone is
on, the filter reads it to decide who gets which message, and the write-back advances it.

Say contacts carry a `follow_up_msg` number. Author one task per step:

| Setting               | Step 1 task                       | Step 2 task                       |
| --------------------- | --------------------------------- | --------------------------------- |
| Filter                | `follow_up_msg` **is** `1`        | `follow_up_msg` **is** `2`        |
| WhatsApp template     | `follow_up_1`                     | `follow_up_2`                     |
| After sending, update | `follow_up_msg` set to `2`        | `follow_up_msg` set to `3`        |
| After sending, update | `last_contacted_at` the send time | `last_contacted_at` the send time |

After step 1 goes out, those contacts stop matching step 1 and start matching step 2, so the
next run picks them up with the next message. Someone at `3` matches neither and drops out of
the sequence.

**Add to it** does the same job without naming the next number — `follow_up_msg` plus **1**
reads whatever the row holds and adds one. A row where the column is empty counts as zero, so
it becomes 1. Use whichever reads more clearly; the filter still decides the template either
way.

### What applies to every write-back [#what-applies-to-every-write-back]

A write-back happens only if the agent has update permission on that column, and only after the
message has gone out. If one fails, the message is **not** re-sent — the send is the thing that
matters, and a failed write-back is recorded in the logs. Columns are handled independently: a
column the agent may not write is skipped while the rest still apply.

<Callout type="warn">
  Moving a date forward happens when the **reminder** is sent, not when the customer acts on it.
  After a payment reminder goes out, the due date already points at the next cycle — so if you also
  need to know who has not paid, track that in a separate column rather than reading it off the due
  date.
</Callout>

## Limits [#limits]

* An audience is capped at **5,000 recipients**. Over that, the task sends **nothing** rather
  than an arbitrary subset — messaging half a list is worse than messaging none of it.
* If the audience is larger than your workspace's remaining monthly allowance, it also sends
  nothing. Check [interaction counting](/reference/interaction-counting) and your plan's
  allowance before scheduling a large run. An image doubles what each recipient costs, so it
  halves how large an audience your remaining allowance covers.
* Messages go out steadily rather than all at once, because WhatsApp and Telegram both pace what
  a business can send. A large audience takes a while to work through.
* Failed sends are retried a few times with an increasing gap. Someone who has blocked your bot,
  or whose number is not reachable, is not retried.

## Who can be messaged [#who-can-be-messaged]

The channel's allow and deny lists apply to every message an audience sends, exactly as they do
to someone messaging in. If a channel is **Restricted**, only people its allow list covers are
messaged. See [access control](/channels#access-control).

On WhatsApp, a customer who has opted out of marketing is skipped for marketing templates, but
still receives utility ones. See
[opting out of marketing messages](/channels/whatsapp#opting-out-of-marketing-messages).

An agent cannot create an audience message for itself. They are made in the dashboard only.

## Troubleshooting [#troubleshooting]

| Symptom                                   | Likely cause                                                                                                          |
| ----------------------------------------- | --------------------------------------------------------------------------------------------------------------------- |
| **Audience Message** is not offered       | The agent has no active WhatsApp or Telegram channel                                                                  |
| The data table is not in the picker       | The agent has no read permission on that table — or on the table it links to                                          |
| "This table has no WhatsApp ID column"    | The addresses are in a phone column, which WhatsApp cannot use                                                        |
| A linked table's columns are not listed   | The relation links to many records, not one — or the agent cannot read the linked table                               |
| "Filters cannot reach through a relation" | A filter names a linked table's column. Filter on the table you picked; address through the link                      |
| The preview finds nobody                  | A filter names a renamed column, or a date token resolved to a different day in the task's timezone                   |
| Saved, ran, nothing sent                  | Every match was skipped — no address in the **Send to** column, no linked record, or the channel's lists exclude them |
| Fewer messaged than the preview showed    | Recipients opted out of marketing, or were added to the deny list since                                               |
| The same people are messaged every run    | The filter has nothing to exclude them — set **After sending, update**                                                |
| "Move forward by" is not offered          | The chosen column is not a **date** column. Each column offers only what its type supports                            |
| A date column was not moved forward       | That row's date was empty or unreadable, so that column was left alone rather than re-dated to today                  |
| One column updated but another did not    | That row could not supply a base value, or the agent has no update permission on that column                          |
| The task failed with "invalid payload"    | A write-back names a renamed, encrypted or linked-table column, or a value the column will not accept                 |
| Nothing sent at all on a large audience   | The audience is over 5,000, or over your remaining monthly allowance                                                  |

## Related pages [#related-pages]

* [Scheduled tasks](/scheduled-tasks)
* [Data tables](/data-tables)
* [WhatsApp setup](/channels/whatsapp)
* [Telegram setup](/channels/telegram)
* [Access control](/channels#access-control)
* [Interaction counting](/reference/interaction-counting)
