# How the Optimizer Works (/docs/concepts/optimizer)



The optimizer is the most consequential thing Layers does for you. It runs
every 6 hours per ad layer (Meta, TikTok, Apple Search Ads), evaluates each
campaign, adset/adgroup, ad, and keyword, and proposes actions such as:

* **Pause** an entity that's burning budget without converting.
* **Adjust budget** (up or down) on an entity with strong or weak unit
  economics.
* **Adjust bid** (Apple / TikTok keywords).
* **Hold** (no action) — the most common outcome, by design.

Proposed actions then pass through a guardrail approval stage; only approved
actions are executed.

## Inputs [#inputs]

For each entity, the optimizer reads:

| Input                                    | Source                                                                                                         |
| ---------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| **Spend** (1d, 3d, 7d, 14d, 30d windows) | Platform metrics (`meta_ad_level_daily_metrics`, `tiktok_ad_level_daily_metrics`, `apple_ads_*_daily_metrics`) |
| **Conversions / installs**               | Same tables                                                                                                    |
| **CPA**                                  | spend / installs per window                                                                                    |
| **CTR, CPM, conversion rate**            | Platform metrics                                                                                               |
| **Fatigue indicators**                   | Frequency (Meta), trend of CPA across windows                                                                  |
| **Age**                                  | Days since entity creation                                                                                     |
| **Creative pool**                        | `generated`, `ugc`, or `manual` (used at selection time, not pause time)                                       |

`ads_content.organic_score` is used when selecting new creatives to launch —
it does NOT pause already-running ads.

## Decisions [#decisions]

The optimizer assigns each ad (and each adset, adgroup, campaign) one of the
following classifications every cycle:

* `TOO_YOUNG` — under 3 days old. &#x2A;*Never paused.**
* `LEARNING_PHASE` — platform reports the entity is still in learning.
  &#x2A;*Never paused.**
* `TOP_PERFORMER` — performance score ≥ 80. &#x2A;*Never paused.**
* `HEALTHY` — performance score ≥ 50. &#x2A;*Never paused.**
* `UNDERPERFORMING` — CPA materially above pool average, or low performance
  score.
* `FATIGUED` — CPA degraded over time, frequency high, sufficient spend.
* `CRITICAL` — >$50 spent with zero installs in 7d, or CPA > 2.5× pool
  average.

Only `UNDERPERFORMING`, `FATIGUED`, and `CRITICAL` entities are eligible for
pausing. Every decision is gated by the
[guardrails](/docs/concepts/safety-guardrails).

## Guardrails [#guardrails]

Hard rules that gate every proposed action:

1. **Min age 3 days** before any action on an entity.
2. **Min $5 spent** before a pause action in the content-refresh disable
   step.
3. **Min 100 impressions** before budget/bid changes.
4. **Never disable** ads classified `TOP_PERFORMER` or `HEALTHY`.
5. **Never leave an adset/adgroup empty**.
6. **Max 50% budget change** per cycle, and never more than $100 absolute.
7. **Max 40 total actions and 20 pauses** per cycle.
8. **Cooldowns** between actions on the same entity: 48h for structural
   and budget changes, 24h for bid changes.
9. **Manual overrides win** — `override = 'include'` keeps a creative
   eligible regardless of score; `override = 'exclude'` forces ineligible.

These rules exist so the optimizer never bricks an ad account by pausing or
rebalancing everything during a noisy day.

## Why a low organic score doesn't pause running ads [#why-a-low-organic-score-doesnt-pause-running-ads]

A common confusion: the **organic score** affects which creatives are *picked*
for the next launch. It doesn't pause already-running ads. If a creative's
organic score decays below `4.0` while it's running, it stays running — the
optimizer is purely watching ad performance metrics (CPA, fatigue, CTR).

This intentional split prevents thrash: an ad with a great CPA but aging
organic score wouldn't be killed mid-run.

See [Organic scoring](/docs/concepts/organic-scoring) and
[Ad eligibility](/docs/concepts/ad-eligibility) for the full picture.

## Cycle cadence [#cycle-cadence]

| Layer            | Optimizer interval     | Emergency watchdog |
| ---------------- | ---------------------- | ------------------ |
| Meta Ads         | every 6 hours          | every 2 hours      |
| TikTok Ads       | every 6 hours          | every 2 hours      |
| Apple Search Ads | every 6 hours (at :30) | every 2 hours      |

The watchdog is a lightweight safety check that auto-pauses entities with
catastrophic spend patterns (e.g., $50+ spent and 0 installs) between full
optimizer cycles.

## Reading optimizer decisions in the UI [#reading-optimizer-decisions-in-the-ui]

Each optimizer run writes its proposals, approvals, and rejections (with
reasons) to the project's workflow-run history. Open the layer detail page
and look at the most recent `ad-optimize` run to see:

* Classifications assigned to each entity and the metrics that drove them.
* Proposed actions, and whether each was approved or rejected by guardrails.
* Reasons actions were rejected (e.g., "Entity too young (2d \< 3d minimum)").

## Pausing the layer [#pausing-the-layer]

If you need a freeze (e.g., during a launch week, regulatory review, or system
incident), use the **Pause** action on the layer's Danger zone. While paused,
no scheduled commands run — the optimizer stops proposing or executing
actions until you resume.

## Next [#next]

* [Safety guardrails](/docs/concepts/safety-guardrails) in detail.
* [Reading the dashboard](/docs/paid-media/reading-the-dashboard).
