Skip to content

Monte Carlo

TruePPM’s Monte Carlo simulation converts three-point task estimates into a probability distribution over the project finish date. Rather than a single deterministic finish from CPM, you get P50, P80, and P95 dates alongside the full distribution curve — a complete answer to “what is the chance we finish by date X?”

This capability is rare in open-source P3M tools. The sections below document exactly how it works so you can trust the numbers and explain them to stakeholders.

Step 1 — Add three-point estimates to tasks

Section titled “Step 1 — Add three-point estimates to tasks”

Open any task’s detail drawer and navigate to the Estimates section. Set all three values (working days):

FieldMeaning
Optimistic (O)Duration if everything goes well — no blockers, best-case execution
Most Likely (M)Your honest expected duration under normal conditions
Pessimistic (P)Duration if significant problems occur — realistic tail risk, not fantasy

All three fields must be set for a waterfall task to be sampled from its three-point estimate. A waterfall task with any of the three missing is treated as having zero duration uncertainty: its deterministic duration is used for every simulation run.

Your planned duration is the floor. The three-point estimate is the risk band read upward from the plan, not a replacement for it: every sampled duration is clamped up to the task’s own duration before the network is solved. A task planned at 20 days and estimated at O=1 / M=2 / P=3 contributes 20 days to every run, not 2 — see The plan is the floor for why. If you believe a task will finish faster than planned, lower its duration; an optimistic estimate alone will not pull the forecast in.

Agile (Scrum) tasks are the exception — a task delivered as sprint work draws its uncertainty from team velocity rather than a three-point estimate, so it does not need O/M/P values set. See Agile tasks: velocity-based sampling below.

Focus your effort on critical path tasks. Tasks with float do not drive the finish date; uncertainty in their durations has little effect on the output.

Phases are not simulated — put your estimates on the leaves. A summary (phase) row is a grouping node: its dates are rolled up from the tasks beneath it and it has no duration of its own, so it is excluded from the simulated network exactly as it is excluded from the CPM pass. A three-point estimate on a phase does nothing. Dependencies drawn to or from a phase are honored — they are applied to its leaf tasks — so you do not need to redraw them.

In the app, open the Schedule view and expand the Forecast bar docked at the bottom, then run a simulation from the Monte Carlo row — no API call needed. See Forecast for what the collapsed and expanded states show.

To script it instead, or to integrate against it directly:

POST /api/v1/projects/<project_id>/monte-carlo/
Content-Type: application/json
{ "n_simulations": 1000 }

The n_simulations field is optional; it defaults to the server’s MC_SIMULATION_CAP setting (1,000 on OSS; operators can raise it). The endpoint is synchronous and fast — about 60–100 ms of engine time for a 200-task project at 10,000 runs on a current laptop CPU, more on slower server hardware.

The endpoint also enforces a task cap (MC_TASK_CAP, 5,000 on OSS): a project with more tasks than the cap returns HTTP 402 rather than running an unbounded simulation. The vectorized engine handles a 5,000-task × 1,000-run simulation in a few seconds; operators on constrained hardware can lower the cap, and self-hosted operators can raise or remove it.

{
"project_id": "...",
"runs": 1000,
"p50": "2025-11-14",
"p80": "2025-12-02",
"p95": "2026-01-08",
"status_date": "2025-10-21",
"distribution": ["2025-10-21", "2025-10-22", "..."]
}

The schedule after a Monte Carlo run: the Forecast bar shows P50, P80 and P95 finish dates, the CPM date, and the top driver

FieldMeaning
p5050% of simulated runs finished on or before this date. Closest to the deterministic CPM date.
p8080% of runs finished by this date. The standard commitment date for most project plans.
p9595% of runs finished by this date. Use for contractual deadlines and hard external commitments.
p50_at_day_start / p80_at_day_start / p95_at_day_startWhich edge of its day each percentile is. true means the start of the day: the finish is a milestone that a lag or a floor puts at the start of that day, such as a milestone a one-day lag lands on a Sunday, shown at the start of Monday. false means the end of the day. The start of a Monday and the end of the Friday before it are the same point in the working week. When some runs finish at the end of that Friday and others at the start of that Monday, the percentile is shown on whichever of the two days its share of runs reaches, so p80 is still a date 80% of runs are shown on or before.
cpm_finish / cpm_finish_at_day_startThe deterministic CPM finish the percentiles are compared with, and which edge of its day it is.
delta_vs_cpmSigned calendar days each percentile sits past cpm_finish (p50/p80/p95), measured in working time from the two readings. A percentile at the start of a Monday against a CPM finish at the end of the Friday before it is 0, not 3. A run recorded before TruePPM kept these readings has none: two equal days count as 0, and otherwise each date is read as the end of its day.
status_date(added in 0.4) The data date this run was actually computed against — the project’s explicit status date (set at Project settings → General → Status date, not API-only), or today when unset (see Progress-aware forecasting below). Recorded on every run so a past forecast states which “today” produced it.
distributionFull sorted list of all simulated finish dates. Use this to render a histogram or answer “what is the probability of finishing by date X?”

The most recent result is also available without re-running the simulation:

GET /api/v1/projects/<project_id>/monte-carlo/latest/

A single percentile date answers “when?” once. The more useful planning question is “is my confidence eroding?” — has the P80 finish slipped since you last looked?

As of 0.3 (ADR-0175), TruePPM persists project-level Monte Carlo runs so you can read finish-date drift over time: “my P80 was Aug 14 two weeks ago, now it’s Aug 28.” Each recorded run carries its P50/P80/P95 as of that moment, so the history is a true before/after, not just the latest snapshot.

From 0.4, a run will be recorded only when it is triggered by a Scheduler or above — the members who own the schedule. Everyone from Viewer up will still be able to run the forecast and read the result and the drift history; their runs will simply not add a history row. This keeps the drift timeline a signal about deliberate re-planning rather than a log of every incidental read, and prevents a low-privilege member from flooding the history or skewing run attribution.

The history will be available at:

GET /api/v1/projects/<project_id>/monte-carlo/history/

It will return persisted runs newest-first. Each run carries:

FieldMeaning
taken_atWhen the simulation was run (ISO 8601).
p50 / p80 / p95The percentile finish dates as of that run.
cpm_finishThe deterministic CPM spine at run time, for context.
n_simulationsNumber of runs in that simulation.
task_countCommitted tasks included in that simulation — leaf tasks only, since phases are not simulated.
status_date(added in 0.4) The data date that run was computed against — see Progress-aware forecasting below. null for runs recorded before this field existed.
deltaPer-percentile signed day change versus the immediately previous run (positive = the forecast slipped later). null on the oldest/baseline run.
triggered_by_nameWho ran the simulation — see the visibility note below.

In the UI, the Monte Carlo drawer (and the mobile bottom sheet) will gain a collapsible Forecast history list showing each run with its per-run delta (for example, P80 ▲ +14d), so the drift is legible at a glance.

The latest-result endpoint (GET .../monte-carlo/latest/) will also fall back to this persisted history once the 24-hour cache has expired, so your most recent forecast survives past the cache rather than disappearing.

Forecast history is Community (OSS) edition and single-project scope. Rolling forecast drift up across programs or a portfolio is out of scope here and belongs to the Enterprise edition.

Consistent with the OSS cap philosophy (MC_SIMULATION_CAP, MC_TASK_CAP), OSS will keep the newest 100 runs per project (MC_HISTORY_CAP); a nightly job trims older runs. Operators can set the cap to None (unlimited history). 100 runs is ample to read multi-month drift on an actively re-forecast project.

Forecast history answers “is my confidence eroding?” over time. The other planning question is forward-looking: “if this task slips a week, where does the whole forecast land?” — without actually changing the plan to find out.

A non-mutating what-if endpoint answers exactly that (#993, added in 0.4). Point it at one task, give it a duration change, and it recomputes CPM and Monte Carlo in memory and hands back the perturbed forecast — persisting nothing, so it is safe to call as many times as you like:

GET /api/v1/projects/<project_id>/monte-carlo/whatif/?task_id=<task_id>&duration_delta=5

Supply exactly one of:

ParameterMeaning
duration_deltaSigned day offset applied to the task’s current duration (5 slips it a week later, -2 pulls it in).
new_durationAbsolute day count to set the task’s duration to (>= 0).

An optional n_simulations controls the iteration count (default and cap are the same MC_SIMULATION_CAP as a normal run). The response will carry:

FieldMeaning
currentThe unperturbed forecast — p50/p80/p95 with their *_at_day_start readings, cpm_finish with cpm_finish_at_day_start, and the critical_path (task IDs).
whatifThe same fields recomputed with your perturbation applied.
critical_path_changedtrue when the perturbation moved which tasks are on the critical path.
delta_vs_currentPer-field signed calendar-day shift (p50/p80/p95/cpm_finish); positive = later/worse. All four are measured in working time: a finish shown at the start of a Monday counts as the end of the Friday before it, so a finish whose shown date only hops a weekend is 0 (Scheduler Conventions).
appliedThe resolved perturbation (base_duration_days, duration_delta_days, new_duration_days).
cpm_status_date / mc_status_dateThe resolved data dates fed to the deterministic CPM and Monte Carlo passes respectively — both floor a null project status date at today, so they always agree — see Progress-aware forecasting below. Shared by both current and whatif, since one call resolves each once. This endpoint never persists a run, so these are the only record of which data date produced the answer.

A perturbation target must be a real unit of work: a milestone (zero duration by definition) and a phase (whose span is rolled up from its children) will both be rejected with a 400. Point it at a leaf task instead.

Both forecasts sample with the same fixed RNG seed, so the delta isolates the effect of your change rather than run-to-run noise, and the same query always returns the same answer.

Sprint-delivered tasks. A task set to the SCRUM delivery mode with story points, on a project that has a velocity history, is simulated from that team’s throughput rather than from a duration estimate — so a day offset is translated into the equivalent story points at the team’s mean pace before the simulation runs. The cpm_finish still moves by exactly the days you asked for; the p50/p80/p95 bands move by that much on average, rounded to whole sprints, because that is how a sprint-delivered task’s finish date actually behaves. A one-day slip on a two-week cadence either changes nothing or costs a sprint, and the forecast says so.

This is the endpoint behind the MCP whatif read tool: because it is a pure, side-effect-free GET, an AI client with a read-only token can ask “what happens to the Apollo forecast if design review slips two weeks?” and the CPM/Monte Carlo engine — not the model — computes the answer, server-side, on your own instance.

What-if analysis is Community (OSS) edition and single-project scope. Cross-program what-if belongs to the Enterprise edition.

As of 0.3 the forecast accounts for what is already done rather than re-simulating the project from its original start date every run. As work progresses, the simulation will:

  • Pin completed tasks to their recorded actual finish dates instead of re-rolling their durations — finished work is a fact, not a probability.
  • Sample only the remaining duration of in-progress tasks. A task that is 60% complete contributes the uncertainty of its last 40%, not its whole estimate.
  • Anchor remaining work at a data date. Not-started and remaining work will be scheduled no earlier than the project’s status date, so the forecast never places future work in the past.
  • Floor in-progress work at its recorded actual start. A task that actually began after the data date will be forecast from the day it really started, not from the status date — actuals are truth and are never smoothed back to an earlier slot in the network. This is the same floor the deterministic CPM schedule applies, so the Monte Carlo band can never claim a finish date the Gantt has already ruled out. (This floor landed in 0.4. Up to and including 0.3 the forecast anchors in-progress work at the data date alone, so a project whose work started later than its last status report can read percentiles earlier than its own CPM finish — re-run those forecasts after upgrading.)

A new optional status_date field on the project (GET/PATCH /api/v1/projects/<id>/) will set that anchor — in the app, under Project settings → General → Status date (data date), not API-only. When it is left null the forecast defaults to today, so an actively-tracked project reads correctly with no extra configuration; a PM who wants a reproducible, frozen forecast for a report can pin an explicit date. The same progress signals will flow through the deterministic CPM schedule, so the Gantt bars and the Monte Carlo band stay consistent.

Whichever date actually anchors a given simulation — the project’s explicit status_date, or today when it is unset — is recorded as status_date on that run’s response and on its history row, so a forecast is always traceable to the data date that produced it. Re-running an otherwise-unchanged project on two different days will show different percentiles with different recorded status_date values, rather than looking like an unexplained change.

Each task’s duration is sampled from a PERT-Beta distribution, a standard technique for converting three-point estimates into a probability distribution. TruePPM’s convention is a Beta fitted by method of moments to the classic PERT mean and σ = (P − O) / 6 (below) — not the λ=4 Beta-PERT that @RISK and Primavera Risk Analysis default to. Both have the same mean. In practice: on a symmetric estimate the band here is slightly narrower (about 12% smaller σ) than @RISK’s default, so P80 and P95 land a little earlier; on an estimate whose most-likely value sits at the optimistic end it is wider (#4133). Changing the convention would move every stored forecast, so it is not a setting. PERT is preferred over a triangular distribution because it gives more weight to the most-likely estimate, producing more realistic samples for human-estimated tasks.

The PERT mean and standard deviation are:

μ = (O + 4·M + P) / 6
σ = (P − O) / 6

The 4× weight on M is what makes PERT more conservative than a triangular distribution: the most-likely estimate pulls the mean strongly toward the center.

Note that σ is one-sixth of the full range. A task with O = 3, M = 10, P = 17 (a 14-day spread) has σ ≈ 2.3 days. This is by design — PERT encodes the assumption that extreme outcomes are genuinely unlikely.

The PERT distribution is a Beta distribution scaled to the interval [O, P]. Parameters α and β are derived by method-of-moments from the PERT mean and variance:

μ_norm = (μ − O) / (P − O) # normalize mean to [0, 1]
var_norm = σ² / (P − O)² # normalize variance
κ = μ_norm · (1 − μ_norm) / var_norm − 1
α = μ_norm · κ
β = (1 − μ_norm) · κ

Samples are drawn from Beta(α, β) and then scaled back to [O, P]:

duration_sample = O + Beta(α, β) · (P − O)

For the symmetric example O = 3, M = 10, P = 17, this produces Beta(4, 4) — a unimodal distribution centered at 10 days whose standard deviation in the scaled domain is exactly (P − O) / 6 = 2.33 days. The PERT approximation is exact for symmetric inputs. The λ=4 Beta-PERT would give Beta(3, 3) here — the same mean with a wider spread (σ ≈ 2.65 days).

Every sampled duration — from the PERT-Beta draw above or from the velocity path below — is clamped up to the task’s own duration before the run is solved:

duration_sample = max(duration_sample, task.duration)

duration is what the deterministic CPM pass lays out, and the estimate is a separate field a separate person often fills in. Without the clamp the two passes answer the same question from different inputs, and a task whose estimates simply sit below its planned duration produces a forecast earlier than the earliest date CPM considers feasible. A risk tool that reports less risk than the plan it is stressing fails silently and plausibly — nothing about the output looks wrong.

Two consequences worth stating plainly:

  • The optimistic tail below duration is not expressed. A plan deliberately padded above its own estimates reports no upside. Model finishing ahead of plan by lowering duration, not by lowering the optimistic estimate.
  • Ordering the estimate is not a substitute. A triple that brackets its duration — O=1 / M=1 / P=20 against a 20-day task — still piles the PERT mass onto the most-likely value and lands P95 well before the CPM finish. Only the comparison against duration itself binds the two passes.

The simulation still expresses risk in the direction that matters: any sample above duration passes through untouched, so P80 and P95 continue to carry the full tail of the estimate.

A task delivered as Scrum work — delivery_mode = scrum with committed story_points — has no meaningful three-point duration estimate; its uncertainty comes from how much the team completes each sprint. For these tasks the simulation samples sprints-to-completion from the team’s velocity distribution instead of a PERT curve:

  1. The completed-points totals from the team’s last eight closed sprints (excluding any sprint flagged exclude from velocity) form the velocity sample set.
  2. Each run bootstraps that set with replacement, accumulating points sprint by sprint until the task’s story_points are burned down.
  3. The number of sprints that took, multiplied by the team’s typical sprint length (converted to working days), is the task’s sampled duration for that run.

A faster team (high-throughput draws) finishes in fewer sprints; the slow tail needs more — so the spread reflects real velocity variability, the dominant source of schedule risk on agile work. This path takes precedence over a three-point estimate: a Scrum task that also carries O/M/P values still samples from velocity, because the delivery mode is an explicit declaration that uncertainty comes from throughput, not a duration guess.

Velocity samples take the same floor as PERT samples: a team burning its points down faster than the task’s planned duration contributes that planned duration, not the shorter one. On a Scrum task duration is usually left at its default, so the clamp is normally a no-op — it binds only where someone has planned a longer duration than the team’s throughput implies.

A project with no usable velocity signal — no closed, velocity-eligible sprint with recorded completed points — falls back to each task’s deterministic duration, exactly as a waterfall task with no estimate does. So an agile project with no sprint history yet still simulates (to a single deterministic date) rather than failing.

Degenerate cases are handled explicitly:

  • If P − O < 1e-9 (zero spread), all samples equal M.
  • If the computed α or β is ≤ 0 (numerically degenerate input), all samples equal M.

The simulation pre-computes an (n_runs × n_tasks) duration matrix — all sampled durations for all runs — using vectorized NumPy operations. It then evaluates the CPM forward pass n_runs times in parallel, respecting all four dependency types (FS, FF, SS, SF) and lead/lag offsets. Working-day calendars are applied when converting numeric offsets back to finish dates.

Project finish for each run is max(early_finish) across all tasks. The full set of simulated finish dates is sorted to produce the percentile output.

At 10,000 runs on a 200-task project, the full simulation takes about 60–100 ms on a current laptop CPU (measured on Apple Silicon with numpy 2.4: 63 ms for a chain, 88 ms for a network mixing all four dependency types with lag). Expect it to take longer on a shared or older server; the scheduler’s test suite holds it under a loose 2 s regression bound (#3859).

A fixed seed reproduces the same percentiles for the same input on the same numpy and trueppm-scheduler versions. numpy guarantees its random streams only within one release, so an upgrade of either can move a seeded forecast (#4099).

Why the Central Limit Theorem compresses your spread

Section titled “Why the Central Limit Theorem compresses your spread”

When the critical path has many tasks, the project finish date is the sum of many sampled durations. By the Central Limit Theorem, the standard deviation of that sum grows as √n · σ_task, but the coefficient of variation (spread relative to the mean) shrinks as σ_task / (√n · μ_task). With ten tasks on the critical path each having σ = 2 days, the project-level σ is only √10 · 2 ≈ 6.3 days — much less than the 20 days you might naively expect from adding up individual ranges.

This is not a flaw. It reflects the real statistical property of summed uncertainty: diversification reduces relative risk. The implication is that P80/P95 divergence from P50 grows slowly with critical path length and is driven primarily by how genuinely pessimistic your P estimates are.

The risk register does not feed this forecast

Section titled “The risk register does not feed this forecast”

The risk register and this simulation are separate systems that do not exchange information. A risk’s severity is the product of two 1–5 ordinals (probability × impact); this forecast is computed from task three-point estimates and, for sprint-delivered work, team velocity (see The math above). Scoring a risk, changing its severity, linking it to a task, or resolving it touches neither input, so the P50/P80/P95 band never reflects the register — a high-severity risk sitting on a task with weeks of total float moves the finish date not at all, and completing mitigation work on the critical path does not pull the forecast in either.

Mitigation: widen the pessimistic value of the three-point estimate on the task(s) a risk affects. This is a genuine workaround with genuine costs: the padding is unattributed (nothing records which risk it was for, so it is never removed when the risk closes) and uncorrelated (one risk touching five tasks becomes five independent spreads instead of one shared draw, which understates the tail rather than overstating it).

This is a tracked, open gap, not an oversight — see Known Issues and #3660. Making risks a first-class simulation input is sequenced for 0.5, per ADR-0711.

At 1,000 runs (OSS default), the P95 estimate is based on the top 50 samples. The binomial standard error on a P95 estimate at this sample size is roughly ±1.4 percentage points, meaning your “P95” line could realistically represent anywhere from P93 to P97. P80 is more stable (200 samples in the tail), and P50 is very stable (500 samples on each side).

For planning conversations where the tail matters, a higher run cap (MC_SIMULATION_CAP is operator-configurable) produces more stable P80 and P95 estimates.

EditionMax runsP50 stabilityP80 stabilityP95 stability
Community (OSS)1,000GoodAcceptableNoisy (±1.4 pp)

Each task’s duration is sampled independently of every other task. In practice, risks are often correlated: a vendor delay, a key person out sick, or a weather event affects multiple tasks simultaneously. The simulation cannot model these shared risk factors, and independent sampling therefore underestimates tail risk in projects with strong correlated uncertainty.

Mitigation: If you know that several critical-path tasks share a common risk driver, widen their pessimistic estimates to reflect the scenario where that driver fires across all of them. This is a manual proxy for correlation, not a true joint distribution, but it moves the tail in the right direction.

The CPM forward pass assumes unlimited resources: two tasks that are both earliest-feasible on the same calendar day are treated as fully parallel. If your project has a bottlenecked resource — a single specialist assigned to sequential critical-path tasks — the simulation will produce dates that are optimistic relative to the resource-leveled schedule.

Mitigation: Ensure your duration and three-point estimates reflect realistic resource availability, not theoretical parallel execution. If you have a resource-leveled CPM baseline, use its task durations as your M values.

A common mistake is setting P = M × 1.2 (20% contingency). PERT with σ = (P − O) / 6 means a P that is only slightly above M produces a very narrow distribution. The P estimate should represent a realistic bad scenario — a dependency that slips, an approval that takes twice as long, a technical problem that requires a rework cycle — not just a scheduling buffer.

A well-calibrated three-point estimate typically has P at 2–4× the O value, not 1.2× M.

LimitOSS default
Max runs per request (MC_SIMULATION_CAP)1,000
Max tasks per project (MC_TASK_CAP)5,000
Max run history per project (MC_HISTORY_CAP, added in 0.3)100

Both settings can be changed in settings/base.py (or a local override). Self-hosted OSS operators may set any integer or None in their local settings — the cap is advisory, not license-enforced.

Exceeding either cap returns HTTP 402 with a structured error body:

{
"error": "simulation_cap_exceeded",
"tier": "team",
"message": "Simulation count exceeds the community-edition cap."
}

Use P50 as your baseline. P50 is close to the deterministic CPM finish date. The gap between deterministic CPM and P50 is small and usually reflects the slight asymmetry in PERT distributions (a long right tail from pessimistic scenarios pulling the mean above the mode).

Use P80 as your commitment date. An 80th-percentile date is the standard for internal and stakeholder commitments in project management practice. It means you have a 4-in-5 chance of finishing on or before that date given your estimates.

Use P95 for hard external commitments. Contractual deadlines, public launch dates, and regulatory submissions warrant a 95th-percentile buffer. At OSS run counts the P95 value is noisy; raise MC_SIMULATION_CAP and run more simulations for meaningful precision.

Do not commit to P50. A P50 date has a 50% probability of being missed by definition. Committing to it is equivalent to flipping a coin on every project.

Velocity and throughput forecasts are a floor

Section titled “Velocity and throughput forecasts are a floor”

The two backlog-delivery forecasts — the velocity Monte Carlo on a sprint board and the weekly-throughput Monte Carlo on a continuous-flow board — bound how far each simulated run is allowed to go. A run that has not cleared the backlog by that horizon is counted as finishing at the horizon rather than sampled any further, which cuts off the slowest outcomes.

The consequence is one-directional: the upper percentiles on those two forecasts are a floor, not an unbiased P80/P95. The true figure is the date shown or later, never earlier, and the gap widens the more the team’s sprint-to-sprint velocity (or week-to-week throughput) varies. The product says so next to every affected number — look for the “a floor, not a percentile” qualifier and its help icon on the backlog forecast, the sprint release-horizon chips, and the board’s flow analytics card.

This does not affect the schedule forecast built from three-point (PERT) task estimates, which is unclamped. For a date you must commit to externally, estimate that work with PERT durations rather than reading it off a velocity forecast.

The estimator fix is tracked as #2469, planned for 0.6.

Read the distribution, not just the percentiles. The distribution field contains every simulated finish date. A bimodal distribution — two clusters of simulated dates — usually signals that one or two tasks have extreme P estimates that dominate the tail. Investigate those tasks; they are your primary risk drivers.

The Monte Carlo finish-date distribution histogram, with the P50, P80 and P95 dates marked as vertical lines

Added time is the number of calendar days the P80 commitment date sits beyond the computed (CPM) finish, measured in working time. A finish at the start of a Monday and one at the end of the Friday before it are the same point in the working week, so a P80 that differs from the computed finish only by that hop over a weekend adds no time. The computed finish is what your plan says if nothing varies; P80 is the date 4 in 5 simulations finished by. The difference between them is the time schedule uncertainty adds on top of the plan.

Every Monte Carlo delta — delta_vs_cpm, the run-to-run deltas in the history and the what-if shifts — is measured the same way, from each finish’s *_at_day_start reading. A run recorded before TruePPM kept those readings is read as the end of its day. For how the rest of TruePPM measures finish shifts, see Scheduler Conventions (#4204).

It appears on the project Overview, alongside both dates it spans, and in the project health chip in the top bar on every other project view — Schedule, Board, Table and the rest — so it is one glance away wherever you are working. On Overview the chip stays quiet, because the full card is already on that screen.

The chip shows added time in whichever form is readable where you are standing. On Schedule it is a bare +11d, because the dashed CPM chip in the forecast row already puts the computed finish on screen beside it. Everywhere else it names its own baseline — +11d vs Oct 24 — because a signed number with no visible reference is a figure you cannot check. If the top bar is too narrow to hold the qualified form, the chip drops it rather than showing the bare number: open the chip and the full read is in the popover, which never drops it.

On a phone the same value rides the Monte Carlo card at the foot of the schedule, and tapping through opens the full added-time card at the top of the forecast sheet.

The Monte Carlo forecast detail panel: the finish-date histogram, added time vs the computed finish at P50/P80/P95, the sensitivity list of what's holding the date, and confidence by date

Every one of those surfaces reads the same server-computed value, delivered on both the project overview API (risk_premium_days and friends) and the Monte Carlo forecast payload. None of them decides for itself what the number means — which is what the next section is about.

It is a gap between two forecasts, not a prediction of lateness. A project with 11 days of added time is not forecast to run 11 days late — it is forecast to need 11 days of buffer beyond the plan to reach 80% confidence.

Compare projects by ratio, not by days. risk_premium_ratio expresses the premium as a share of the duration remaining between today and the computed finish. Eleven days on a six-week project and eleven days on a three-year program are very different findings, and only the ratio says so.

A blank is not a zero. Added time reports one of three things, and the difference matters:

What you seeWhat it means
A number (+11d, −4d)Measured. Estimates existed and the simulation produced a spread.
No added timeMeasured. Estimates exist and the variance is genuinely low.
Can’t be measured yetNot measured. No task could contribute duration variance, so the simulation had nothing to work with.

The third case is the one to watch. A project with no three-point estimates simulates flat, which makes its premium exactly zero days — numerically identical to a well-estimated project with low variance. TruePPM will not print that zero, because doing so would tell the least-understood project in your portfolio that it carries no schedule risk. Add estimates to the tasks you are unsure about and re-run the forecast.

Check the as-of date. Added time is only as current as the run it came from. A forecast older than a week is marked stale and its date is shown beside the commitment, so an old number cannot be read as a current one.

Is this forecast still about my current plan?

Section titled “Is this forecast still about my current plan?”

Age is only half the question. A forecast can be minutes old and already out of date, because the plan moved after it ran.

TruePPM answers this on the server and reports it as forecast_staleness on every forecast payload, so the UI, the API and an MCP client all get the same answer:

ValueMeaning
currentThe run matches the plan the project is on now, and it is recent.
project_changedThe project has been written to, or its schedule recomputed, since the run.
agedNothing has changed, but the run is over a week old, so the data date has moved under it.
unknownThe run predates this field and cannot be placed against the plan.

On the Schedule view’s forecast bar, anything other than current offers the Rerun button. The bar also says Edited since this run for project_changed — and deliberately does not say your plan changed, because the signal behind it is “something in this project was written”, which includes a logged time entry or a renamed label. Treat it as a reason to rerun, not as evidence that a date moved.

unknown is the one value that offers Rerun without any accompanying claim: it means TruePPM does not know, and it will not present that as reassurance. It resolves itself the next time the forecast is run.

Because the judgment lives on the server rather than in your browser session, it survives a page reload and reflects a collaborator’s edits as readily as your own — the answer is about the data, not about how long the page has been open.

There is deliberately no severity band on added time yet — no “low / moderate / high” verdict. Threshold cut points chosen without evidence would be a judgment dressed as a measurement. The band arrives once TruePPM can calibrate it against measured actual-vs-estimate history.

When P50, P80, and P95 are identical, the simulation found no uncertainty to model — every run finished on the same day. This is correct when no committed task can vary the finish, but the cause is not always “missing estimates”. The result’s forecast_diagnostic field reports the reason, and the schedule view shows it in place of the distribution:

  • Estimates awaiting approval — in Suggest & Approve estimation mode, three-point estimates do not feed the forecast until a Scheduler approves them (estimate_status = accepted). The estimates are visible on the tasks, but the forecast treats them as not-yet-trusted. Approve them to fold their range in. estimate_status is read-only on the task API: POST /api/v1/tasks/{id}/approve-estimates/ is the only way to reach accepted over the API, and it requires the Resource Manager role or above. A task’s assignee cannot approve their own estimate by writing the field on a task update. The one exception is imported data — MS Project and seed imports write accepted directly, because the values are PM-authored migration data rather than contributor suggestions. Importing into an existing project requires Project Manager; the import-as-new-project and seed-import endpoints need only a signed-in user, but they can only ever create a new project or program that the importer owns, never modify an existing task.

  • Who may write an estimate at all — this is set by the project’s estimation mode, and it is enforced on the server, not just in the browser. In Open (the default) any Team Member or above may write three-point estimates directly. In Suggest & Approve they may write, but the value lands pending and is withheld from the forecast until approved, as described above. In PM Only only the Project Manager role and above may write them — a Team Member who can otherwise edit the task receives 403 on any request carrying optimistic_duration, most_likely_duration or pessimistic_duration, on create and update alike. The task API reports the caller’s own authority as can_edit_estimates, which is what the estimate inputs gate on.

  • No estimate ranges — tasks carry only a single duration (or a degenerate range where optimistic = pessimistic). Add genuine optimistic/most-likely/ pessimistic estimates to the tasks you are unsure about.

  • Agile work with no velocity history — story-point (Scrum) tasks sample from the team’s completed-sprint velocity rather than a duration range. Until at least one sprint has closed there is no distribution to draw from. Close a sprint and re-run.

  • Estimated work off the critical path — your estimates vary, but the longest path runs through fixed-duration work, so the variance never reaches the finish. Estimate the tasks that actually drive the date (see What’s holding the date).

  • Estimates below the planned duration — the range is real, but all of it sits at or below the task’s own duration, which every sample is floored at. A 20-day task estimated at 1 / 2 / 3 days samples to a constant 20. Adding more estimates cannot open a range here: either lower the planned durations to match what you expect, or raise the pessimistic values above them.

  • All work complete, or nothing committed — finished tasks have no remaining work to vary, and backlog cards are excluded from the forecast entirely.