# Ads Optimization

> Conversion reporting for click-to-WhatsApp ads — event sets over one automation scope, the seven triggers, and the permission that decides whether any of it is delivered.

Page: https://sdk.chatfuel.com/docs/modules/ads-optimization
Markdown: https://sdk.chatfuel.com/docs/modules/ads-optimization.md

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

Nothing here is an ads dashboard. This module writes one setting on one automation scope: the
event sets on `bot.fuelyAutomations(scope: WhatsAppClickFromAds)`, which say which moments in a
click-to-WhatsApp conversation are reported to Meta as conversions over the Conversions API — so
Meta can optimize delivery towards the people who actually convert.

The surface is one screen: the sets on the left, the selected set beside them. And the ads a set
claims are opaque strings that nothing ever checks.

## Installed as [#installed-as]

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

Neither `hidden` nor `railHidden`. The two recommendations are the surfaces the same data is
seen from elsewhere: `automations` is the AI side of the very automations these sets live on,
and `contacts` owns the properties and statuses two of the triggers fire on.

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

The path segment is not a view name here — it is the set. There is only one view.

| Path                               | What it shows                                         |
| ---------------------------------- | ----------------------------------------------------- |
| `/ads-optimization`                | The rail of sets, and the default set open beside it. |
| `/ads-optimization/<automationID>` | The same, with that set open.                         |

| Parameter | What it does                                                             |
| --------- | ------------------------------------------------------------------------ |
| `e`       | The event whose editor is open — an event id, or `new` for an empty one. |

Both survive a reload, because both are places a link can be sent to. Nothing else about the
surface is, so nothing else is in the address.

## The model [#the-model]

**A set is a `FuelyAutomation`.** A bot has exactly one **base** set — `isBase: true`, and the
API leaves its `name` null, which is why the module supplies one ("Default events for all ads")
— and up to **thirty** custom ones. The base set carries no list of ads at all: the API strips
filter settings from a base automation, and &#x2A;that absence is what makes it "everything"*. A
custom set claims the ads it lists in `FuelySettingListOfAds.adIDs` and overrides the base for
exactly those.

Every setting on a custom set either holds its own value or follows a parent, named by
`inheritsFrom`; `canInheritFrom` says which parents it may be pointed at. Two consequences are
worth building around. Saving an inherited setting takes a private copy — the set stops
following, and for `sendEventsToMeta` **every event id is regenerated**, so ids read a moment
ago are gone. And a write on a parent republishes its children: one edit arrives as several
subscription events, one per affected set, so merge by id.

An event is a **conversion name** plus a **trigger**. The name is one of Meta's fourteen
standard ones, or one of your own — at most fifty characters, reported to Meta verbatim, and
refused if it is one of the fourteen spelled differently.
`bot.availableMetaConversionEventNames` remembers the custom names this bot has already used;
do not hard-code either list.

The seven triggers, in the order the picker offers them:

| Trigger              | Fires when                                                                                                                  |
| -------------------- | --------------------------------------------------------------------------------------------------------------------------- |
| Keywords             | The contact sends a message that matches the words you list.                                                                |
| Contact property     | A property on the contact card comes to match your condition.                                                               |
| Contact status       | The contact reaches one of the statuses you pick — five of them, the same words the rest of the app uses for a sales stage. |
| Handed to a human    | The AI passes the chat to a teammate, or a teammate takes it.                                                               |
| Booking              | A booking is made in the conversation.                                                                                      |
| First message        | The contact sends the first message of the conversation.                                                                    |
| A condition in words | The AI decides the conversation has met a condition you describe.                                                           |

**Every save rewrites the whole ordered list of events.** There is no add-one and no delete-one
mutation; order is stored and is part of the value. Leave an id out to delete that event, keep
it to keep it, omit it entirely to add one. Two events reporting the same conversion on the same
trigger are refused. The API also cleans the input before it validates — ad ids and keywords are
trimmed, blanks dropped, repeats removed — so what you read back is not always what you sent.

<Callout type="warn">
  **Configured is not delivered.** Nothing here is sent unless the bot's WhatsApp number can
  carry it, and the three ways it cannot all look identical on screen: no
  `WhatsAppPhoneContactScope` (no number at all), `hasMetaConversionsAPIPermission: false` (the
  stored Meta token lacks the permission), or `accessLost: true` (the number is no longer
  reachable with this token). Re-granting the permission is an interactive consent inside
  Chatfuel, which an app on the public API cannot run on somebody's behalf — so the module reads
  the state, shows an alert and links out. Without that alert, a fully configured set that
  reports nothing is indistinguishable from one that works.
</Callout>

**Ad ids are opaque and unverified.** Any string under sixty characters is accepted, no query
turns an id back into an ad, and nothing reports that an id names an ad that was deleted, paused
or never existed. So a typo saves happily. The module validates the *shape* — a bare 15-to-20
digit run — and never claims an id is real. It also takes ids out of whatever is in the
clipboard, because nobody reads an id off a screen: they copy the Ads Manager address bar. When
`selected_ad_ids=` is present it is trusted **exclusively**, since `act=` and `business_id=` are
digit runs too and taking either for an ad id silently points a set at nothing.

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

No secret and no database of its own. Every variable the app reads, and which side reads it, is
on [environment variables](/docs/deploy/environments).

## Permissions [#permissions]

| Object  | Action | Required for                                                            |
| ------- | ------ | ----------------------------------------------------------------------- |
| `Flows` | `View` | Reading the event sets and their settings.                              |
| `Flows` | `Edit` | Creating, renaming and deleting sets, and writing ads and events.       |
| `Bot`   | `View` | Reading the WhatsApp connection that conversions are delivered through. |

The third one is why a token with full Flows rights can still show an empty delivery state: the
permission that decides whether anything is sent is read off the bot, not off the automation.

## Limits [#limits]

**The server refuses the whole list.** Not the offending entry — the list. One bad twentieth
event throws away nineteen good ones typed in the same session, which is why the module checks
every ceiling before the write, using the same error codes the server would have answered with.

**Two sets claiming the same ad is not reported anywhere.** One of them silently loses. The
whole list is in hand after one query, so the module builds the reverse index itself and names
the rival set on the chip; nothing on the server will.

**A contact-property condition the server dislikes is stored anyway.** The save succeeds and the
error comes back on the event in `attributeConditionErrors`, so read that array or the condition
looks fine and does nothing.

**Errors arrive one level down.** The top-level message names a failed subgraph fetch; the code
is at `errors[0].extensions.errors[0].extensions.code`. Read both levels or you will find
nothing.

**There is no history and no read-back.** Conversions are sent and forgotten: no query reports
what Meta received, what it accepted, or how an ad performed afterwards. That question is Ads
Manager's. The API *does* expose ads — `currentUser.metaAdAccounts` and
`MetaAdAccount.ads(...)` — and none of it is used here, for three reasons: it hangs off the
signed-in person's Facebook login rather than off the bot, `MetaAd` cannot be looked up by id
(resolving a set's ids means paging an account and matching client-side), and the insights carry
no date range at all — one aggregate figure per ad, and no trend to draw.

**Every write lands on the production bot immediately.** There is no draft mode and no preview.
Undo is one entry, sixty seconds, and a compensating write: a restored event comes back with a
new id. And turning a set on can be refused by the plan —
`NotAllowedBySubscriptionFeatureSet` on a plan without AI. A concurrent write on the same bot
answers `FuelyAutomationBeingEdited`, which is a lock rather than a conflict and clears on its
own.

The ceilings, each with the code the server answers when you pass it:

|                               |                |                                               |
| ----------------------------- | -------------- | --------------------------------------------- |
| Custom sets per bot           | 30             | `FuelyAutomationScopeLimitReached`            |
| Set name                      | 200 characters | `FuelyAutomationNameInvalid`                  |
| Ads per set                   | 50             | `FuelyListOfAdsTooManyEntries`                |
| One ad id                     | 60 characters  | `FuelyAdIDTooLong`                            |
| Events per set                | 20             | `FuelySendEventsToMetaTooManyEvents`          |
| A conversion name of your own | 50 characters  | `FuelySendEventsToMetaCustomEventNameTooLong` |
| A condition in words          | 512 characters | `FuelySendEventsToMetaConditionPromptTooLong` |
| Keywords per event            | 50             | `FuelyKeywordsTooMany`                        |
| One keyword                   | 50 characters  | `FuelyKeywordTooLong`                         |

<Cards>
  <Card title="Ads Optimization, as a product" href="/modules/ads-optimization" description="The fourteen conversions, the seven triggers, how inheritance works, and the questions this API cannot answer." />

  <Card title="AI Agent" href="/docs/modules/ai-agent" description="The same automations from the AI side — the eighteen sources, of which WhatsAppClickFromAds is one." />

  <Card title="Contacts" href="/docs/modules/contacts" description="The properties and statuses two of the seven triggers fire on." />

  <Card title="Your agent's notes" href="/docs/concepts/agent-skills" description="The wizard installs .claude/skills/chatfuel-ads-optimization/ (Codex: .agents/skills/chatfuel-ads-optimization/) — references/events.md for the triggers and names field by field, and references/ads.md for what nothing can tell you about an ad id." />
</Cards>
