# Agency multi-account

> One rule shape applied across client ad accounts, with each client's ROAS target and monthly cap read from a Google Sheet at evaluation time.

Source: https://adops.id/use-cases/agency-multi-account/
Last updated: 2026-08-20
Publisher: AdOps (https://adops.id) — an independent product, not affiliated with Meta Platforms, Inc.

---

## Key facts

- **Work distribution:** One queued job per ad account (Rules due for execution are dispatched by a sweep that runs every 10 seconds)
- **Custom metric source:** Google Sheets (A spreadsheet id, sheet name, lookup column and value column, resolved at evaluation time)
- **Custom metric cache:** 2 minutes in memory, last-access TTL (Falls back to the last rows stored in MongoDB if the live fetch fails)
- **Pre-filter fields:** 3 (Campaign ID equals or not equals, Campaign Name contains or does not contain, Campaign Status is or is not)
- **Meta entity fetch retries:** 3 total attempts, waiting 1 second then 2 seconds
- **Shipped rule templates:** 10, in 4 categories (budget, status, naming, monitoring)

**Who it is for:** An agency media buyer holding a dozen Meta ad accounts, each with its own target, cap and reporting rhythm.

**The problem:** Every client has a different ROAS target and a different monthly cap, so the same rule has to be rewritten and re-checked per account, and one edited threshold on a Friday is discovered on a Monday.

**The outcome:** One rule shape is reused across accounts, per-client numbers live in a Google Sheet the account manager owns, and each ad account gets its own queued job with its own log.

## Playbook

### Compare against the client's own target

- **When:** Purchase ROAS over the last 7 days is less than the custom metric `target_roas` for that client AND Spend over the last 7 days is greater than Rp 3,000,000
- **Then:** Pause campaign, and Add to name in Append mode with the text `[BELOW-TARGET {date}]`
- **Why:** The target lives in a Google Sheet row the account manager maintains, so changing a client's target is a spreadsheet edit rather than a rule edit.

### Hold the retainer cap

- **When:** Spend this month is greater than the custom metric `monthly_cap` for that client
- **Then:** Pause campaign, and Add to name in Append mode with the text `[CAP-REACHED]`
- **Why:** A retainer overspend is the one mistake an agency pays for out of its own margin, and the cap is different for every client.

### Ceiling on any single campaign

- **When:** Daily Budget is greater than Rp 2,000,000
- **Then:** Set budget to Rp 2,000,000 as a fixed amount
- **Why:** The engine re-reads the live Daily Budget from Meta before writing, so a budget raised by hand or by another tool is pulled back to the agreed ceiling.

### Scope one rule to one client inside a shared account

- **When:** Pre-filtered to campaigns whose Campaign Name contains `ACME`, then Spend over the last 3 days is greater than Rp 1,500,000 AND Purchases over the last 3 days is equal to 0
- **Then:** Pause campaign, and Add to name in Append mode with the text `[NO-SALES {date}]`
- **Why:** Campaign Name is one of the three pre-filter fields, so a naming convention already in use becomes the client boundary without a list of campaign IDs.

### Make the campaign list the weekly report

- **When:** Spend over the last 7 days is greater than Rp 1,000,000
- **Then:** Add to name in Append mode with the text `[ROAS {metric|purchase_roas.omni_purchase|last_7d}]`
- **Why:** A client who opens Ads Manager sees the same 7-day number the agency is reporting, written onto the campaign itself.

AdOps lets an agency run one rule shape across many Meta ad accounts while each client keeps its own numbers. A condition can compare a live Meta metric against a custom metric that AdOps resolves from a Google Sheet at evaluation time, so a client's ROAS target or monthly cap is edited in a spreadsheet rather than inside a dozen separate rules.

## Why does multi-account automation break at the fifth client?

The rules are not the hard part. The thresholds are. Ten clients means ten ROAS targets, ten monthly caps and ten different opinions about when to pause, and every one of them changes at some point in the quarter.

Copying a rule per client works until the day a target moves. Then the change has to be found in the right account, applied to the right rule, and verified, and the account manager who agreed the new number is usually not the person who edits rules.

## How does a custom metric hold each client's target?

A custom metric in AdOps names a Google Sheets spreadsheet id, a sheet name, a lookup column and a value column. At evaluation time the engine resolves the metric to a number, and a condition set to compare against a metric uses that number instead of a hardcoded threshold.

The practical shape is one sheet with a row per client: client key, target ROAS, monthly cap, floor budget. Purchase ROAS over the last 7 days is less than `target_roas` then means something different in every account, and updating a client's target is a cell edit that takes effect on the next evaluation.

Two limits are worth stating. Rows are cached in memory on a 2-minute last-access TTL with the last stored rows in MongoDB as a fallback, so a continuously read sheet can serve a cached snapshot. And the lookup reads the first matching row for each formula line, so the sheet should hold one row per key.

## How do you keep client work separated?

Each rule belongs to an ad account, which is the first boundary. Inside an account, three pre-filters narrow a rule further: Campaign ID with equals or not equals, Campaign Name with contains or does not contain, and Campaign Status with is or is not, over the values ACTIVE and PAUSED.

Campaign Name is the one that scales. If a shared account uses a client or brand prefix, a rule pre-filtered to campaigns whose name contains that prefix picks up new campaigns automatically the day they are created.

Ad accounts also start switched off. Every account discovered from Meta is stored inactive, and nothing is evaluated until someone activates it, which is a useful property when a new client's account is connected mid-week.

## How does the work get distributed?

The dispatcher sweeps for rules whose next execution time has passed, on a 10-second cron with overlap prevention, and enqueues one job per ad account onto a Redis-backed queue. The worker consumes the job, issues a batched Meta Insights read for the metrics the conditions need, evaluates the conditions, and executes the actions.

That split has a property worth knowing when you are handing an agency's whole book to an automation: the dispatcher only ever issues reads to the Meta Graph API. Every budget change, pause and rename happens in the downstream worker.

Entity fetches from Meta are retried up to 3 total attempts, waiting 1 second and then 2 seconds, so a single transient Graph API error does not silently skip an account.

## What does the agency see afterwards?

Every run writes a batch record and one result per campaign per task. The Rule Log Detail screen shows an Executed, Not Executed or Skipped badge per campaign, each condition marked Pass or Fail beside the value it read, the parameters the action used, the execution time in seconds, and the next scheduled execution.

For client-facing reporting, the Add to name action carries live shortcodes such as `{metric|purchase_roas.omni_purchase|last_7d}` and `{date}`, so a campaign name can carry its own 7-day ROAS. A client who opens Ads Manager reads the same number the agency reports.

## What AdOps does not solve for agencies

There is no cross-account rule: a rule belongs to one ad account, and running the same policy across a book means creating it per account. Actions are campaign-shaped, so plan around campaign budgets. AdOps connects to Meta Ads only today, with TikTok Ads, Google Ads and Snapchat Ads listed as coming soon, and billing runs in Rupiah through Duitku.
