Data Model and Scheduling Engine Guide

Table of Contents

  1. Introduction
  2. Data Hierarchy
  3. Parent–Child Relationships & WBS Ordering
  4. Field Rules — What Every Row Must Carry
  5. Rollup Rules & Field Aggregation
  6. Scheduling Engine & Scenarios
  7. Predecessor & Constraint Logic
  8. Where the Full Field List Lives
  9. What Recalculate All Does
  10. Values the Code Depends On
  11. Status and Health Automation

1. Introduction

The platform is a multi-product scheduling application available in three variants — Agilebars (AB), Timebars (TB), and Costbars (CB). All three products share the same underlying data model and a single client-side IndexedDB database. The differences between products are driven by different scheduling engines and configuration, not by separate codebases.

At the heart of the platform is a five-level parent child data hierarchy, where every item — from a top-level portfolio down to an individual resource allocation — is a record in the tbTimebars store with a tbType field that identifies its level:

Level tbType Bar colour Typical purpose What its tbSelfKey2 must point at
L1 Portfolio or Program dark brown Top-level grouping of related projects nothing — a root node has no parent
L2 Project green A discrete deliverable or initiative the parent L1, when a Portfolio exists
L3 Sub-Project or Work-Package orange A work package or component within a project the parent L2
L4 Task, Milestone or Gate blue Individual work items or schedule control points its immediate parent — L3, or L2 when there is no Sub-Project
L5 Allocation gold A resource's assignment to a task (TB/CB only) the parent L4 Task — and it must also carry a tbResID that exists in tbResources

A row may never skip a level or point at a parent that does not exist. An unresolved tbSelfKey2 orphans the row: it disappears from the hierarchy and contributes nothing to any rollup.

Important Note: Agilebars is a Two Tier Hierarchy, Projects and Tasks, it has its own scheduling engine that skips the other levels.

Parent–child relationships are established via the tbSelfKey2 field: every child record stores the tbID of its parent. This single field is what the scheduling engine, rollup functions, and hierarchy rendering all rely on to traverse the tree — upward to aggregate totals and downward to render the timescale and Kanban views.

Work and cost values are entered at the lowest level in use — normally L5 Allocations, but any childless row (a Task with no allocations, or even a Sub-Project, Project, or Portfolio with nothing beneath it) acts as the leaf for its branch and may carry directly entered values — and they are rolled up automatically through the hierarchy. The tbWork, tbAWork, and tbWorkRemaining fields represent forecast hours, actual hours, and remaining hours respectively; tbCost, tbACost, and tbCostRemaining follow the same pattern for costs. A parent with children always holds derived totals: the rollup overwrites any manually set values on it, ensuring consistency from allocation to portfolio.

The three products use that model differently:

Product Hierarchy Scheduling
Agilebars two levels — Project (L2) → Task or Story (L4). L1, L3 and L5 are not used lane-based progress steps on the Kanban board, plus a sprint burndown
Timebars the full five levels on a gantt-style timescale dragging an L5 Allocation recalculates actual start, actual and remaining hours and cost, and percent complete, then rolls the totals up the chain
Costbars the full five levels, same cost and hours model as Timebars, extended with portfolio pipeline features — strategic value scoring and demand vs. capacity analysis

This guide documents how data flows through that hierarchy — from the scheduling engine calculations at L5 up through each rollup stage to the portfolio total — and how the two stores (tbTimebars for core fields, tbMetaData for extended fields) relate to each other across all levels.


2. Data Hierarchy

The tbTimebars store in IndexedDB holds rows that represent different levels of work items. Each row is keyed by tbID (unique ID) and references its parent with tbSelfKey2. The tbType field determines its role in the hierarchy.

Timebars and Costbars

The full hierarchy is:

Portfolio → Project → Sub-Project → Task → Allocation

Example:

  • Portfolio: "Digital Workplace - Transformation"
  • Project: "Synchronisation Client"
  • Sub-Project: "Private Cloud Phase 1"
  • Task: "Consume requirements"
  • Allocations: "Alloc 1" and "Alloc 2"

Each row stores cost, work, dates, durations, and scheduling metadata. Higher-level rows (Sub-Projects, Projects, Portfolios) that have children always hold rolled-up totals — anything entered directly on them is overwritten by the rollup. A higher-level row without children is the leaf of its branch and may hold directly entered cost and work (see Manual Cost, Work & Percent Complete below).

At the task level, the rollup engine checks whether a Task has child Allocations. If Allocations exist, the Task's cost and work totals are summed from them. If no Allocations exist, the Task itself is treated as the leaf node and its own stored values are used directly in the rollup. This means allocations are optional — tasks without them still participate correctly in the hierarchy rollup.

Agilebars

Agilebars uses a simplified two-level hierarchy:

Project → Task (or Story)

L1 (Portfolio/Program), L3 (Sub-Project), and L5 (Allocation) are not supported. Example:

  • Project: "Sprint 4 - Mobile App"
  • Tasks / Stories: "Design login screen", "Write unit tests", "Fix auth bug"

Tasks are always the leaf nodes in Agilebars. Cost and work values are entered directly on each Task, and the Project rolls them up. There is no allocation layer — resource assignment in Agilebars is handled through the Kanban board lanes and sprint membership rather than discrete allocation records.


3. Parent–Child Relationships & WBS Ordering

Every row in tbTimebars links to its parent through a single field: tbSelfKey2, which stores the tbID of the parent row. Root nodes (Portfolios with no parent) have tbSelfKey2 set to null or empty. This one field is what every traversal — rollup aggregation, hierarchy rendering, connector drawing — depends on.

Breadcrumb labels (tbL1 through tbL5) are derived display fields, not structural ones. They are cleared and regenerated each time updateL1ToL5() runs. Each row gets its ancestor names written into these fields by walking up the tbSelfKey2 chain, so that any row can show its full path without a live tree traversal at render time:

Field Contains
tbL1 Portfolio or Program name
tbL2 Project name
tbL3 Sub-Project name
tbL4 Task name
tbL5 Allocation name

WBS ordering (tbHierarchyOrder) is a dot-separated position string, zero-padded to two digits per level, always five segments deep regardless of the actual depth of the row. Examples:

  • Portfolio: 01.00.00.00.00
  • Project: 01.01.00.00.00
  • Sub-Project: 01.01.02.00.00
  • Task: 01.01.02.03.00
  • Allocation: 01.01.02.03.01

This field is used for display ordering and WBS-style report sorting. Like tbL1–tbL5, it is a computed field regenerated on demand by updateTbHierarchyOrder() — it is not a source of truth for structure, only for ordering. This pre-processed data make managing project data in the spreadsheets and the reports easier for the users.


4. Field Rules — What Every Row Must Carry

The rules in this section apply to every route data takes into the product: typed on a form, generated by AI, or synced from the workbook. Data that breaks them does not throw an error — it renders wrongly, or calculates wrongly, or quietly does nothing.

Mandatory fields

Every tbTimebars row, at every level:

Field What it must hold
tbID a unique alphanumeric identifier — the key everything else joins to
tbName the display label shown on the bar, in grids and in reports
tbStart, tbFinish planned dates, DD-MMM-YYYY (for example 15-Jan-2026)
tbType one of the reserved values — Portfolio, Project, Sub-Project, Task, Milestone, Gate, Allocation
tbSelfKey2 the parent's tbID, per the table in section 1. Empty only on a root Portfolio
canvasNo always 1

Every tbMetaData row: tbMDID, which must exactly match its bar's tbID. That single field is the entire join between the two stores — one metadata row per bar, no more and no less.

Allocations (L5) additionally require the fields the scheduling engine calculates from: tbPayRate, tbPercentTimeOn, tbCalendar, tbWork, tbWorkRemaining, tbDuration, tbRemainingDuration and tbPercentComplete. Section 6 shows how they interact.

Field typing

Three rules, and they are absolute.

Numeric fields must be numbers, and default to 0 — never null, never "na", never blank:

tbWork, tbAWork, tbWorkRemaining, tbCost, tbACost, tbCostRemaining, tbBudgetCost, tbDuration, tbRemainingDuration, tbPercentComplete, tbPayRate, tbPercentTimeOn, tbCalendar, tbCoordTop, kbCoordTop

A null or "na" in any of these propagates through every sum it touches: one bad value at allocation level can empty a portfolio total. This is the single most common defect in data arriving from a spreadsheet.

Date fields must be DD-MMM-YYYY strings — 07-Oct-2026:

tbStart, tbFinish, tbAStart, tbAFinish

Text fields hold "" when empty, never null or undefined. Every null and undefined is blanked on the way in.

Where a bar sits on the canvas

tbCoordTop is the vertical pixel position of a bar. On the Timebar View you place bars where you like, and that freedom is what lets far more work fit on one screen than a conventional gantt chart — so the position has to be stored, not derived.

Data created inside the application gets this for free. Data you generate or import must set it, or every bar stacks at the same height:

  • Give the first bar in the hierarchy a starting value — 100 pixels is the default if you have no reason to choose otherwise.
  • Step each following bar down from it. The Bar Creator uses 50 pixels; AI-generated rows use 35, and place an Allocation 40 below its parent Task.

Any consistent step works. The numbers above are what the application itself uses.

Metadata defaults

When a metadata row is created without them, three fields are filled in so the row is never left in an unreportable state:

Field Default
tbMDStatus New
tbMDHealth Not Assessed
tbMDPriority Not Assessed

Risks, Issues and Change Requests are Tasks

There is no tbType of Risk. A risk, an issue and a change request are all Tasks carrying a tbSubType:

To create a Set tbType Set tbSubType
Risk Task Risk
Issue Task Issue
Change Request Task CR

tbType is forced to Task for all three. The tbSubType value is what routes the item to the Risks and Issues views, draws its flag icon on the canvas, and keeps it out of resource calculations — a risk is tracked work, not scheduled effort.


5. Rollup Rules & Field Aggregation

Costs and work flow upward through the hierarchy via the rollup engine (tbschedulingengineRollups.js). It builds an in-memory map of the entire hierarchy keyed by tbID, links children to parents via tbSelfKey2, then walks the tree bottom-up with aggregateCostsAndWork().

The engine applies one rule at every node, regardless of type or level:

  • If a node has children: its cost and work fields are overwritten with the sum of its children's values.
  • If a node has no children (leaf node): its own stored values (originalValues) are written back as-is and passed upward unchanged.

The leaf-node rule is not type-aware. It does not distinguish between an Allocation, a Task, a Sub-Project, or a Project. Any node without children becomes the leaf at that branch and its own stored cost and work values participate in the rollup. This means it is valid to mix approaches within a single portfolio — some projects fully broken down to allocations, others with a cost and work figure entered directly at the project or sub-project level — and the rollup will aggregate all of them correctly into the portfolio total.

The general convention is that cost and work are entered at the Allocation level and that higher-level rows hold only derived totals. But this is a convention, not an enforcement. The engine simply sums whatever exists beneath each node and falls back to the node's own values when nothing is beneath it.

Running the rollup multiple times produces the same result — it is fully idempotent.

Fields that roll up at every level:

Field Meaning
tbWork Forecast hours
tbAWork Actual hours to date
tbWorkRemaining Remaining hours
tbCost Forecast cost
tbACost Actual cost to date
tbCostRemaining Remaining cost

The full bottom-up flow:

Allocation (or any childless node)
  → summed into Task (or nearest parent with children)
    → summed into Sub-Project
      → summed into Project
        → summed into Portfolio

The Portfolio row is always the root node (tbSelfKey2 = null). Its totals equal the sum of every leaf node beneath it, wherever in the tree those leaves sit.

Percent complete is derived, not summed. Percentages cannot be meaningfully added, so tbPercentComplete is excluded from the summed fields. Instead, immediately after a parent's children are summed, its percent complete is derived from the fresh totals using the same rule hierarchy as manual entry: 100 when the parent itself is complete (it has an actual finish date), otherwise from work when the parent has work (tbAWork ÷ tbWork), from cost when it has cost but no work (tbACost ÷ tbCost), and left unchanged when it has neither.

Manual Cost, Work & Percent Complete (rows without Allocations)

When a user chooses not to create Allocations, cost and work are entered by hand on the lowest row in use — typically the L4 Task, but the same applies to a childless L3, L2, or L1. These rows are edited in the Cost & Schedule popup form (launched from the bar's status icon), where the Forecast row's three cost cells, three work cells, and % complete render as editable dotted-border inputs on Timebars and Costbars. Rows with children stay read-only there — their values come from the rollup, and any manual entry on them would be overwritten (children always win).

Every manual entry is auto-reconciled so the seven fields stay mathematically aligned:

You enter The system derives
Work (first entry) Work Remaining = Work, A Work = 0, % Complete = 0
A Work later Work Remaining = Work − A Work, % Complete = A Work ÷ Work × 100
% Complete later (work present) A Work = Work × %, Work Remaining = Work − A Work
Work Remaining directly A Work is kept; Work is re-forecast to A Work + Work Remaining (same convention as the Allocation hours-to-go override)
Cost fields, no work present identical rules on the cost triple; % Complete derived from cost
Cost fields, work present Cost Remaining = Cost − A Cost kept consistent, but % Complete stays work-driven
% Complete with neither work nor cost the typed percent is simply held for reporting

The rule hierarchy for tbPercentComplete is therefore: complete first, then work, then cost, user-entered as last resort. If Allocations are later created beneath the row, the next rollup replaces the manual values with the children's totals — no cleanup needed.

Completion always wins (all bar types). A bar that carries an actual finish date (tbAFinish) is finished — that single field is what makes the Schedule Status read Completed and what the Hide Completed Bars filter looks for. The scheduling engine sets it on any bar type whose finish falls behind the report date, not just Allocations, so tbPercentComplete is forced to 100 on those rows too: Task, Sub-Project, Project and Portfolio as well as Milestone, Gate and Release. Previously only Allocations got the matching 100, which left completed rows sitting at whatever percent they happened to hold — complete by date, part-done by number — and Hide Completed Bars (which needs both the actual finish and 100%) never hid them.

Actual work and actual cost are not rewritten to match. The hours and money a row really consumed stay exactly as entered, so a completed row can legitimately read 100% with A Work below Work. Dates decide whether a row is done; the numbers stay a record of what it took. To show less than 100% on such a row, clear its actual finish date or move the bar out past the report date — percent then falls back to the work/cost rules on the next recalculation.

Because data can also arrive via AI generation and Spreadsheet Sync, these seven fields can drift out of alignment. The Recalculate All button runs a consistency checker before the rollup: on every childless L4–L1 row it treats forecast and actual work (and cost) as the source of truth, corrects the remaining and percent complete fields to match, and assumes actuals of 0 when only a forecast value exists. Allocation (L5) rows are never touched by the checker — they belong to the scheduling engine.

The scheduling engine (tbschedulingengine2.js) is triggered when a user drags or resizes an allocation bar on the timescale. It only recalculates allocation-level fields — tasks, sub-projects, projects, and portfolios are updated afterwards by the rollup engine, not by the scheduling engine directly.


Important — Manual Cost & Work at L1–L4: The scheduling engine only recalculates cost and work fields for Allocation (L5) rows. If cost and work have been entered manually on a Task, Sub-Project, Project, or Portfolio row (i.e. the row has no children and is acting as a leaf node), those values will not be recalculated when the bar is dragged or resized on the timescale. The engine will still update the bar's dates and duration, but tbWork, tbCost, and all related actual and remaining fields will remain unchanged. To have cost and work recalculate automatically on drag, the values must be held at the Allocation level.

The one progress field the engine does write on these rows is tbPercentComplete, and only on completion: when the drag, resize, or report-date change puts the bar entirely behind the report date, the engine stamps the actual finish date and sets percent complete to 100. Work and cost are still left alone.

Manually entered values are maintained through the Cost & Schedule popup form (click the bar's status icon): on a childless L4–L1 row the Forecast row's cost, work, and % complete cells render as editable (dotted-border) inputs, and every entry is auto-reconciled by the manual calculation rules described in the Manual Cost, Work & Percent Complete section below.


6. Scheduling Engine & Scenarios

The scheduling engine runs when a user drags or resizes an Allocation bar on the timescale. It recalculates that allocation's dates, duration, hours, cost and percent complete; everything above it in the hierarchy is then updated by the rollup engine, not by the scheduling engine directly.

Input: Pixels to Days

The entry point is prepForDateHoursCostCalculator(), which converts the pixel offset of the drag (leftDiff for a move, resizeDelta for a resize) into days using the current timescale pixel factor (tsPxFactor, set per the weekly or monthly view). This produces daysdiffS (start shift) and daysdiffF (finish shift), which are passed to dateHoursCostCalculator().

Scenario Detection

The engine reads the allocation's tbStart and tbFinish dates, converts them to JavaScript Date objects, then compares against apStatusDate (the user's status/report date) to determine which of three scenarios applies:

Condition Scenario
dStart < statusDate AND dFinish > statusDate In Progress
dStart < statusDate AND dFinish < statusDate Completed
dStart >= statusDate Not Started

Scenario 1 — In Progress

The allocation has started but not yet finished. The actual start is locked to the bar's start date; actual finish remains empty.

Standard (no override):

tbDuration         = working days (dStart → dFinish)
tbADuration        = working days (dStart → statusDate)
tbRemainingDuration = tbDuration - tbADuration
tbWork             = tbCalendar × (tbPercentTimeOn / 100) × tbDuration
tbAWork            = tbCalendar × (tbPercentTimeOn / 100) × tbADuration
tbWorkRemaining    = tbCalendar × (tbPercentTimeOn / 100) × tbRemainingDuration
tbCost             = tbWork × tbPayRate
tbACost            = tbAWork × tbPayRate
tbCostRemaining    = tbWorkRemaining × tbPayRate
tbPercentComplete  = (tbWork - tbWorkRemaining) / tbWork × 100
tbAStart           = dStart (formatted DD-MMM-YYYY)
tbAFinish          = "" (not yet finished)

Override path (tbSchEngOverride = "Overridden"): The user has manually fixed tbWorkRemaining. The engine respects that stored value instead of recalculating it from duration:

tbAWork            = tbCalendar × (tbPercentTimeOn / 100) × tbADuration
tbWorkRemaining    = stored value (not recalculated)
tbWork             = tbWorkRemaining + tbAWork
tbCost/tbACost/tbCostRemaining recalculated from the above
tbPercentComplete  = tbAWork / tbWork × 100

Scenario 2 — Completed

The entire bar sits before the status date. The allocation is 100% done.

tbAStart           = dStart (formatted DD-MMM-YYYY)
tbAFinish          = dFinish (formatted DD-MMM-YYYY)
tbDuration         = working days (dStart → dFinish)
tbADuration        = tbDuration
tbRemainingDuration = 0
tbWork             = tbCalendar × (tbPercentTimeOn / 100) × tbDuration
tbAWork            = tbWork
tbWorkRemaining    = 0
tbCost             = tbWork × tbPayRate
tbACost            = tbCost
tbCostRemaining    = 0
tbPercentComplete  = 100

Scenario 3 — Not Started

The bar's start date is on or after the status date. No actual progress exists.

tbAStart           = "" (not started)
tbAFinish          = "" (not started)
tbDuration         = working days (dStart → dFinish)
tbRemainingDuration = tbDuration
tbWork             = tbCalendar × (tbPercentTimeOn / 100) × tbDuration
tbAWork            = 0
tbWorkRemaining    = tbWork
tbCost             = tbWork × tbPayRate
tbACost            = 0
tbCostRemaining    = tbCost
tbPercentComplete  = 0

Key Allocation Fields Used by the Engine

Field Role
tbCalendar Work hours per day for this resource
tbPercentTimeOn Percentage of time allocated to this task (0–100)
tbPayRate Hourly cost rate — used to derive all cost fields from work hours
tbSchEngOverride "Overridden" locks tbWorkRemaining against recalculation

After the scenario calculation completes, results are written back to IndexedDB via UpdateProgressHoursCalculator() (called from the drag/resize path in tbschedulingengine.js; the allocation popup form path fills the form via initSchEngForm()), and the rollup engine then propagates the updated totals up the hierarchy chain.


Important — Progress is Calculated Against the Status Date, Not Today's Date: The engine always compares bar positions against apStatusDate — the status or report date set by the user in the admin panel — not against the current calendar date. This is a deliberate project management discipline, not a technical limitation.

In formal project management, schedules are updated on a defined reporting cycle: weekly, fortnightly, or monthly. The status date is the agreed "as of" date for that cycle — the point in time at which the team has collectively assessed progress and recorded actuals. Using today's date instead would mean that the same schedule opened on a Monday shows different percent complete, actual hours, and remaining work than it does on a Friday, even if no update has been made and no real work has been recorded. Reports sent to a steering committee or PMO would not match the numbers the PM sees in the tool, and trend analysis across reporting periods would become unreliable because the reference point keeps drifting.

By anchoring all calculations to a fixed status date, the platform ensures that every report, chart, and rollup total reflects a consistent snapshot: what the project looked like as of that date. The PM advances the status date at the start of each new reporting period, updates the schedule, and the next snapshot is produced cleanly.

Note: The Agilebars Kanban board offers an optional setting (apABuseStatusDate) that allows teams to use today's date instead of the status date, for teams that prefer to update progress continuously rather than on a fixed reporting cycle.


7. Predecessor & Constraint Logic

Predecessor (tbPredecessor)

A row can reference one other row as its predecessor by storing that row's tbID in the tbPredecessor field. This establishes a Finish-to-Start relationship in intent: the successor is expected to start when its predecessor finishes. The field stores the predecessor's tbID as a plain string and defaults to "" when no predecessor exists.

Predecessors are supported on Projects (L2) and Tasks, Milestones, and Gates (L4). Sub-Projects (L3), Allocations (L5), and Portfolios (L1) do not carry a predecessor reference in their rendered bar HTML and will not display connector lines even if a value exists in tbPredecessor on the underlying record.

Constraint Type (tbConstraintType)

The tbConstraintType field controls whether a bar's position is locked against movement caused by a predecessor cascade. The meaningful value is:

Value Effect
"Pinned" The bar is locked. A predecessor cascade will not move it. A pin icon is rendered on the bar.
"" (empty) No constraint. The bar moves freely and participates in predecessor cascades normally.

Pinning is useful when a task has a hard deadline or a fixed contractual date that must not drift when upstream bars are rescheduled.

Connector Rendering

When the timescale draws bars, each bar element receives data-pred and data-tbid HTML attributes. After rendering, tbCanvas.js iterates the bar elements, reads data-pred on each successor, looks up the corresponding predecessor element by id="key{tbID}", and draws a connector line between the two. If the predecessor element is not found in the current view (filtered out or off-screen), the connector is silently skipped.

Important — Simplified Dependency Relationships (Scheduling Light)

Traditional project scheduling tools support four dependency relationship types: Finish-to-Start (FS), Start-to-Start (SS), Finish-to-Finish (FF), and Start-to-Finish (SF), each optionally combined with a positive or negative lag value. This platform intentionally implements a simplified subset of that model.

Only Finish-to-Start relationships are supported, with the option to apply a positive lag (a gap between the predecessor finishing and the successor starting) or a negative lag (which effectively approximates a Start-to-Start with overlap, allowing the successor to begin before its predecessor finishes). Finish-to-Finish and Start-to-Finish relationships are not supported, as Finish-to-Finish is rarely practical in day-to-day scheduling and Start-to-Finish is almost never used in practice.

When a Task is dragged on the timescale, the engine automatically cascades the move to any successor Tasks linked to it via tbPredecessor, recursively cascading through the full chain. A successor Task that has been Pinned will not move regardless of what its predecessor does — the pin takes priority over the cascade. This cascade is limited to Tasks only — dragging a Project bar does not trigger a cascade to linked successor Projects. The predecessor connector line will display on Project bars as a visual reference, but Project-level predecessors must be manually rescheduled.

This approach is intentionally described as Scheduling Light — enough dependency structure to communicate sequence and intent clearly on the timescale and in reports, without requiring teams to manage the complexity of a fully automated predecessor network across all levels of the hierarchy.


8. Where the Full Field List Lives

Every field in the system is a row in the tbSchema store, and you read it in the application rather than here: Report Menu → Other → Schema Values.

For each field it gives you the internal name, its label, what it is for, the picklist it draws from and whether that picklist is a tag list or the live resource pool, its data type, whether it is mandatory, whether it is validated, and which spreadsheet sheet it belongs to. It is filterable by sheet and by picklist, and its purpose column is full-text searchable.

That report is the field reference. A field list printed into a guide would start drifting from the product the day it was written; the report cannot.

The related stores, in one line each:

Store What it holds
tbTimebars every bar — the scheduling backbone
tbMetaData the extended fields for each bar, joined on tbMDID = tbID
tbBaseline a point-in-time snapshot that mirrors the tbTimebars structure exactly — the same fields populate it
tbResources the resource pool, one row per person or generic role. L5 Allocations link to it through tbResID
tbTags every picklist value, grouped by tbTagGroup, each group pointed at a field by tbTagTbInternalName

The picklists and their values are documented in the Forms, Reports and Graphs Guide; the stores and how they move in and out of the product are in the Data Synchronization, Backup, Recovery and Retention Guide.


9. What Recalculate All Does

Recalculate All is the single button that puts every derived value back in step. It matters because data arrives from three directions — typed in the app, generated by AI, synced from a workbook — and only the first of those keeps the derived fields current as it goes.

It runs in this order, and the order is the point: each step feeds the next.

# Step What it fixes
1 Consistency check on every childless L4–L1 row, treats forecast and actual work and cost as the truth and corrects the remaining and percent-complete fields to match. Runs first, so corrected leaf values feed the rollup in the same pass. Allocations are left alone — they belong to the scheduling engine
2 Cost and work rollup sums the six cost and work fields bottom-up in a single pass, and derives each parent's percent complete from its fresh totals
3 Hierarchy names clears and rewrites the tbL1–tbL5 breadcrumb fields by walking tbSelfKey2 up the tree
4 WBS ordering regenerates tbHierarchyOrder, the five-segment sort path
5 Grouping fill-down empty grouping fields inherit the nearest tagged ancestor's value, so reports do not group everything as untagged. Runs after the ordering is fresh and before the rebuild below, so the joined rows carry the filled values
6 Joined table rebuild rebuilds tbMdJoined, the cached join of bars and metadata that every report reads

Run it after any spreadsheet import, any AI generation, and any bulk edit — and before publishing, exporting, or trusting a portfolio total. Running it twice changes nothing the second time; every step is idempotent.

Resource calculations are not part of this. The weekly resource demand figures behind the charts and grids are rebuilt separately, so a resource chart that looks stale after a Recalculate All needs its own refresh rather than another run of this.

10. Values the Code Depends On

This section replaces the old Picklist Values Locked By Code document and the old System-Reserved Metadata Fields and Values section, which said similar things in different words and had both drifted from the product.

144 values across 46 picklists are locked. Twelve picklists are locked end to end. Every locked value is shown in bold in the A–Z reference in the picklist reference in the Forms, Reports and Graphs Guide, so there is one list to read rather than two to reconcile.

The picklists whose meaning is not obvious

Most locked values explain themselves. These do not, and getting them wrong is expensive:

Picklist Field Why the exact text matters
tbType tbType Tells the scheduling engine whether a row is a Project, Task, Milestone or Allocation. Everything downstream branches on it.
SubType tbSubType Turns a Task into a Risk, Issue or CR. The Risks & Issues module, and the health rules in section 7, find their rows by this value.
Timebar Status tbMDStatus Drives the whole PPM pipeline — what counts as a proposal, in-flight work, or closed. Rejected, On Hold and Blocked also make a row immune to the status automation.
Schedule Status tbMDHealth The word behind the schedule colour. Section 6 maps each value to a colour by exact text.
Escalation Level tbMDEscalationLevel Executives and Directors push a project's scope, risk or issue health straight to Red, and drive Enterprise Dashboard notifications.
Budget Estimate tbMDROMEstimate Band names are parsed as money ranges, used as the fallback when Budget Cost is zero in the PPM select/kill process.
Risk Probability, Risk Impact tbMDProbability, tbMDImpact Each value carries a hidden 1–5 weight held in code, not in sort order. Rename one and risks stop scoring — see the warning below.
Labour Type tbResLabourType Human is what makes a resource appear in every people-picker; Generic and Human split the supply-and-demand analysis.
Health Cost/Hours/Issues/Overall/Risk/Schedule/Scope tbMDHealth* Green, Yellow, Red, Not Assessed are matched exactly to select an indicator colour. There is no Grey — grey is how Not Assessed renders.
Executive Commitment tbMDSeniorLevelCommitment Feeds PPM scoring and notification thresholds.
Show In Reports tbMDShowIn Each value is a report filter. A renamed value silently removes rows from that report.

Risk Probability and Risk Impact deserve a specific warning. The 1–5 weights behind them are an explicit map in the code, deliberately not taken from the tag sort order — because Not Assessed shares a sort order with the worst value in both lists, and taking sort order would score an unassessed risk as the worst possible one. The consequence for you: renaming a probability or impact value stops it scoring altogether, and the risk silently reads 0 / Not Assessed. Add values freely; do not rename the five in each list.

Why this list can drift, and what to do about it

The lock list is maintained alongside the code, and the picklist values are maintained in the spreadsheet. Nothing automatically checks one against the other, and they have gone out of step before — the Part-time Full-time rename detached two values the portfolio report still branches on, and left the resource form's picker opening empty for months.

Two things protect you:

  • Config Integrity after every import.
  • The A–Z in the picklist reference in the Forms, Reports and Graphs Guide is generated from the live data, so a value that has been renamed away simply stops appearing in bold rather than sitting in the documentation as a value that no longer exists.


11. Status and Health Automation

The product can set status, stage, phase, schedule status and all seven health indicators for you, across the whole database, from the dates, baselines, budgets and risk register you already hold. This section documents what each button does.

Where: the buttons sit on the Core Form and on the CS status form.

Button Sets On
Update Status Status, PPM Stage, PMI Phase, Schedule Status Projects and Sub-Projects (all four); Tasks, Milestones, Allocations (Status only)
Sch Status Schedule Status Projects, Sub-Projects, Tasks, Milestones, Allocations
…and the Health Schedule colour Projects and Sub-Projects only — it is the only level the colour is reported on
Cost Hours Health Cost, Health Hours Projects and Sub-Projects
RIC Health Scope, Health Risk, Health Issues Projects and Sub-Projects
Overall Portfolio rollup, then Health Overall Portfolios, Projects, Sub-Projects
Run All all five, in the correct order, in one pass everything

Run All before sending reports to management. The buttons can be pressed individually and in any order, but Overall reads what the others wrote, so running them out of order gives an Overall based on stale inputs.

What automation never touches

  • A row whose Status is Rejected, On Hold or Blocked is skipped entirely. Somebody put it there deliberately; nothing automatic moves it out, and skipping the whole row means it can never end up half updated.
  • Actual work and actual cost are never rewritten. The automation reads them.
  • Every indicator is recomputed from current state on every run, so a withdrawn escalation or a corrected date moves a project back down as readily as up. Nothing gets stuck on Red.

Update Status — lifecycle from the actual dates

A row's state is read from its actual dates alone:

The row has State Status Stage Phase Schedule Status
no actual dates Not started New 1 Proposed Initiating Not Assessed
an actual start Started In progress 6 Delivering Executing On schedule
an actual finish Finished Closed 8 Closed Closing Completed

On Projects and Sub-Projects all four fields move together, so the row always reads consistently. On Tasks, Milestones and Allocations only Status is set — stage, phase and schedule status are left alone.

Sch Status — schedule health

Two methods, tried in order:

With a baseline — compares forecast finish against baseline finish. The tolerances scale with the length of the item, because five days late on a three-week task is not the same event as five days late on a two-year programme:

Default
Slip tolerance the greater of 3 days or 5% of baseline duration
Late tolerance the greater of 10 days or 10% of baseline duration
Result When
Early earlier than baseline by more than the slip tolerance
On schedule within the slip tolerance either way
Slipping late, but within the late tolerance
Late later than the late tolerance
Completed the row has an actual finish — this wins over everything

Without a baseline — it can only see what is already overdue against the Report Date, which is a genuinely different question. So it returns Late or Not Assessed and never Early or Slipping. Work still ahead of the Report Date stays Not Assessed rather than being flattered with an "On schedule" there is no evidence for.

The Health Schedule colour is a pure projection of the word, so the two can never disagree: Early and On schedule and Completed are Green, Slipping is Yellow, Late is Red, Not Assessed is grey.

Each run tells you which method it used. If it reports that no baselines were found, every schedule status on that run came from the Report Date alone — useful, but not schedule variance. Set a baseline to get the real answer.

Cost Hours — cost and hours health

Two questions, combined, with the worse of the two winning:

  • Variance — is the forecast bigger than the plan? An estimating question. The plan is the baseline if there is one, otherwise the entered Budget Cost or Budget Hours. Green within 5%, Yellow within 15%, Red beyond — with an absolute floor (5,000 currency units, 40 hours) so a small project's rounding noise does not read as an overrun.
  • CPI — are we burning faster than we are earning? An execution question, meaningful only once something has actually been spent. Green at 0.95 and above, Yellow at 0.85, Red below.

Two safeguards worth knowing, because they are what stop a dashboard lying:

  • With neither a baseline nor a budget the answer is Not Assessed, never Green. A brand-new project reporting green cost health is the most common way a portfolio dashboard misleads.
  • Below 15% complete, a variance-driven Red is reported as Yellow. Early on, a forecast above plan is an estimate revision rather than an overspend — attention, not alarm. A genuinely bad CPI still shows Red at any percent complete, so nothing is hidden.

RIC — scope, risk and issue health

Each of the three indicators counts the Change Requests, Risks or Issues beneath the project. A row stops counting once its Status is Closed or Rejected.

Result When
Not Assessed the register is empty — no items of that kind at all
Green items exist and none are open
Yellow an open item is escalated to the Project Office, or 3 or more remain open
Red an open item is escalated to Executives or Directors, or a risk is marked Escalated

Overall — the summary indicator

First, Portfolios take their six indicators from their Projects — the worst reading among them. Sub-Projects are deliberately excluded, because their costs already roll into the Project totals and their risks are already counted at Project level; reading them again would count the same problem twice. A Portfolio with no Project in progress stays Not Assessed rather than claiming health it has not earned.

Then Health Overall is set from the other six indicators on the same row:

Result When
Red any indicator is Red, or 4 or more are Yellow
Yellow 1 to 3 indicators are Yellow
Green all assessed indicators are Green
Not Assessed no indicator has been assessed

Indicators reading Not Assessed are excluded from the count entirely, so an unassessed dimension neither dilutes nor inflates the result. Zero assessed dimensions gives Not Assessed, never a confident green built on no evidence.

Tuning the thresholds

Every threshold above is a default, and every one can be overridden per install through an apMetadataRules JSON value on the admin panel row. Absent or unparseable, the defaults apply — so a malformed entry degrades to standard behaviour rather than breaking anything.

Key Default Controls
slipDaysMin / slipPct 3 / 0.05 Slipping tolerance
lateDaysMin / latePct 10 / 0.10 Late tolerance
costGreen / costYellow 0.05 / 0.15 cost variance bands
costMateriality 5000 absolute floor below which cost variance is ignored
hoursGreen / hoursYellow 0.05 / 0.15 hours variance bands
hoursMateriality 40 absolute floor, in hours
cpiGreen / cpiYellow 0.95 / 0.85 earned-value bands
evEarlyPct 15 percent complete below which variance cannot force Red
ricYellowCount 3 open items before an unescalated register goes Yellow
overallYellowToRed 4 Yellow count that forces Overall to Red

The dependency you must not break

Every value in the tables above — In progress, On schedule, Executives, Green — is matched by exact text against the picklists. The automation writes the value, and the picker has to be able to offer the same value back to the user afterwards.

Rename one of these values and the automation stops working on that field, silently. They are locked for exactly this reason (section 4). Note the lower-case second word in In progress and On schedule; they are not In Progress or On Schedule.



End of guide.