Every finance team has a workforce model and a revenue model long before it has a decent opex model. Payroll and bookings are big, visible, and politically interesting. Meanwhile the other 30–40% of the cost base — software, marketing programs, travel, professional services, facilities, cloud — gets planned in a spreadsheet template that goes out to 120 cost center owners in October and comes back in 140 slightly different shapes.
That template round trip is the single most common remaining Excel process in companies that already run Anaplan. It is also one of the easiest wins available, because opex planning is structurally simple: a modest hierarchy, a manageable line item count, and a workflow problem dressed up as a modeling problem.
This tutorial builds an operating expense and cost center planning model in Anaplan that supports both the incremental "last year plus 3%" pattern and genuine zero-based budgeting, in the same structure, without forking the model. It assumes you are comfortable with hierarchies, SUM/LOOKUP, time ranges, and Dynamic Cell Access.
Scope the model before you build it
Opex models fail in two opposite directions. Built too coarse, they produce a number finance cannot defend line by line. Built too fine, they ask a marketing manager to forecast postage by month and nobody fills them in.
Decide these four things in writing first:
- The planning grain. Cost center × account × month is right for most companies. Anything finer — vendor, employee, project — belongs in a driver module that feeds that grain, not in the grain itself.
- The account level. Plan at a rolled-up "planning account" level of perhaps 40–70 members, mapped to the full GL chart of several hundred. Cost center owners do not know your GL.
- Who owns what. One named owner per cost center, one named finance business partner per function. If a cost center has no owner, it has no budget cycle.
- Which costs are driver-based and which are entered. Cloud spend driven by headcount or transaction volume, software driven by license counts, travel driven by headcount and a policy rate, programs entered directly. Roughly half the P&L can be driven; pretending all of it can be is how planners lose faith in the model.
Step 1: Lists and hierarchies
| List | Type | Notes |
|---|---|---|
Cost Centers | Composite hierarchy: Company → Function → Department → Cost Center | Keep the leaf level equal to the GL cost center. Do not invent planning-only leaves. |
Planning Accounts | Composite: Account Category → Planning Account | Mapped to GL accounts via a property, not by name matching. |
Vendors | Flat, production | Used only in the contract driver module. |
Budget Requests | Numbered list | The zero-based decision package grain, built in Step 5. |
Cost Types | Flat: Fixed, Semi-Variable, Discretionary, Allocated | Drives ZBB treatment and challenge reporting. |
Time | Native, monthly | Use a time range for the plan year plus two actual years; do not run the whole model on the full model calendar. |
Two structural decisions worth defending:
Keep allocations out of the entry hierarchy. Owners should budget the costs they control. Allocated costs (IT chargeback, facilities per seat, shared services) belong in a separate module that writes to the same cost center dimension but is read-only to the owner. Mixing controllable and allocated spend in one editable grid produces arguments instead of budgets.
Version the structure, not just the numbers. Reorganizations happen mid-cycle. Hold a Cost Center Mapping module dimensioned by Cost Centers × Time with the parent for each period, so you can report actuals under both the old and new structure. Restating a year of actuals because a department moved is otherwise a week of someone's life.
Step 2: The actuals spine
Before anyone plans anything, load three years of actuals at cost center × GL account × month, and map GL accounts to planning accounts in the hub.
ACT01 Actuals by Planning Account
Actual $ = SUM of GL Actuals mapped by Planning Account
Actual $ LY = LAG(Actual $, 12, 0)
Actual $ L12M = MOVINGSUM(Actual $, -11, 0, SUM)
Run Rate (3M) = MOVINGSUM(Actual $, -2, 0, SUM) / 3
Run Rate (6M) = MOVINGSUM(Actual $, -5, 0, SUM) / 6
Volatility Flag = IF Run Rate (3M) = 0 THEN FALSE
ELSE ABS(Run Rate (3M) - Run Rate (6M)) / Run Rate (3M) > 0.25
Run rates are the quiet workhorse of opex planning. Most cost center owners cannot forecast a number, but every one of them can react to "your last six months annualize to $840K — is next year higher or lower, and why?" Put the 3-month and 6-month run rates directly on the entry grid as reference columns.
Step 3: The seeding engine
Seeding is what makes or breaks adoption. Give each cost center a defensible starting point and let the owner adjust from there.
Build SD01 Seed Method dimensioned by Cost Centers × Planning Accounts, with a Method line item on a list:
| Method | Seed formula |
|---|---|
Zero | 0 — the ZBB default for discretionary accounts |
Prior Year Actual | Actual $ LY |
Run Rate Annualized | Run Rate (6M) * 12, spread by the account's seasonal profile |
Prior Year + Growth % | Actual $ LY * (1 + Growth %) |
Driver-Based | calculated in Step 4 and not editable |
Contract Schedule | from the vendor commitment module |
Then:
SD02 Seeded Budget
Seed Annual $ = SELECT by Method, per the table above
Seasonal Profile = from a profile module by Account Category, sums to 1 across the year
Seed $ (Month) = Seed Annual $ * Seasonal Profile
Set the default Method by Cost Type: Fixed accounts seed on contract schedule, Semi-Variable on driver, Discretionary on Zero. That single mapping is the practical difference between a ZBB cycle and an incremental one — and it means you can run ZBB on the 25% of the cost base where it actually pays, rather than forcing all 100% of it through decision packages and exhausting the organization by November.
Step 4: Driver-based cost modules
Keep every driver in its own module, each writing a single result line item into the planning grain. Resist the temptation to fold drivers into the entry module; you will not be able to explain the numbers six months later.
Headcount-driven costs. Pull ending and average headcount by cost center from the workforce model.
DR01 Headcount Driven Costs
Avg Headcount = from workforce model, by Cost Center and month
Cost per Head = policy rate by Planning Account (laptop amortization,
software seats, travel allowance, training)
New Hire One-Off = Hires in Period * Onboarding Cost per Hire
Driven $ = Avg Headcount * Cost per Head + New Hire One-Off
Vendor contract costs. This module usually pays for the whole project on its own, because most companies do not actually know what they have committed to.
DR02 Vendor Commitments (Vendors x Time)
Contract Start / End = date
Annual Value = number
Billing Frequency = list: Monthly / Quarterly / Annual
Auto-Renew? = boolean
Notice Period (Days) = number
Renewal Uplift % = number
In Term? = Contract Start <= Period End AND Contract End >= Period Start
Committed $ = IF In Term? THEN Annual Value / 12 ELSE 0
Renewal Decision Date = Contract End - Notice Period
Decision Due in Period = boolean flag for the alerts page
Publish a "renewals in the next 120 days" page off Decision Due in Period. Procurement teams reliably discover a six-figure auto-renewal three days after the notice window closed; a model that surfaces it is worth more than the forecast accuracy it delivers.
Volume-driven costs. Cloud, payment processing, shipping, support tooling — driven by transaction or customer volumes from the revenue or demand model, with a unit rate and a tiered discount schedule where relevant.
Step 5: Zero-based decision packages
For the discretionary slice, the budget is not a number in a grid — it is a set of requests that get approved or rejected.
Build Budget Requests as a numbered list with a BR01 Request Detail module:
Cost Center = list-formatted
Planning Account = list-formatted
Title / Rationale = text
Requested Annual $ = number
Start Month = time period
Spread Profile = list: Even / Front-Loaded / Back-Loaded / Custom
Tier = list: Must-Do / Should-Do / Nice-to-Have
Business Case = list: Compliance / Growth / Efficiency / Maintenance
Expected Benefit $ = number
Status = list: Draft / Submitted / Approved / Rejected / Deferred
Decision Note = text, mandatory when Status = Rejected
Approved Annual $ = number, may be less than requested
Then spread approved requests into the planning grain:
BR02 Approved Request Spread
Approved $ (Month) = IF Status = Approved
THEN Approved Annual $ * Spread Factor
ELSE 0
SD03 Total Budget
Budget $ = Seed $ (Month) + SUM of Approved $ over Budget Requests
+ Driven $ + Committed $
Two practical notes. First, force a tier on every request and report the tier mix by function — a function whose requests are 90% "Must-Do" has not done the exercise. Second, Approved Annual $ as a separate field from Requested Annual $ lets finance fund a request partially without a resubmission round, which is how most of these conversations actually end.
Step 6: Targets, challenge, and the gap
Top-down targets and bottom-up submissions have to meet somewhere visible.
TG01 Target vs Submission (Cost Centers x Time)
Target $ = finance entry at Function level, spread to cost centers
Submitted $ = SUM of Budget $ from the leaf grain
Gap $ = Submitted $ - Target $
Gap % = IF Target $ = 0 THEN 0 ELSE Gap $ / Target $
Challenge Required = Gap % > Tolerance %
Challenge Note = text, mandatory when Challenge Required
Give finance business partners a single challenge page: functions down the rows, target, submission, gap, gap %, prior-year actual, and headcount for context, sorted by gap descending. That page replaces the email thread, and it is usually the artifact the CFO ends up living in during the cycle.
Step 7: Workflow and locking
Use a Cycle Control module dimensioned by Cost Centers with a Status line item: Not Started, In Progress, Submitted, In Review, Approved, Locked.
Drive Dynamic Cell Access from it:
Owner Can Edit? = Status = "Not Started" OR Status = "In Progress"
Finance Can Edit? = Status <> "Locked"
Write Access = IF User Role = "Owner" THEN Owner Can Edit?
ELSE IF User Role = "Finance" THEN Finance Can Edit?
ELSE FALSE
Then wire the submission and approval steps through Anaplan Workflow so status transitions, reminders, and the audit trail are handled by the platform rather than by a spreadsheet tracking who has replied. Owner completion percentage by function, on a chart, on the finance homepage, does more for cycle time than any reminder email.
Step 8: Reforecast without rebuilding
The annual budget is used for about six weeks. The model earns its keep on the monthly reforecast, so build for that shape from day one.
FC01 Working Forecast
Actual or Plan? = Period <= Current Close Period
Forecast $ = IF Actual or Plan? THEN Actual $ ELSE Budget $ + Reforecast Delta $
Reforecast Delta $ = owner entry, by exception only
FY Forecast = YTD Actual + Remaining Forecast
Variance to Budget = FY Forecast - FY Budget
Variance Note = text, mandatory where ABS(Variance to Budget) > threshold
The "by exception only" delta is the important pattern. Asking owners to re-enter a full-year forecast every month guarantees stale copies of last month's numbers. Asking them to enter only what changed, against a locked budget baseline, gives you both a clean variance bridge and a reforecast people actually maintain.
Pair it with a bridge module that decomposes variance into volume, rate, timing, and new-scope components. "We are $2.1M over" starts an argument; "$1.4M of it is timing on two programs slipping from Q3 to Q4, $500K is the cloud unit rate, $200K is genuinely new" ends one.
Step 9: The pages
Four pages carry the whole model:
- My Cost Center — the owner's entry grid: planning accounts down, months across, with prior-year actual, run rate, and seed as read-only reference columns; driver-based rows greyed out with a link to the driver page; a submit action.
- Function Challenge — the finance business partner's target-versus-submission view with drill to cost center and to request detail.
- Request Review — the decision package queue, filtered by status and tier, with approve/reject actions and mandatory decision notes.
- Renewals and Commitments — vendor contracts by decision date, with committed spend by year.
Resist building a fifth. Opex models accumulate pages faster than any other model type, and every page you add is a page someone has to be trained on.
Performance and sizing
Opex models are rarely large, but they get bloated in predictable ways:
- Do not dimension entry modules by
Vendors. Vendor detail lives in the commitment module and aggregates in. ACost Centers×Planning Accounts×Vendors×Timemodule is the single most common opex model sizing mistake. - Use a plan-year time range on planning modules rather than the full model calendar. Three unnecessary years of history across every module is pure waste.
- Keep text line items off large grids. Notes and rationale belong on the request and variance modules, at coarse grain, not on the monthly entry grid.
- Summary settings matter more here than anywhere. Most opex line items need
Sumacross cost centers and accounts; rates, percentages, and per-head costs needNoneorFormula, or your Function-level view will show a cost per head of $4.2 million.
What this changes operationally
A finished opex model usually moves three numbers. Budget cycle time drops from eight or ten weeks to three or four, because the template consolidation step disappears entirely. Reforecast effort drops by more, because the exception-only delta pattern means most cost centers touch nothing in most months. And the contract commitment module typically surfaces somewhere between 5% and 15% of software spend that nobody could name an owner for.
None of that requires zero-based budgeting across the whole P&L. It requires the structure to support ZBB where it is worth the effort, a defensible seed everywhere else, and a workflow that makes the cycle visible while it is running rather than after it has finished.
If your opex plan still leaves Anaplan as an export and comes back as an email attachment, this is the highest-return model your team can build next.