An AdOps custom metric is a named number that comes from a Google Sheet instead of from Meta. You give the metric a spreadsheet id, a sheet name, a key column and a value column; at evaluation time AdOps matches the campaign against the key column and reads the value column. That number can then act as a condition threshold, a budget amount, or a token written into a campaign name.

What is a custom metric for?

For every decision that depends on a number Meta does not have. Meta knows your spend, your purchases and your ROAS. It does not know your margin, your stock level, your supplier’s price change, your target ROAS per product, or the fact that this SKU is discontinued on Friday.

Three shapes cover most of it:

  • A target you maintain. One target_roas column, keyed by campaign id, edited when margins move.
  • A state from another system. Stock on hand, exported to a sheet by whatever runs your inventory.
  • A rate that changes. A daily budget allocation per product line, decided in a planning meeting and typed into a sheet rather than into twelve rules.

The common thread is that the number belongs to the business, changes on a human schedule, and should live in one place.

How should the sheet be laid out?

AdOps reads the sheet as a table, so the layout rules are the ones any CSV parser cares about. Get these right and the metric behaves; get them wrong and it silently reads the wrong column.

  • One header row, at the top. Header names are trimmed, so trailing spaces are forgiven; merged cells and title rows above the header are not.
  • One row per key. AdOps takes the first matching row for a key. A duplicate campaign id means whichever row sorts first wins, which is not a decision you want to make by accident.
  • Consistent column counts. Rows whose column count does not match the header are dropped rather than shifted into the wrong fields — a deliberate choice that turns a ragged export into missing data instead of wrong data.
  • Commas and quotes are fine inside cells. The parser handles quoted cells and doubled quotes, so product names and addresses survive.
  • Values should read as plain numbers. A value column carrying Rp 1.500.000 is text, not a number.

The metric form asks for the Google Spreadsheet ID — the long identifier in the sheet’s URL — plus the Sheet Name, the Column ID (the key column) and the Value Column. On save, AdOps reads the sheet and checks that both columns really exist. If either is missing, a red Google Sheets Validation Failed modal lists every problem with a “How to fix” checklist, and the metric is not saved. That check is the moment to catch a renamed tab.

One access note: AdOps reads spreadsheets as a Google service account, not as you. A sheet the service account cannot open will fail validation no matter how correct the column names are.

Where can the value be used?

Anywhere a metric is chosen, a Custom metrics tab appears next to the standard Meta metrics. That gives four positions:

PositionWhat it does
Condition metricTest the sheet value directly — stock equals 0, flag greater than 0
Comparison metricCompare a live Meta metric against the sheet value, each with its own period
Budget valueUse the sheet value as the amount for increase, decrease or set budget
Campaign-name tokenWrite the value into the campaign name with a {custom_metric|<id>} token

The comparison position is the most useful of the four. A condition of Purchase ROAS, Last 7 days, less than the target_roas custom metric gives every campaign its own threshold, held in a spreadsheet, without a rule per campaign.

The budget position is the one that changes the shape of an account. Set budget accepts a literal amount or a custom metric, so a planning sheet with a daily_budget column becomes the account’s budget allocation, applied by a rule on a schedule.

A worked example: pausing on stock

The rule everybody wants first, because it removes a recurring embarrassment.

  1. Export stock to a sheet with two columns: campaign_id and units_in_stock. One row per campaign.
  2. Create a custom metric named “Units in stock”, pointing at that sheet, with Column ID campaign_id and Value Column units_in_stock.
  3. Write a rule with two tasks:
    • Pause. Condition: the “Units in stock” custom metric equals 0.
    • Add to name. Same condition, APPEND mode, text [OOS {date}], action frequency once in a lifetime.
  4. Write the mirror rule to bring campaigns back: filter Campaign Status is PAUSED and Campaign Name contains OOS, condition “Units in stock” greater than a restock threshold, actions start plus remove from name.

The pair works because the sheet is the source of truth and both rules read the same number. Nobody has to remember which campaigns were paused, and the reason is written on the campaign.

How fresh is the number really?

This is the part to be precise about, because “reads a live spreadsheet” is close to true but not exactly true.

AdOps holds sheet rows in an in-memory cache with a two-minute window measured from the last access, and mirrors every successful fetch into MongoDB. A fetch goes to the direct spreadsheet export first with a 30-second timeout, falls back to the Google Sheets API, and falls back again to the rows stored in MongoDB.

Two consequences follow. First, because the cache window is measured from last access rather than from fetch time, a sheet being read continuously by frequent rule evaluations can keep serving the same snapshot; treat the value as near-real-time, not instantaneous, and do not build a rule that assumes an edit lands within seconds. Second, because the last resort is stored rows, a sheet that has become unreadable — sharing revoked, tab renamed, file moved to a bin — can keep evaluating against previously stored values rather than failing loudly. After any change to sharing or structure, open the metric and re-save it so the validation runs again.

How do you tell it is working?

From the execution log. When a rule condition used a custom metric, the log detail resolves the metric id back to its name, so a condition reads as “Units in stock” rather than a database id, and the Condition Evaluations panel prints the actual value the engine saw next to the expected one. That is the fastest check that the sheet lookup matched the row you meant.

In the rule builder, a custom metric id that no longer resolves renders in red — the signal that a metric was deleted while a rule still referenced it.

Two habits keep the whole thing boring, which is the goal. Keep one sheet per purpose rather than one giant sheet with many tabs of different shapes. And when a number matters enough to drive automated spend, put a person’s name in the sheet’s header row, because a spreadsheet that changes budgets deserves an owner.

A column in a Google Sheet becomes a condition you can rule on. Map it once and the builder treats it like any Meta metric. No sound. 7 sec No sound
  • custom-metrics
  • google-sheets
  • rules