Why probabilistic risk?
A deterministic schedule says "this project finishes on 12 Dec 2027." A probabilistic schedule says "this project has an 80% chance of finishing by 28 Jan 2028." The difference matters more than it looks — and is the difference between defensible forecasting and optimistic guessing.
Deterministic vs Probabilistic
Project teams traditionally produce a single forecast — one finish date, one budget number, one performance figure. This is deterministic. It feels confident because it's a single answer, but it conceals the variability that every project actually contains.
Probabilistic risk analysis runs the project model thousands of times under different "what-if" combinations of risk events firing, durations varying, and costs fluctuating. The output is a distribution: P50 (50% confidence), P80 (80% confidence), P90, and so on. Each is a meaningful answer to a different question.
"12 Dec 2027"
(false confidence)
distribution
P80: 28 Jan
P90: 14 Feb
The Monte Carlo idea
Monte Carlo simulation is brutally simple. For each of (say) 10,000 iterations:
- For every risk in the register, roll a die against its probability to decide if it fires this iteration.
- If it fires, sample a value from its three-point estimate using the chosen distribution shape (BetaPert by default).
- Sum the sampled values across all firing risks to get this iteration's total schedule delta, cost delta, and performance loss.
- Store the totals. Repeat 9,999 more times.
You end up with three arrays of 10,000 numbers each. Sort them, take the value at the 80th percentile — that's your P80. Take the value at the 50th — that's P50. The histogram of these arrays is the probability distribution of project outcomes.
Each bar grows from the bottom as iterations land in that bucket. Right-skewed shape with a long tail — typical for project risk. P50 (median) is the centre of mass; P80/P90 sit in the right tail because that is where the painful outcomes accumulate.
This 5-step cycle runs 10,000 times per simulation. Each pass produces one observation. The cycle completes in milliseconds — the whole 10k-iteration run finishes in 1-3 seconds depending on schedule complexity.
QSRA, QCRA, QPRA — three dimensions
Risk Intelligence runs all three simulations in parallel, each iteration produces three totals:
Why all three matter together
Project performance is not just "did we finish on time" or "did we spend the budget." Performance KPIs capture quality, safety, technical objectives, resource adequacy — the dimensions a client actually cares about. A project that finished early but with 40% performance loss is a failure that schedule-only analysis would miss.
Risk Intelligence's KPI framework lets you define per-project quality, safety, deliverable acceptance, and similar dimensions and run them through the same Monte Carlo machine.
The remaining-only philosophy
This is the single most important conceptual point in the platform, and it differs from how some traditional Monte Carlo tools work. The engine simulates from the data date forward, using the remaining work on every activity. Original (target) durations are not used by the engine.
This matters because risks fundamentally model future uncertainty. A risk event firing in iteration 4,247 can't go back in time and add days to work already completed. Treating the activity as if it has 90 days of "at-risk" duration over-states the exposure and produces phantom risk against work that's already in the bank.
What changes practically
- P80 schedule numbers will typically be lower than legacy tools that use original duration, on schedules with significant in-progress portions.
- For not-started activities, the philosophy makes no difference (remaining = original).
- For completed activities (remaining = 0), any risk mistakenly linked contributes nothing.
The Workbench tour
The platform's workbench is organised into 11 functional tabs plus three sidebars (Activities, Issue Modeling, Help). Each tab is a workflow stage. Knowing where to look first saves real time on real projects.
The 11 tabs at a glance
scd_end_date) and BOQ. Reference for interpreting all P-value tiles below.| Tab | What it's for | When to use |
|---|---|---|
| Overview | Headline P50/P80/P90 + recommended actions | First stop after a run; client-facing summary |
| Schedule | Activity table with CPM, float, critical path | Anchor risks to activities; verify import |
| Risks | Full risk register with edit dialogs | Add/edit/review every risk individually |
| Uncertainty | Aleatory uncertainty — per-activity duration bands | Capture estimating uncertainty separate from discrete risk events |
| QSRA / QCRA / QPRA | Dimension-specific Monte Carlo dashboards | Deep dive on schedule / cost / performance |
| Confidence | Per-activity P50/P80 finish dates | Negotiating contract milestones with the client |
| Sensitivity | Tornado / driver analysis | "Which risks drive the tail" — Spearman correlations |
| Insights | Generated narrative findings | Talking points; report copy |
| Report | Multi-section deliverable for export | PDF / Word / Excel publication |
The right sidebar: Analysis Controls
The right edge of the workbench has a collapsible sidebar with the simulation controls. Click the small ‹ tab to collapse it for full-width viewing of dashboards. Key controls:
- Run Simulation — fires the Monte Carlo engine. Auto-run can be toggled in Settings → General.
- Scenario — Pre-mitigation, Post-mitigation, or Uncertainty-only.
- Focus dimension — Schedule / Cost / Performance — drives which P-value is the headline KPI.
- P-value slider — 50 to 95. Default is 80 (industry-standard confidence target).
- Iterations — 1,000 to 100,000. Default 5,000. Convergence banner advises if more are needed.
- Sampling method — Monte Carlo (random) or Latin Hypercube (stratified, fewer iterations needed).
- Distribution — BetaPert (default), Triangular, Uniform, Normal, LogNormal, Beta, Trigen, Discrete, Bitriangular, Two-point, Point.
- Mitigation effectiveness — slider for the Post scenario, scales risk impacts.
Roles & permissions
The platform uses Firebase custom claims for role-based access. Roles are assigned by your administrator and govern what you can do:
Loading data — file formats
Drop any combination onto the workspace:
- Primavera P6 XER — full schedule with TASK, TASKPRED, TASKRSRC, RSRC, UMEASURE tables. Activities, relationships, resource assignments, units-of-measure all populate.
- Excel risk register — .xlsx / .xls. Multi-sheet workbook with the platform's 20-column Schedule sheet (see Module 12 for round-trip).
- CSV / TSV — flat risk register. Schema auto-detection picks up ID, Description, Probability, three-point impacts, Category, Owner, etc.
- JSON — full project archive (re-import an exported project).
- PDF / Word / PowerPoint — unstructured documents. Parser extracts candidate risks for review; you confirm before they enter the register.
Risk Register basics
The risk register is the heart of the platform. Each row is a discrete risk event with a probability of firing and a three-point impact estimate. Understanding the data model makes every downstream feature easier to use.
Anatomy of a risk record
| R-014 | Unexpected ground conditions in earthworks | HIGH | P × I = 0.40 × 35 = 14.0 |
Every risk record carries these fields:
- ID — unique identifier (R-001, R-002... or user-defined)
- Description — clear, specific risk statement (avoid generic "schedule risk" — describe the actual event)
- Category — risk category for grouping (Design, Procurement, Construction, Commissioning, External)
- Owner — person accountable for managing this risk
- Probability — 0–100% chance of firing during the project
- Three-point impacts — Min / Most Likely / Max for each dimension (Schedule days, Cost currency, Performance % loss)
- Per-KPI estimates — if you've defined custom KPIs, three-point per KPI overrides the legacy single "performance %"
- Activity links — risks linked to one or more activities anchor the impact spatially in the schedule
- Response strategy — Avoid / Reduce / Transfer / Accept
- Source citation — where this risk came from (workshop, SME interview, lessons learned)
- Apply-mode override — per-risk Add/Replace setting (see Module 05)
Probability × Impact
Two independent quantities. Probability is "how likely is this event to occur at all during the project life." Impact is "if it does occur, how bad is it." The product P × I gives you the expected value — useful for sorting but never a substitute for the full Monte Carlo run.
| Risk | Probability | Impact (ML) | P×I | Why ranking by P×I is wrong |
|---|---|---|---|---|
| A | 50% | 20d | 10 | Stable, predictable, fires routinely |
| B | 5% | 200d | 10 | Same P×I but the tail risk is catastrophic — drives P95 even though P50 doesn't see it |
Risk categories
Categories enable grouping in tornado charts, sensitivity analysis, and reports. Default categories on a new project:
Add custom categories via Settings → Categories. The tornado view supports a "by category" tornado-mode to roll up sensitivity to the category level.
Risk vs Uncertainty — two registers
The platform distinguishes two types of variability:
Example: "Permit delay" — might happen (40% chance), might not. If it does, 5-15-30 day impact.
Each iteration includes a die-roll against probability.
Example: "Earthworks duration estimate is 90d ± 20%". Always varies, never "doesn't happen".
Captures inherent estimating uncertainty separate from discrete events.
Use both. A complete model has a risk register for things-that-might-happen and an uncertainty register for things-that-have-uncertain-magnitude-regardless. The QSRA / QCRA / QPRA tabs let you scenario-toggle between Pre (risks active), Post (mitigated risks), or Uncertainty-only (just the bands, no risk events).
Activity linkage & context
Linking a risk to one or more activities anchors the impact in time and space. The CPM forward-pass uses the linkage to propagate risk effects through the precedence network. The activity context block makes your estimates calibrated.
Why link risks to activities
An unlinked risk is a project-level overlay — sampled days are added to the bottom-line schedule, not routed through any specific activity. This is fine for high-level risks ("strategic alignment shift") but loses precision for activity-specific risks.
A linked risk is routed through the CPM network. If the risk fires in a given iteration:
- The sampled time impact extends the linked activity's working duration in that iteration.
- The CPM forward-pass recomputes early start and early finish dates downstream.
- If the activity is on the critical path, the entire project shifts. If not, the activity may consume float silently — until it doesn't, and the path becomes critical.
- The PRA convention: each linked activity receives the full sampled impact. Multiple links = multiple parallel effects.
sampled = 20d
+= 20d
extended
sampled = 20d
(project overlay)
extended
The activity picker in the risk dialog
Type-ahead search resolves on code or name. Click a result to add it as a chip. Click the × on a chip to unlink. Multiple chips = multiple linked activities (the risk's impact applies to each).
The chip's tooltip shows the activity's start/finish dates and duration. For in-progress activities, both original and remaining are displayed in the chip text.
The activity context block
Once you link one or more activities, the dialog renders a context panel directly below the chip area. This is the platform's calibration aid — it shows you the actual schedule footprint so your three-point estimates are grounded in reality.
A-4010 Earthworks and foundationsThe panel auto-aggregates if multiple activities are linked (heading becomes "Linked activities (N) — aggregated"). Resources shown are filtered to those ticked as performance-relevant in Settings → Performance → Resources.
Inline baseline strips above three-point inputs
Each impact section (Schedule, Cost, Performance) now carries a thin strip directly above the Min/ML/Max inputs. This is the operative reference for your estimate.
The screenshot below shows every section of the risk dialog when a risk is linked to one activity. Note the apply-mode dropdown at the top, the activity context block in the middle, and the differently-styled baseline strips above each impact section (accent for schedule + cost, muted N/A for performance).
A-4010 Earthworks and foundationsMulti-activity linkage and aggregation
When a risk is linked to multiple activities, both the engine and the dialog handle aggregation:
- In the engine — each linked activity independently receives the full sampled impact (PRA convention). If the sampled value is 20d and there are 3 links, each activity's working duration gains 20d in that iteration. The CPM forward-pass then determines which activity drives the project finish.
- In the dialog baseline display — durations and costs are summed across linked activities. The strip shows the totals (e.g. "Total remaining: 187d across 3 activities").
- Replace mode with multi-link — each linked activity's working duration is set independently to the sampled value, so each gets
sample - this_activity's_remainingas its delta. Multiple replace-mode risks on overlapping activities accumulate deltas (additive engine consequence — slightly more pessimistic than pure replace; usually conservative).
Apply-mode: Add vs Replace
The single most consequential simulation control after distribution choice. Add mode treats the sampled value as a delta on top of remaining work. Replace mode treats it as the new remaining work entirely. Choose deliberately — it changes the engine math.
The two modes — concept
ADD mode — sample is the extra work on top of remaining
REPLACE mode — sample IS the new remaining work; risk sampled at 70d
The visual point: add mode lengthens the activity by the sampled amount. Replace mode resets the activity's remaining duration to the sampled amount, regardless of what was there before.
The completed 27d portion never changes — it is burned and immutable. Add mode extends the remaining segment; Replace mode swaps it out entirely for the sampled value.
Worked example — Add mode
Worked example — Replace mode
The per-risk override + the global setting
Apply-mode is configurable at two levels:
When to use which
- Default Add globally — most projects, most risks. Standard Monte Carlo PRA convention.
- Per-risk Replace when an individual risk fundamentally re-bases the activity, not just adds to it. Examples: "Permit forces a complete redesign approach" (the activity is now a different activity), "Site access change makes the work much faster" (opportunity risk).
- Global Replace — rare, but useful for sensitivity testing ("if I treat all my estimates as the new totals, what's the P80?").
Settings → Advanced panel showing the global apply-mode dropdown introduced in Phase 13.
Scope: schedule and cost only
The apply-mode dropdown has no effect on the Performance dimension. Performance is structurally different:
- Performance is measured as percentage loss against 100% nominal, not as an absolute number.
- The sampled value IS already a delta (% reduction), not a candidate "new total".
- There's no XER-derived "remaining performance baseline" the way duration and cost have. Performance baselines are KPI definitions, not schedule data.
- Add and Replace would produce identical results for performance —
sample - 0 = sample.
The dialog's performance baseline indicator carries an APPLY-MODE N/A badge to make this explicit. The explainer says: "Multiple risks compound via the global performance mode (mult / add / max) — the per-risk Add/Replace toggle has no effect on performance."
The remaining-only philosophy
The engine operates on remaining work, never on original target durations. This is a fundamental design choice that affects how every result is interpreted. Take this module seriously — it's the difference between defensible numbers and inflated ones.
The core principle
Risks model future uncertainty. They can only affect work that has not yet happened. Work already completed is in the past, immutable, not subject to perturbation.
Why this matters — concrete comparison
Consider the same in-progress activity (90d original, 63d remaining), same risk (sample = 80d), with two different engine designs:
| Scenario | Old (original-based engine) | New (remaining-based engine) |
|---|---|---|
| Add mode, sample 20d | workingDur = 90 + 20 = 110d Over-states: counts the 27d already done |
workingDur = 63 + 20 = 83d Correct: only 83d of work still needs to happen |
| Replace mode, sample 80d | workingDur = 80d (set directly) Credits back the 27d burned! Activity duration < what's already been done. |
workingDur = 63 + (80 - 63) = 80d worth of remaining work + 27 already done = 107d total elapsed Correct: 27d burned + 80d new remaining = 107d total elapsed |
The replace-mode case is where the old approach went badly wrong. A pure "set the activity to 80d" treatment doesn't make physical sense for an in-progress activity — you can't un-burn the 27d already in the bank. The remaining-based engine handles this correctly by treating sample as the new remaining, then letting the engine derive the right delta.
Engine internals — how it's implemented
Inside simulate(), the platform precomputes two structures from App.activities:
// Precomputed once per simulation run (outside iteration loop) const _remainDurByIdx = new Float64Array(network.acts.length); for (let i = 0; i < network.acts.length; i++) { const a = network.acts[i]; // Trust explicit 0 for completed activities; fall back only when missing _remainDurByIdx[i] = (a.remainDrtnDays != null) ? a.remainDrtnDays : (network.baseDuration[i] || 0); } // Per-risk baseline for replace-mode delta const riskBaselines = risks.map(risk => { let baseSched = 0, baseCost = 0; String(risk.activityId || '').split(/[;,]/).forEach(code => { const a = App.activityIndex[code]; if (!a) return; baseSched += (a.remainDrtnDays != null) ? a.remainDrtnDays : (a.origDrtnDays || a.drtnDays || 0); baseCost += (a.remainCost != null) ? a.remainCost : (a.origCost || 0); }); return { baseSched, baseCost, mode: risk.applyMode || globalMode }; });
Then per iteration, workingDurations is reset from _remainDurByIdx (not network.baseDuration):
// Per-iteration reset — start from REMAINING, not original workingDurations.set(_remainDurByIdx); // Per-risk handling in inner loop const inc = _replaceLinked ? (schedSample - _remainDurByIdx[links[li]]) // Replace: delta against remaining : schedSample; // Add: just the sample workingDurations[links[li]] += inc; // Cost — same pattern cost += _replaceCost ? (costSample - _rb.baseCost) : costSample;
What changes practically
If you previously ran the same XER through a legacy build, expect these differences in the new (current) build:
- P80 schedule numbers may be lower on schedules with significant in-progress portions. The engine isn't double-counting burned time.
- Per-activity Confidence dates are sharper — early finish dates reflect remaining-from-data-date, not from project start.
- Replace-mode behaviour on in-progress activities is now correct — sample becomes the new remaining, not the new total elapsed.
- Completed activities (remain = 0) contribute zero to the CPM working durations, even when linked to risks. (You shouldn't link risks to completed activities, but if you do, they're a no-op.)
Same philosophy for cost
Cost dimension follows identical logic. The cost variable per iteration is a "delta over project baseline cost" — it represents additional cost beyond the planned remaining cost.
_rb.baseCostis the sum of remaining costs of linked activities (from XER TASKRSRC remain_cost field, or backfilled from import).- Add mode:
cost += costSample— extra cost added to the project overrun. - Replace mode:
cost += (costSample - _rb.baseCost)— if sample is below remaining baseline, this can be negative (an opportunity / savings).
For a 100k-remaining-cost activity with a 30k replace-mode sample: cost += (30 - 100) = -70k. The project's cost forecast drops by 70k for that iteration, reflecting that the new remaining cost is lower than the planned remaining. Replace mode handles opportunity risks correctly.
Performance under the philosophy
Performance handling is consistent with the philosophy because:
- Each risk's sampled performance value IS a % loss — it's always relative.
- The loss conceptually applies to remaining work (which is the only work the simulation horizon covers).
- There's no need for an explicit "remaining performance baseline" because percentage loss is dimensionless.
- The perfMode setting (mult / add / max) controls how multiple losses compound. Default is multiplicative:
retention = (1 - loss1) × (1 - loss2) × ...
Add/replace toggle is therefore N/A for performance — and the dialog says so explicitly. Module 5 covered the scope; Module 7 deep-dives the KPI framework.
KPIs & Resources
Performance risk needs measurable indicators. The KPI framework lets you define what "performance" means for this project — on-time delivery, safety incidents, quality acceptance, resource utilisation, anything dimensional. Resources are first-class KPIs.
Defining KPIs
Settings → Performance has the KPI table. Each KPI carries these attributes:
Per-KPI three-point estimates in the risk dialog
Once KPIs are defined, the risk dialog renders a grid where you enter Min/ML/Max per KPI. This replaces the single legacy "performance %" field.
| KPI | Min loss | ML loss | Max loss |
|---|---|---|---|
| On-time delivery primary | 2 | 5 | 12 |
| Quality acceptance | 1 | 3 | 8 |
| Safety incident rate | 0 | 1 | 3 |
| Earthworks Crew resource | 0 | 5 | 15 |
Resources also appear in this grid (Phase 9 unification — "Resources ARE KPIs"). Tick a resource as performance-relevant in Settings → Performance → Resources, and it appears here for per-risk three-point input.
Resources from the schedule
When you load a Primavera XER, the parser reads the RSRC table (resource catalog) and TASKRSRC table (assignments). Each resource carries:
- Name + Code — from rsrc_name + rsrc_short_name
- Type — Labour, Material, Non-Labour
- UOM — from the UMEASURE table (units like crew-hr, m³, tonnes, kWh)
- targetQty / remainQty — aggregated from TASKRSRC target_qty / remain_qty across all assignments
- targetCost / remainCost — aggregated from TASKRSRC target_cost / remain_cost
- targetDurDays / remainDurDays — aggregated duration across assignments
- selectedForPerf — user-controlled tick; only ticked resources surface as performance KPIs
| ✓ | Name | UOM | Qty (remain / total) | Cost (remain / total) |
|---|---|---|---|---|
| ☑ | Earthworks Crew | crew-hr | 2240 / 3200 | 595k / 850k |
| ☑ | Concrete Plant | m³ | 180 / 400 | 120k / 240k |
| ☐ | Light Vehicles | hr | 820 / 1200 | 12k / 18k |
| ☑ | Specialist Welders | crew-day | 30 / 200 | 8k / 60k |
| ☐ | Generic Labour | hr | 450 / 1800 | 14k / 55k |
Why tick a resource as performance-relevant
Three things happen when a resource is ticked (selectedForPerf = true):
- It appears in the per-KPI grid of every risk dialog. You can enter three-point % loss estimates against it.
- The engine includes it in the per-KPI simulation pass (
_simulateKpisOnly), producing per-resource P50/P80/P90 numbers. - If amplification is configured (Module 8), the engine multiplies impacts against this resource based on its consumption ratio.
Not every resource needs ticking. Tick only the ones that meaningfully drive project performance — typically the critical-path resources, scarce specialist resources, or anything that's near its capacity ceiling.
The per-KPI driver tornado
After a simulation, the QPRA dashboard shows a per-KPI driver tornado — the top risks ranked by Spearman rank correlation with each KPI's iteration outcome. This tells you "if you mitigate this one risk, which KPI shifts most".
Drivers are computed independently per KPI, so the same risk might be a high driver for one KPI and low for another. This is the value of the unified framework: a single risk register feeds into multiple KPI-specific dashboards.
Custom resources & amplification
Beyond resources auto-imported from the schedule, you can define custom resources for any performance-relevant consumable. When resources approach their consumption ceiling, the amplification engine increases the impact of risks affecting them — capturing real-world fragility.
Custom resources — what and when
XER schedules don't always contain every resource the project cares about. Some examples that warrant a custom resource:
- Specialist crews not modelled in the schedule (e.g. permits team, design review board)
- Environmental constraints (e.g. allowable noise hours, water-extraction permit volume)
- Capital constraints (e.g. drawdown limits, working capital)
- Stakeholder bandwidth (e.g. owner review hours, regulator engagement days)
Sanity-clamping on save: remainQty is bounded between 0 and targetQty. If you enter values outside this range, the platform clamps them silently.
The amplification concept
Real-world intuition: a project that's already consumed 90% of its specialist welding crew is more exposed to further perturbation than one at 30% consumed. Less buffer, fewer alternatives, more cascading impact.
= (target - remain) / target
(default 0.8)
(no amp)
(amplification)
The amplification math
Settings → Performance has two knobs:
- Resource consumption amplification (
resourceAmp) — 0 to 2, default 0 (off) - Amplification threshold (
resourceAmpThreshold) — 0.5 to 0.95, default 0.8
function _resourceConsumptionMultiplier(metric) { if (metric.kind !== 'resource') return 1; const r = metric.resource; const orig = r.targetQty || 0; const remain = r.remainQty != null ? r.remainQty : orig; if (orig <= 0) return 1; const ratio = (orig - remain) / orig; const amp = App.settings.resourceAmp || 0; const threshold = App.settings.resourceAmpThreshold || 0.8; if (ratio <= threshold || amp <= 0) return 1; const excess = (ratio - threshold) / (1 - threshold); return 1 + amp * excess; }
In words: at exactly the threshold, multiplier = 1. At 100% consumed, multiplier = 1 + resourceAmp. Linear in between.
Worked example
When to use amplification
- Late-stage projects with significant resource consumption — set
resourceAmp> 0 to capture fragility. - Greenfield projects — leave at 0 (default). Nothing's consumed yet.
- Stress-testing — set amp = 1.0 or 2.0 to model "what if our specialist resources are near their limit by month X."
- Threshold tuning — 0.8 (80% consumed = stressed) is the default. Lower it (e.g. 0.6) for projects where you want amplification to kick in earlier.
Schedule tab & CPM
The Schedule tab is your activity table. The CPM forward-pass runs against this table every simulation iteration. Understanding what's stored and what's computed clarifies why P80 numbers move the way they do.
What lives in App.activities
{
task_id: 'T12345', code: 'A-4010', name: 'Earthworks and foundations',
wbs_id: '1.3.2', type: 'Task', status: 'Active',
// Duration fields
drtnDays: 90, // target/planned (legacy)
origDrtnDays: 90, // from XER target_drtn_hr_cnt
remainDrtnDays: 63, // from XER remain_drtn_hr_cnt — USED BY ENGINE
pctComplete: 30,
// Schedule dates
startISO: '2025-07-23', endISO: '2025-10-21',
// Float & criticality
tfDays: 0, ffDays: 0, isCritical: true,
// Cost (aggregated from TASKRSRC)
origCost: 850000, remainCost: 595000,
// Per-resource assignment breakdown
assignments: [{ rsrc_id: 5, targetQty: 3200, remainQty: 2240, targetCost: 850000, remainCost: 595000 }],
}
CPM forward-pass — the engine's heartbeat
Every simulation iteration:
- Reset
workingDurationsto per-activity remaining (_remainDurByIdx) - Sample each risk; add sampled value (Add mode) or compute delta (Replace mode) to each linked activity's working duration
- Run
_cpmForwardPass(network, workingDurations, workES, workEF)— Kahn's topological sort + forward-pass honouring FS/SS/FF/SF lag - Compute
cpmDelta = iterFinish - baselineCpmFinishfor this iteration's schedule contribution
All four relationship types and lag values are honoured — from the XER's TASKPRED table or from Excel round-trip via _rebuildTaskRelationshipsFromImport.
Design
30d
Engineering
90d
Earthworks
63d
Handover
45d
Each node lights up gold as the forward-pass arrives. ES = max(predecessor EF + lag). EF = ES + working duration. Critical-path activities (no float) are highlighted red. This walk repeats every iteration with the perturbed durations from that iteration's risk samples.
Critical path identification
The Schedule tab can be filtered to "critical only" — activities with TF below the criticality threshold (default 1 day). Use this filter to identify candidate activities for risk linkage.
Simulation engine internals
Under the hood, the engine is disciplined JavaScript. Understanding the per-iteration loop lets you reason about edge cases, choose iteration counts intelligently, and interpret results with confidence.
The simulate() function — high level
function simulate(risks, scenario) { // 1. Pre-compute (once per simulation) const network = _buildCPMNetwork(); const riskActIdx = _buildRiskActivityMap(risks, network); const riskBaselines = risks.map(...); // remaining-based const _remainDurByIdx = ...; // per-slot remaining // 2. Per-iteration loop (N = iterations) for (let it = 0; it < N; it++) { let sched = 0, cost = 0; workingDurations.set(_remainDurByIdx); for (let r = 0; r < risks.length; r++) { if (probSamples[r][it] > risk.effProb) continue; // gate const schedSample = sampleDist(impSamples[r][it], min, ml, max, distTime); if (links.length > 0) { for (let li = 0; li < links.length; li++) { const inc = _replaceLinked ? (schedSample - _remainDurByIdx[links[li]]) : schedSample; workingDurations[links[li]] += inc; } } else sched += schedSample; cost += _replaceCost ? (costSample - _rb.baseCost) : costSample; perfRetention *= Math.max(0, 1 - perfSample / 100); } const iterFinish = _cpmForwardPass(network, workingDurations, workES, workEF); schedTotals[it] = Math.max(0, iterFinish - baselineCpmFinish) + sched; costTotals[it] = cost; perfLosses[it] = (1 - perfRetention) * 100; } return processResults(schedTotals, costTotals, perfLosses, ...); }
Sampling — Monte Carlo vs Latin Hypercube
- Monte Carlo (default) — Independent uniform draws. Robust, well-understood. Needs more iterations to converge (5,000–10,000 typical).
- Latin Hypercube — Stratified sampling. Converges with fewer iterations (2,000–5,000). Slightly biased estimator for far-tail percentiles (P95+) on heavily correlated inputs.
Each bar draws over 2.5 seconds with a 0.2s stagger between rows. Bars rank by Spearman correlation between a risk's per-iteration sample and the iteration's total outcome. Top of the tornado = the highest-impact risk to mitigate next.
Convergence and reproducibility
After every run, the engine computes the relative standard error (RSE) of the schedule mean: RSE = (σ / √N) / μ. The Convergence slider sets the target (default 1%); when exceeded, an amber banner recommends N' = N × (RSE / target)².
Reproducibility: enable Seed Lock in Settings → Engine. The locked seed is captured in archive snapshots, the Run Log sheet, and report cover pages. Two analysts with the same locked seed get bit-identical results.
Scenarios — Pre / Post / Uncertainty
- Pre-mitigation — every risk uses raw probability and impact. The "untouched" view.
- Post-mitigation — every risk uses
postProb/postImp(or scaled by mitEffect). Models project after planned mitigation. - Uncertainty-only — no risk events fire; only activity-level uncertainty bands. Models inherent estimating noise.
Distributions & sampling
Eleven distribution shapes. Choose deliberately — shape drives the tail behaviour, and the tail is what your P90 and P95 come from.
The eleven shapes
Distribution choice guide
| Scenario | Recommended shape | Why |
|---|---|---|
| Standard schedule risk | BetaPert | Industry standard; ML weighting realistic |
| Cost overrun | LogNormal or BetaPert | Right-skewed tails capture catastrophic overruns |
| "Anywhere between 5 and 30 days" | Uniform | No preferred value |
| "Will succeed or fail, no middle" | Two-point | Discrete binary outcome |
| Activity estimating uncertainty | Triangular or Bitriangular | Wider tails for estimation noise |
| Confidence-bounded estimate | Trigen (trigenLo=10, trigenHi=90) | Honest about what "Min" means |
Correlation groups
Risks in the same correlation group sample their probability die-rolls together. Pooling three 50%-probability risks into one group → in any iteration either all three fire or none do.
Effect: increases variance of the sum (heavier right tail). Use when risks share a root cause — e.g. three weather-related risks driven by the same monsoon timing.
Excel template round-trip
Take a P6 XER, export to Excel, edit offline, re-import — the relationship graph and resource assignments rebuild from the workbook alone. The 20-column Schedule sheet is the single source of truth.
The 20 columns
| # | Column | Notes |
|---|---|---|
| 1-2 | Activity Code* / Activity Name* | Required |
| 3-5 | WBS / Type / Status | Categorisation |
| 6-7 | Start Date / Finish Date | ISO format preferred |
| 8-9 | Original / Remaining Duration (days) | Engine uses remaining |
| 10 | % Complete | 0-100 |
| 11-13 | Total Float / Free Float / Critical (Y/N) | Slack & criticality |
| 14-15 | Original / Remaining Cost | From TASKRSRC aggregation |
| 16 | Resource Quantity | Sum of qty assignments |
| 17 | Predecessors | A-1020 (FS,0d); A-1030 (SS,5d) |
| 18 | Successors | A-1100 (FS,0d) |
| 19 | Resources | Earthworks Crew (3200 crew-hr, 850000) |
| 20 | Source | File / template origin |
Template and export workbook use the SAME 20 columns — single source-of-truth header (SCHEDULE_SHEET_HEADERS).
Predecessor / Successor format
A-1020 (FS,0d); A-1030 (SS,5d); A-1040 (FF,-2d)
- Types: FS Finish-to-Start, SS Start-to-Start, FF Finish-to-Finish, SF Start-to-Finish
- Lag: in days; negative = lead
- Parser is tolerant:
(FS, 0d),(FS,0d),(FS 0d), andA-1020alone (defaults FS,0d) all work
Resource format
Earthworks Crew (3200 crew-hr, 850000); Concrete Plant (400 hr, 240000)
- Format:
<name> (<qty> <uom>, <cost>) - Multiple assignments separated by semicolons
- On first import, remaining defaults to target (no consumption recorded). Refine via the resource editor afterward.
Round-trip flow
_rebuildTaskRelationshipsFromImport() parses Pred/Succ strings into App.taskRelationships. _rebuildAssignmentsFromImport() parses Resources into App.resources + activity assignments. _syncScheduleResources() mirrors into App.performance.resources.fromSchedule.Filename pattern: <project_name>_Risk_Intelligence.xlsx for export, <project_name>_Risk_Intelligence_Template.xlsx for the empty template. Project name normalised (non-alphanumeric → underscore).
Reports & exports
Excel, PDF, and Word formats each have their purpose. Pick the right deliverable for the audience.
The Excel workbook
| Sheet | Content |
|---|---|
| Cover | Project metadata, run parameters, seed, iteration count |
| Risk Register | Full register with probabilities, three-point estimates, owners |
| Confidence Levels | P10/P25/P50/P75/P80/P90/P95 across all three dimensions |
| Sensitivity | Spearman rank correlations per risk, per dimension |
| Schedule | 20-column Schedule sheet — same as template |
| Performance KPIs | Per-KPI P50/P80/P90/Mean + driver risks per KPI |
| Insights | Auto-generated narrative findings |
| Issue Register / Claims | Materialised issues, claims tracker |
| Run Log / Audit Trail | Every run + every meaningful edit |
PDF reports
- Standard PDF — on-screen Report tab printed as PDF. Quick; methodology summary, top drivers, recommendations. Use for internal review.
- Detailed PDF — formal multi-page report: cover, executive summary, methodology with reproducibility statement, risk matrix, sensitivity drivers, per-risk appendix, per-KPI breakdown. Use for client deliverables and dispute panels.
Word export
The Word format wraps the Report tab's HTML in a Microsoft Word-compatible structure (Word-HTML with xmlns:o/w/Mso). Opens cleanly in Word for native editing — useful when the report needs annotation or further drafting.
Filename: <project_name>_Risk_Intelligence.doc
The Report tab structure
- Executive Summary — three KPI tiles (P80 sched / cost / perf) + headline recommendations
- Methodology — iteration count, sampling method, distribution choice, seed status
- Per-Dimension Analysis — QSRA, QCRA, QPRA
- 3a · Performance KPI Breakdown — per-KPI P50/P80/P90 table
- 3b · Top Driver Risks per KPI — Spearman-ranked drivers for each KPI
- Scenario Comparison — Pre vs Post deltas, mitigation effectiveness
- 4b · Section — additional dimensional analysis
- Risk Register Appendix + Sensitivity Drivers
Best practices & glossary
Distilled lessons from actual engagements. The patterns that produce defensible analyses and the pitfalls that produce contested ones.
Workshop facilitation patterns
- Capture the source citation for every risk. "Workshop 2026-04-12 · SME interview with K. Patel" is defensible; a blank source field is a vulnerability in a claim.
- Calibrate against the activity context. Use the remaining-duration baseline strip as your reality check. If you're entering 60d ML for an activity with 30d remaining, that's a replace-mode call, not an add.
- Don't conflate uncertainty with risk. Activity-level estimating uncertainty (Uncertainty register) is separate from discrete risk events (Risk register).
- Iterate on the apply-mode call. Default Add. Switch to Replace when the three-point estimates the new total, not additional time. Document reasoning in the Description.
Calibrating three-point estimates
- Min ≠ absolute lowest. The 10th–15th percentile. Use optimistic-but-plausible.
- ML = the most likely if everything goes as expected.
- Max ≠ apocalyptic. The 85th–90th percentile. Worst plausible given current info.
- Width check: Min and Max too close → BetaPert near-deterministic. Widen if the team has uncertainty.
- Skew check: Max − ML > 4 × (ML − Min) → heavily right-skewed. Consider LogNormal.
When Add, when Replace
- Use Replace when the three-point estimates the new total: contractor change, permit-driven redesign, fundamentally different execution approach.
- Stay Add when the three-point estimates additional impact: weather delays, scope additions, defect rework.
- Document the call in the risk description. Future reviewers should understand why this specific risk is in Replace mode.
- Don't mix without reason. Categorical consistency makes the rationale clear in audit.
Audit trail discipline
- Every override, sign-off, deletion, and material edit is logged automatically with timestamp + user.
- Use the Source / Citation field for traceability — workshop date, SME name, document reference.
- Use Review and Approve sign-offs (Settings → Categories → Reviewers) to capture two-eye gates on risk register edits.
- Archive snapshots at every major engagement milestone — "Pre-mitigation review", "Post-Apr workshop", "Final submission". The archive history acts as your audit trail.
- Export the Audit Trail CSV (Settings → Audit) at engagement close.
Common pitfalls
| Pitfall | Symptom | Fix |
|---|---|---|
| Single P50 used as forecast | Stakeholders treat P50 as "the answer" | Always quote P80 alongside; explain confidence semantics |
| Risks linked to completed activities | No engine effect (remain = 0) | Move risks to currently-active activities, or unlink |
| Mixing original and remaining mental models | Numbers don't match analyst intuition | Re-read Module 06; the engine uses remaining everywhere |
| Too few iterations | Convergence banner amber | Bump to recommended N; or use Latin Hypercube |
| Replace mode on add-style estimates | P80 collapses oddly | Audit the per-risk applyMode column; reset to inherit |
| Forgetting to tick resources | Per-KPI driver tornado empty for resources | Settings → Performance → Resources → tick what matters |
Master glossary
delta = sample − remaining. The engine adds delta to working duration, which lands the activity at the sampled value.Filename conventions
| Export | Filename |
|---|---|
| Excel workbook | <project_name>_Risk_Intelligence.xlsx |
| Excel template | <project_name>_Risk_Intelligence_Template.xlsx |
| Word document | <project_name>_Risk_Intelligence.doc |
| PDF (standard / detailed) | Browser print-to-PDF naming |
| JSON project archive | <project_name>_archive.json |
Project name is normalised: non-alphanumeric characters become underscores. "ACME / Phase 2 Construction" becomes ACME_Phase_2_Construction.
Where to go from here
- Try a real XER end-to-end: load → review activities → link risks → run engine → publish report. The platform's intuition is hard to build without hands-on iteration.
- Read the Help tab in the app. It mirrors this training course plus has FAQ entries that capture answers to common engagement questions.
- Build a personal library of risk-source citations you trust. Workshop minutes, SME interview notes, lessons-learned databases. Reference these in the Source / Citation field consistently across projects.
- Pair-review. Every claim-package level analysis should have a second analyst review the register, distribution choices, apply-mode calls, and per-KPI estimates. The audit trail will capture the review.
- Iterate the methodology. Capture your own engagement-specific patterns. Keep them in a project README so the next analyst on the same engagement has continuity.