Zero-Based Budgeting — Semantic Model
1. Overview
A budgeting platform implementing Peter Pyhrr’s Zero-Based Budgeting (ZBB) methodology: every cost-centre owner rebuilds their budget from zero each cycle by submitting decision packages (discrete activities or expenditures), each with multiple funding levels (minimum, current, enhanced) and a granular cost breakdown. Packages are ranked within their cost centre, reviewed through an explicit approval workflow, and the chosen funding level becomes the funded amount. The model captures the planning artefacts (cycles, packages, levels, costs) and the governance artefacts (rankings, approval actions, role assignments) but does not model actuals, variance analysis, or downstream GL postings — those live in upstream/downstream finance systems.
2. Entity summary
| # | Table name | Singular label | Purpose |
|---|---|---|---|
| 1 | budget_cycles | Budget Cycle | The planning period (e.g. FY26) over which budgets are rebuilt from zero |
| 2 | cost_centers | Cost Center | Org unit responsible for justifying its own budget — the ZBB “decision unit” |
| 3 | decision_packages | Decision Package | A discrete activity, service, or expenditure being justified — the atomic unit of ZBB |
| 4 | funding_levels | Funding Level | A service-level option for a package (minimum / current / enhanced) with its own cost and benefit |
| 5 | cost_line_items | Cost Line Item | Granular cost row inside a funding level (e.g. salaries, software, travel) |
| 6 | cost_categories | Cost Category | Taxonomy of cost types (Salary, Contractor, Software, Travel, Capex, …) |
| 7 | gl_accounts | GL Account | Chart-of-accounts entry linking cost lines to the general ledger |
| 8 | cost_drivers | Cost Driver | Quantitative driver reusable across line items (FTE count, transaction volume, square footage) |
| 9 | package_rankings | Package Ranking | Priority ordering of packages within a cost centre, scoped to a cycle |
| 10 | approval_actions | Approval Action | Audit-trail entry for a review decision on a package |
| 11 | users | User | A person who owns, reviews, or approves packages |
| 12 | cost_center_assignments | Cost Center Assignment | Junction: which user holds which role on which cost centre |
Entity-relationship diagram
flowchart LR
budget_cycles -->|scopes| decision_packages
budget_cycles -->|scopes| package_rankings
cost_centers -->|hierarchy| cost_centers
cost_centers -->|owns| decision_packages
cost_centers -->|has| cost_center_assignments
cost_centers -->|has| package_rankings
users -->|owns| cost_centers
users -->|owns| decision_packages
users -->|performs| approval_actions
users -->|assigned via| cost_center_assignments
decision_packages -->|has| funding_levels
decision_packages ---|selects| funding_levels
decision_packages -->|has| approval_actions
decision_packages -->|ranked in| package_rankings
funding_levels -->|breaks down| cost_line_items
funding_levels -->|actioned in| approval_actions
cost_categories -->|categorises| cost_line_items
cost_categories -->|hierarchy| cost_categories
gl_accounts -->|maps| cost_line_items
gl_accounts -->|hierarchy| gl_accounts
cost_drivers -->|drives| cost_line_items
3. Entities
3.1 budget_cycles — Budget Cycle
Plural label: Budget Cycles
Label column: cycle_name
Audit log: yes
Description: A planning period (typically annual) during which cost centres rebuild their budgets from zero. The cycle bounds all packages, rankings, and approvals.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
cycle_name | string | yes | Cycle Name | (label) e.g. “FY26 ZBB Cycle” |
fiscal_year | integer | yes | Fiscal Year | e.g. 2026 |
start_date | date | yes | Start Date | |
end_date | date | yes | End Date | |
cycle_status | enum | yes | Status | values: draft, planning, in_review, locked, archived |
description | text | no | Description |
Relationships
- A
budget_cyclemay scope manydecision_packages(1:N, viadecision_packages.budget_cycle_id). - A
budget_cyclemay scope manypackage_rankings(1:N, viapackage_rankings.budget_cycle_id).
3.2 cost_centers — Cost Center
Plural label: Cost Centers
Label column: cost_center_name
Audit log: yes
Description: An organisational unit (department, function, team) accountable for justifying its own budget. In ZBB language this is the “decision unit”. Hierarchical via parent_cost_center_id.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
cost_center_code | string | yes | Code | unique, e.g. “CC-1001” |
cost_center_name | string | yes | Name | (label) |
parent_cost_center_id | reference | no | Parent Cost Center | → cost_centers (N:1, self-ref hierarchy, clear on delete) |
owner_user_id | reference | no | Primary Owner | → users (N:1, clear on delete) |
gl_segment | string | no | GL Segment | optional ERP linkage |
is_active | boolean | yes | Active | default true |
Relationships
- A
cost_centermay have a parentcost_center(N:1, self-referential). - A
cost_centermay have a primary owneruser(N:1). - A
cost_centerowns manydecision_packages(1:N, parent, restrict on delete — historical packages are preserved). - A
cost_centerhas manycost_center_assignments(1:N, parent, cascade on delete). - A
cost_centerhas manypackage_rankings(1:N, parent, cascade on delete).
3.3 decision_packages — Decision Package
Plural label: Decision Packages
Label column: package_title
Audit log: yes
Description: The atomic unit of ZBB — a discrete activity, service, or expenditure being justified from zero. Every package is owned by a cost centre, scoped to a cycle, broken into 2+ funding levels, and moves through an approval workflow.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
package_code | string | yes | Code | unique, e.g. “PKG-FY26-001” |
package_title | string | yes | Title | (label) |
cost_center_id | parent | yes | Cost Center | ↳ cost_centers (N:1, restrict on delete) |
budget_cycle_id | reference | yes | Budget Cycle | → budget_cycles (N:1, restrict on delete) |
package_type | enum | yes | Package Type | values: continuing, new, discretionary, mandatory |
priority_tier | enum | no | Priority Tier | values: must_have, should_have, nice_to_have |
package_status | enum | yes | Status | values: draft, submitted, in_review, approved, rejected, cut, deferred |
business_justification | html | yes | Business Justification | the “why” narrative — core ZBB artefact |
consequences_of_not_funding | html | no | Consequences if Not Funded | what breaks if killed |
alternatives_considered | html | no | Alternatives Considered | |
selected_funding_level_id | reference | no | Selected Funding Level | → funding_levels (N:1, clear on delete) — set after approval |
owner_user_id | reference | yes | Package Owner | → users (N:1, restrict on delete) |
submitted_at | date-time | no | Submitted At | |
approved_at | date-time | no | Approved At |
Relationships
- A
decision_packagebelongs to onecost_center(N:1, parent, restrict on delete). - A
decision_packageis scoped to onebudget_cycle(N:1, required). - A
decision_packageis owned by oneuser(N:1, required). - A
decision_packagehas manyfunding_levels(1:N, parent, cascade on delete). - A
decision_packagemay select one of itsfunding_levelsas the funded option (N:1, viaselected_funding_level_id, clear on delete). Circular reference with the parent edge above —selected_funding_level_id.decision_package_idmust equalthis.id. - A
decision_packagehas manyapproval_actions(1:N, parent, restrict on delete to preserve audit trail). - A
decision_packagemay appear in manypackage_rankings(1:N).
3.4 funding_levels — Funding Level
Plural label: Funding Levels
Label column: funding_level_label
Audit log: yes
Description: A service-level option for a decision package (typically minimum / current / enhanced). Each level has its own cost stack and benefit narrative; the package owner recommends one and an approver selects one.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
funding_level_label | string | yes | Level Label | (label) e.g. “Minimum”, “Current”, “Enhanced +2 FTE” |
decision_package_id | parent | yes | Decision Package | ↳ decision_packages (N:1, cascade on delete) |
level_tier | enum | yes | Tier | values: minimum, current, enhanced, custom |
level_order | integer | yes | Order | 1 = lowest, ascending |
is_recommended_level | boolean | yes | Recommended by Owner | default false; the owner picks one before submission |
headcount_fte | float | no | Headcount (FTE) | total FTE at this level |
currency_code | string | yes | Currency | ISO 4217, e.g. “USD” |
service_description | html | yes | Service Description | what’s delivered at this level |
benefit_narrative | html | no | Incremental Benefit | benefit vs the next-lower level |
risk_narrative | html | no | Risk if Chosen |
Relationships
- A
funding_levelbelongs to onedecision_package(N:1, parent, cascade on delete). - A
funding_levelhas manycost_line_items(1:N, parent, cascade on delete). - A
funding_levelmay be referenced by manyapproval_actions(1:N, viaapproval_actions.funding_level_id, clear on delete). - A
funding_levelmay be the selected level on its parentdecision_package(1:0..1, viadecision_packages.selected_funding_level_id).
3.5 cost_line_items — Cost Line Item
Plural label: Cost Line Items
Label column: line_item_label
Audit log: yes
Description: A granular cost row inside a funding level — e.g. “Senior Engineer salaries (×2)”, “Datadog enterprise licence”. Supports either driver-based input (quantity × unit_cost) or lump-sum entry; total_cost_amount is always the canonical roll-up figure.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
line_item_label | string | yes | Description | (label) |
funding_level_id | parent | yes | Funding Level | ↳ funding_levels (N:1, cascade on delete) |
cost_category_id | reference | yes | Cost Category | → cost_categories (N:1, restrict on delete) |
gl_account_id | reference | no | GL Account | → gl_accounts (N:1, clear on delete) |
cost_driver_id | reference | no | Cost Driver | → cost_drivers (N:1, clear on delete) |
quantity | float | no | Quantity | optional driver-based input, e.g. 2 (FTE) |
unit_cost_amount | float | no | Unit Cost | optional, paired with quantity |
total_cost_amount | float | yes | Total Cost | canonical figure used in roll-ups |
currency_code | string | yes | Currency | ISO 4217 |
cost_period | enum | yes | Period | values: one_time, recurring_annual |
notes | text | no | Notes |
Relationships
- A
cost_line_itembelongs to onefunding_level(N:1, parent, cascade on delete). - A
cost_line_itembelongs to onecost_category(N:1, required, restrict on delete). - A
cost_line_itemmay map to onegl_account(N:1, optional). - A
cost_line_itemmay be driven by onecost_driver(N:1, optional).
3.6 cost_categories — Cost Category
Plural label: Cost Categories
Label column: category_name
Audit log: no
Description: Taxonomy of cost types used to classify line items (Salary, Contractor, Software, Travel, Capex, etc.). Hierarchical via parent_category_id so categories can roll up.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
category_code | string | yes | Code | unique, e.g. “SALARY” |
category_name | string | yes | Name | (label) |
category_type | enum | yes | Type | values: opex, capex, mixed |
parent_category_id | reference | no | Parent Category | → cost_categories (N:1, self-ref, clear on delete) |
Relationships
- A
cost_categorymay have a parentcost_category(N:1, self-referential). - A
cost_categorymay classify manycost_line_items(1:N).
3.7 gl_accounts — GL Account
Plural label: GL Accounts
Label column: account_name
Audit log: no
Description: Chart-of-accounts entry that links ZBB cost line items back to the general ledger. Hierarchical (parent/child accounts).
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
account_code | string | yes | Code | unique, e.g. “5100” |
account_name | string | yes | Name | (label) e.g. “Salaries Expense” |
account_type | enum | yes | Type | values: asset, liability, equity, revenue, expense, contra |
parent_account_id | reference | no | Parent Account | → gl_accounts (N:1, self-ref, clear on delete) |
Relationships
- A
gl_accountmay have a parentgl_account(N:1, self-referential). - A
gl_accountmay be mapped to by manycost_line_items(1:N).
3.8 cost_drivers — Cost Driver
Plural label: Cost Drivers
Label column: driver_name
Audit log: no
Description: A reusable quantitative driver of cost — e.g. headcount, transaction volume, square footage. Cost line items can reference a driver to make the cost-build transparent and easy to flex.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
driver_code | string | yes | Code | unique, e.g. “FTE_COUNT” |
driver_name | string | yes | Name | (label) e.g. “Full-Time Equivalents” |
unit_of_measure | string | yes | Unit | e.g. “headcount”, “transactions/month” |
current_value | float | no | Current Value | most recent quantity |
description | text | no | Description |
Relationships
- A
cost_drivermay drive manycost_line_items(1:N).
3.9 package_rankings — Package Ranking
Plural label: Package Rankings
Label column: ranking_label
Audit log: yes
Description: A prioritisation entry — within a cost centre and cycle, this row says “package X is ranked at position Y”. Used by the cost-centre owner during the ZBB ranking ceremony and by finance during roll-up reviews.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
ranking_label | string | yes | Ranking | (label) caller composes on insert, e.g. “FY26 / CC-1001 / #3 K8s Migration” |
cost_center_id | parent | yes | Cost Center | ↳ cost_centers (N:1, cascade on delete) — the scope of this ranking |
budget_cycle_id | reference | yes | Budget Cycle | → budget_cycles (N:1, restrict on delete) |
decision_package_id | reference | yes | Decision Package | → decision_packages (N:1, cascade on delete) |
rank_position | integer | yes | Rank | 1 = highest priority |
rationale | text | no | Rationale |
Composite uniqueness expected on
(cost_center_id, budget_cycle_id, rank_position)and(cost_center_id, budget_cycle_id, decision_package_id). Implementation enforces.
Relationships
- A
package_rankingbelongs to onecost_center(N:1, parent, cascade on delete). - A
package_rankingis scoped to onebudget_cycle(N:1, required). - A
package_rankingranks onedecision_package(N:1, required).
3.10 approval_actions — Approval Action
Plural label: Approval Actions
Label column: action_label
Audit log: yes
Description: A single review event on a decision package — submission, approval, rejection, cut to a lower funding level, deferral. The full sequence of approval_actions for a package is the audit trail of how the package moved through governance.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
action_label | string | yes | Action | (label) caller composes on insert, e.g. “Approve · Jane Doe · 2026-04-15” |
decision_package_id | parent | yes | Decision Package | ↳ decision_packages (N:1, restrict on delete to preserve audit trail) |
funding_level_id | reference | no | Funding Level | → funding_levels (N:1, clear on delete) — level approved or cut to |
actor_user_id | reference | yes | Actor | → users (N:1, restrict on delete) |
action_type | enum | yes | Action Type | values: submit, approve, reject, cut, defer, request_changes, withdraw |
comment | text | no | Comment | |
acted_at | date-time | yes | Acted At |
Relationships
- An
approval_actionbelongs to onedecision_package(N:1, parent, restrict on delete). - An
approval_actionmay reference onefunding_level(N:1). - An
approval_actionis performed by oneuser(N:1, required).
3.11 users — User
Plural label: Users
Label column: display_name
Audit log: no
Description: A person who owns, reviews, or approves decision packages. The table_name: users matches the Semantius built-in exactly so the deployer can deduplicate against the platform user table.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
user_email | email | yes | unique | |
display_name | string | yes | Display Name | (label) e.g. “Jane Doe” |
is_active | boolean | yes | Active | default true |
department | string | no | Department | |
job_title | string | no | Job Title |
Relationships
- A
usermay own manycost_centers(1:N, viacost_centers.owner_user_id). - A
usermay own manydecision_packages(1:N, viadecision_packages.owner_user_id). - A
usermay perform manyapproval_actions(1:N, viaapproval_actions.actor_user_id). - A
usermay have manycost_center_assignments(1:N).
3.12 cost_center_assignments — Cost Center Assignment
Plural label: Cost Center Assignments
Label column: assignment_label
Audit log: no
Description: Junction entity that captures which user holds which ZBB role on which cost centre — owner, reviewer, approver, or controller. Drives package routing and review permissions during the cycle.
Fields
| Field name | Format | Required | Label | Reference / Notes |
|---|---|---|---|---|
assignment_label | string | yes | Assignment | (label) caller composes on insert, e.g. “Jane Doe · Owner · CC-1001” |
cost_center_id | parent | yes | Cost Center | ↳ cost_centers (N:1, cascade on delete) |
user_id | reference | yes | User | → users (N:1, cascade on delete) |
assignment_role | enum | yes | Role | values: owner, reviewer, approver, controller |
is_primary | boolean | no | Primary | one primary per (cost_center, role) by convention |
valid_from | date | no | Valid From | |
valid_to | date | no | Valid To |
Relationships
- A
cost_center_assignmentbelongs to onecost_center(N:1, parent, cascade on delete). - A
cost_center_assignmentreferences oneuser(N:1, cascade on delete). cost_centers↔usersis many-to-many through this junction (with role).
4. Relationship summary
| From | Field | To | Cardinality | Kind | Delete behavior |
|---|---|---|---|---|---|
cost_centers | parent_cost_center_id | cost_centers | N:1 | reference | clear |
cost_centers | owner_user_id | users | N:1 | reference | clear |
decision_packages | cost_center_id | cost_centers | N:1 | parent | restrict |
decision_packages | budget_cycle_id | budget_cycles | N:1 | reference | restrict |
decision_packages | selected_funding_level_id | funding_levels | N:1 | reference | clear |
decision_packages | owner_user_id | users | N:1 | reference | restrict |
funding_levels | decision_package_id | decision_packages | N:1 | parent | cascade |
cost_line_items | funding_level_id | funding_levels | N:1 | parent | cascade |
cost_line_items | cost_category_id | cost_categories | N:1 | reference | restrict |
cost_line_items | gl_account_id | gl_accounts | N:1 | reference | clear |
cost_line_items | cost_driver_id | cost_drivers | N:1 | reference | clear |
cost_categories | parent_category_id | cost_categories | N:1 | reference | clear |
gl_accounts | parent_account_id | gl_accounts | N:1 | reference | clear |
package_rankings | cost_center_id | cost_centers | N:1 | parent | cascade |
package_rankings | budget_cycle_id | budget_cycles | N:1 | reference | restrict |
package_rankings | decision_package_id | decision_packages | N:1 | reference | cascade |
approval_actions | decision_package_id | decision_packages | N:1 | parent | restrict |
approval_actions | funding_level_id | funding_levels | N:1 | reference | clear |
approval_actions | actor_user_id | users | N:1 | reference | restrict |
cost_center_assignments | cost_center_id | cost_centers | N:1 | parent | cascade |
cost_center_assignments | user_id | users | N:1 | reference | cascade |
cost_centers ↔ users is many-to-many through cost_center_assignments (with assignment_role).
5. Enumerations
5.1 budget_cycles.cycle_status
draftplanningin_reviewlockedarchived
5.2 decision_packages.package_type
continuingnewdiscretionarymandatory
5.3 decision_packages.priority_tier
must_haveshould_havenice_to_have
5.4 decision_packages.package_status
draftsubmittedin_reviewapprovedrejectedcutdeferred
5.5 funding_levels.level_tier
minimumcurrentenhancedcustom
5.6 cost_line_items.cost_period
one_timerecurring_annual
5.7 cost_categories.category_type
opexcapexmixed
5.8 gl_accounts.account_type
assetliabilityequityrevenueexpensecontra
5.9 approval_actions.action_type
submitapproverejectcutdeferrequest_changeswithdraw
5.10 cost_center_assignments.assignment_role
ownerreviewerapprovercontroller
6. Open questions
6.1 🔴 Decisions needed (blockers)
None.
6.2 🟡 Future considerations (deferred scope)
- Should ZBB scopes that span multiple cost centres (cross-functional initiatives, shared services) be supported via a
cost_center_groupsentity, or is the current single-cost_center_idlink ondecision_packagessufficient? - Should
currency_codebe promoted to its owncurrenciesentity with FX rates, to support multi-currency budget consolidation? Currently a free-text ISO 4217 string onfunding_levelsandcost_line_items. - Should justification supporting evidence (spreadsheets, vendor quotes, slide decks) be modelled via an
attachmentsentity, or kept in an external document store? - Should prior-period actuals be loaded into the model for variance reporting, or always pulled from upstream finance systems at query time? (ZBB de-emphasises prior periods, but reviewers often want the comparison.)
- Should funding-level cost roll-ups be stored as denormalised snapshots on
funding_levels(for performance) or always derived fromcost_line_itemsat query time? - Should rankings be expressible at multiple scopes (cost centre → function → corporate), e.g. via a
ranking_scopeenum and an optional roll-up parent ID, or stay scoped to cost centres only with corporate roll-up handled in the reporting layer? - Should
cost_center_assignmentsenforce a single concurrent assignment per (user, cost_center, role) viavalid_from/valid_to, or permit overlapping assignments? Currently the date fields are optional.
7. Implementation notes for the downstream agent
A short checklist for the agent who will materialise this model in Semantius (or equivalent):
- Create one module named
zero_based_budgeting(the module name must equal thesystem_slugfrom the front-matter — do not invent a different slug here) and two baseline permissions (zero_based_budgeting:read,zero_based_budgeting:manage) before any entity. - Create entities in the order given in §2 — entities referenced by others first. The circular reference between
decision_packages.selected_funding_level_idandfunding_levels.decision_package_idrequires a two-pass approach: create both entities, then adddecision_packages.selected_funding_level_idafterfunding_levelsexists. - For each entity: set
label_columnto the snake_case field marked as label in §3, passmodule_id,view_permission,edit_permission. Do not manually createid,created_at,updated_at, or the auto-label field. - For each field in §3: pass
table_name,field_name,format,title(the Label column), and forreference/parentfields alsoreference_tableand areference_delete_modeconsistent with §4. (The §3Requiredcolumn is analyst intent; the platform manages nullability internally and does not need a per-field flag.) - Fix up each entity’s auto-created label-column field title.
create_entityauto-creates a field whosefield_nameequals the entity’slabel_column, and itstitledefaults tosingular_label. Every entity in this model has a label_column whose §3 Label differs fromsingular_label(e.g. entityvendorswould yield title “Vendor” but we want “Vendor Name”). After eachcreate_entitycall, follow up withupdate_fieldto set the correct title. Theupdate_fieldidis the composite string"{table_name}.{field_name}"(e.g."cost_centers.cost_center_name","decision_packages.package_title","funding_levels.funding_level_label") — pass it as a string, not an integer, or the update will fail. The full list of fixups:budget_cycles.cycle_name→ “Cycle Name”cost_centers.cost_center_name→ “Name”decision_packages.package_title→ “Title”funding_levels.funding_level_label→ “Level Label”cost_line_items.line_item_label→ “Description”cost_categories.category_name→ “Name”gl_accounts.account_name→ “Name”cost_drivers.driver_name→ “Name”package_rankings.ranking_label→ “Ranking”approval_actions.action_label→ “Action”users.display_name→ “Display Name”cost_center_assignments.assignment_label→ “Assignment”
- Deduplicate against Semantius built-in tables. This model is self-contained and declares
users, which exists in Semantius as a built-in. For each declared entity, read Semantius first: if a built-in already covers it, skip the create and reuse the built-in as thereference_tabletarget — do not attempt to recreate. Optionally add the model’s required fields (display_name,is_active,department,job_title) to the built-in only if they are missing (additive, low-risk changes only). - Junction-table label population. Three entities have label fields the caller must populate on insert because they have no natural single-field label:
package_rankings.ranking_label,approval_actions.action_label,cost_center_assignments.assignment_label. The implementing application or workflow should compose these from the related records (e.g."{cycle_name} / {cost_center_code} / #{rank_position} {package_title}"for a ranking). - After creation, spot-check that
label_columnon each entity resolves to a real field, that allreference_tabletargets exist, and that thedecision_packages↔funding_levelscircular reference resolves cleanly in both directions.