Guide
Reading your risk-adjusted schedule.
What the bars, tails, and percentiles actually mean, why tails land where they do, and the weekly loop that keeps the forecast honest.
1. Getting started
Five steps from a blank workbook to a risk-adjusted Gantt. The same list, plus system requirements, lives on Support.
- Install the Epoch SRA add-in: in Excel, Insert > Get Add-ins (or Home > Add-ins), search for “Epoch SRA” and click Add. Then click the Epoch SRA button that appears on the Home tab to open the task pane. In some organizations the add-in store is switched off by IT policy: if Get Add-ins is missing, greyed out, or the search will not install anything, ask your IT admin to allow Office Store add-ins for your account, or to deploy Epoch SRA centrally from the Microsoft 365 admin center (Integrated apps).
- Click Insert blank template to create the three empty structures: the
ESRA_Scheduletable (with theESRA_ProjectStartcell), theESRA_Risksregister, and theESRA_Eventslog. Your real data is kept untouched, and sample rows from See an example are treated as disposable and replaced, so the button is always safe to press. Have an MS Project schedule instead? Export it as XML (File > Save As > type: XML) and click Import MS Project XML. The file is read locally and never leaves your machine, and the import creates the empty register and event log alongside the schedule. - Fill the schedule (dates first; see below) and type your risks into the
ESRA_Riskssheet. The category and status columns carry in-cell dropdowns, likelihood and severity accept whole numbers 1–5, and the register section documents every column. Have an existing register elsewhere? Select its rows including headers and click Import from selected range: any layout works, columns are recognized by name and synonyms, and your cells are never modified. - Click Compute to run the analysis and write the
ESRA_Resultssheet, then Open full Gantt to see the risk-adjusted schedule (and Save as PNG to export it). - Want to explore first instead? See an example inserts a fictional demo schedule and register. In a blank workbook the demo is fully computable; in a workbook that already has your data, the demo sheets go in side-by-side as view-only and the forecast keeps reading your structures — the status line names exactly what stays canonical. Looking at an example never touches your data.
Entering a schedule by hand: dates first
The ESRA_Schedule table takes dates as first-class input, because PMs think in dates: give each row a planned_start + planned_finish and the working-day duration is derived, or a planned_start + duration_days and the finish is derived. A bare duration_days still works and schedules purely from links, exactly as before. If dates and a duration are both given and disagree, the dates win and Compute says so in a schedule note. Nothing is silently picked; a planned_finish alone is an error naming the row. Dates on a weekend snap forward to the next working day, with a note. And the model is the same one the MS Project import uses: a dated task holds its date; risk pushes it later only, through its links or its own risks, never earlier.
2. Importing from MS Project
Export your plan from MS Project as XML (File > Save As > type: XML), then click Import MS Project XML in the pane’s Schedule section. The file is read on your machine and never uploaded; your .xml is read-only and is not modified. The translated schedule lands on an add-in-owned Imported_Schedule sheet as the ESRA_Schedule table plus the ESRA_ProjectStart cell. Your own sheets are not touched.
The stored dates are the baseline
The import reproduces your plan as stored: each task carries its file’s own Start and Finish into planned_start / planned_finish, and Compute pins every dated task at its stored start. The deterministic schedule shows MS Project’s dates; manually scheduled or not, dates come from the file either way. Risk then pushes tasks later only: through a translated link from a risk-stretched predecessor, or through the task’s own risks. Nothing ever moves earlier than the plan you imported. (One convention note: planned_finish is written as the working-day instant after the stored finish day, the same point-in-time convention every Epoch SRA sheet and chart uses, so derived durations equal MS Project’s working-day spans.)
What translates
- Tasks with their stored dates: each becomes a row, numbered
T001upward, scheduled from its stored Start/Finish. A task with no stored dates falls back to its work-hours duration, and is listed. Blank/null rows are skipped with a count. - Finish-to-start links with zero lag: these become
depends_onentries. - Links touching summary rows are re-anchored to leaf level rather than dropped: a summary predecessor re-anchors to its last-finishing leaf descendant, a link into a summary to its earliest-starting leaf. Nested summaries resolve to leaf level; ties break to the lowest UID. Every re-anchoring is listed by name in the report.
- Milestones, from MS Project’s milestone flag, pinned at their stored date.
- Project start = the earliest stored task start, into
ESRA_ProjectStart. The file’s project-level StartDate is not used for scheduling; when the two disagree, the report shows both and says which was used.
What doesn’t translate yet
v1 translates finish-to-start, zero-lag links only. SS, FF, and SF links, and any link with a lag, are not translated. They are listed by name in the import report, never dropped silently. The same goes for a predecessor that doesn’t resolve (a task that isn’t in the file). Non-ASAP constraints (Start No Earlier Than and friends) are report-only: with the stored-date baseline a constrained task already sits at its stored date, so the report simply names each one. If a duration can’t be parsed it is written as 0 and named; if a task has a blank name it is named too, because the schedule reader requires one.
This is deliberate. Approximating an SS link as FS would move dates by an amount nobody asked for, and you would have no way of knowing it happened. A named refusal is recoverable; a silent guess is not.
What the report’s counts mean
- tasks converted
- Rows written to
ESRA_Schedule, and how many are milestones. - rows skipped
- Summary/rollup rows (their children carry the work) and blank/null rows. Counted, never silent.
- links resolved
- Finish-to-start, zero-lag links that made it into
depends_onuntouched. - re-anchored
- Links whose summary endpoint was re-anchored to a leaf descendant and then translated. Each is named, with the leaf it now holds at. Resolved + re-anchored + not-translated + unresolved (+ any dropped from an empty summary) accounts for every link in the file.
- manually scheduled
- Informational only: how many tasks MS Project marks manually scheduled, counted over every row including summaries, the same numbers MS Project itself shows. Dates come from the file either way, so this is context, not an alarm.
- links not translated
- Everything else, listed individually with its type and lag (
T003 — SS/lag=0) or the reason it wouldn’t resolve. Each one is a dependency MS Project has and Epoch SRA does not.
What to do about a link that didn’t translate
Two honest options, and the report gives you the task ids for both.
Add the dependency by hand. In the ESRA_Schedule table, put the predecessor into the task’s depends_on cell. The column takes typed links, so an SS link is not lost in translation: write T003:SS, or T003:FS:5 for a five-day lag. This is the option that keeps your network intact.
Or accept the task as unlinked, but know what you are accepting. A task with nothing in depends_on cannot receive risk from upstream: its dates hold at the imported baseline unless its own risks fire. It will show a clean, flat bar and no tail, which reads as safety and is not. When a significant share of tasks are unlinked, the results header says exactly this, in one line.
3. How to read the Gantt
Every row is one task from your schedule table. The bar is your plan; the amber is what the simulation adds to it.
- planned
- The bar:
planned_starttoplanned_finish, straight from your table. Epoch SRA never moves it. - critical (float 0)
- A darker bar with an outline, so it survives greyscale printing. It reads the
is_criticalcolumn ofESRA_Results, which the engine sets when total float is zero within a rounding epsilon. - P80 tail · field-calibrated
- The amber extension, from the planned finish to the selected percentile’s finish. Its length is the schedule exposure: how much later than plan the simulation puts this task. The second half of the label is the provenance of the percentile you selected, not a fixed string.
- milestone (planned / P80)
- Milestones draw no bar. They draw two diamonds joined by a connector. Solid is the planned date, hollow is the selected percentile’s date. The gap between them is the slip. The hollow diamond only appears when there is a tail.
- today
- A vertical line with an ISO-dated chip. It is drawn only when today falls inside the chart’s date range, so it is absent on a purely historical or future window.
- last-run P80 (ghost)
- A dashed vertical line per row marking where the previous Compute put that task’s P80 finish. Toggle it with Since-last-run ghosts; the checkbox is disabled when there is no prior run. Ghosts always mean P80, whichever percentile you are viewing.
The line under the chart
It reads something like Run 2026-07-13T14:32:10Z · P80 finish — field-calibrated · 12 of 13 tasks rendered · 1 not rendered · 1 without a P80 tail, and it is stamped into the frame itself so an exported PNG can never lose its provenance.
Not rendered and without a tail are different failures. A row is not rendered when it could not be drawn at all (a missing or unparseable planned_start, a finish before its start, a year outside 1990–2100), and it carries the reason. Rendered plus not-rendered always equals the number of rows in the sheet; a row is never silently dropped. A row without a tail was drawn normally and simply had no exposure to show.
4. The charts
After Compute, Open charts gives you four views of the same run. Each carries its run timestamp, seed, iteration count and percentile tiers inside the image, so a chart pasted into a review deck cannot lose the context it was computed under. Save as PNG exports at 2×.
S-curve
Cumulative confidence against finish date: for any date, the share of simulated runs that finished by it. The markers follow one colour law throughout: amber is a commitment (P80 and above), and a cool dashed marker is a reference you should not commit to (P20, P50, P70). The dotted line is the deterministic finish: where the plan lands with no risk at all.
Histogram
The same run as a distribution: every simulated finish, binned. It shows the shape the S-curve smooths away: whether the spread is tight or long-tailed, and whether it is lumpy.
Tornado: measured, not estimated
One bar per risk: the working days the project P80 moves between the runs where that risk fired and the runs where it did not. It is measured on the run's own samples, not calculated from probability × impact beforehand.
This matters when you compare tools. The task pane's top risk drivers list ranks by an up-front estimate; the tornado ranks by observed effect. They will not always agree, and that is correct. An up-front estimate cannot know that a risk lands on a path that was already late, or that two risks bite the same slack. Where they disagree, the tornado is the one that watched what happened.
A risk that never fired, or is certain to fire, has no two worlds to compare, so it has no bar; it is counted as not measurable this run rather than shown as zero. A bar from very few firings is marked thin. The footer states how many risks were measured and how many bars are shown, so a truncated list never reads as the whole list.
Sensitivity
One row per task, sortable by either column.
— in sensitivity means no mapped risk, not zero effect.- criticality
- The share of iterations the task landed on the critical path. 100% means it was always critical; a task at 40% is only sometimes the thing holding the date.
- sensitivity
- The measured contribution of the risks mapped to this task: how much the project P80 moves because of them. A task with no mapped risk shows
—. - P20–P80 finish
- When this task finishes, across the run. The low end is a reference dot, the amber bar runs to the P80. All rows share one date scale, so the bars are comparable.
The two numeric columns answer different questions and are worth reading together: sensitivity is measured against the project finish, criticality is about this task. A task can sit at 19% criticality and still be the largest single contributor to your P80: rarely on the critical path, yet carrying the risk that moves the program when it bites. Sorting by criticality alone would bury it.
Where P20 comes from
P20, P70, P90 and P95 appear only when Extended percentiles is ticked before Compute, which also raises the run to 20,000 iterations, because deep quantiles are lumpy at 5,000. All four are labelled model estimate (calibrated priors): same simulation, same calibrated priors, but the field evidence is against P80, not against them.
P20 is written to ESRA_Results and shown in the charts, but the Gantt never offers it. That is deliberate. The Gantt's tail is drawn in the commitment colour, and P20 is an optimistic bound: an 80% chance of missing, not a planning date. An amber P20 tail would say “commit to the date you will almost certainly miss.”
5. Percentiles
The selector offers exactly those percentiles whose p*_finish column exists in ESRA_Results. By default that is P50 and P80. It opens on P80 and never auto-selects P50.
P50: the median, not a commitment
Half the simulated outcomes finish later than this date. That is the whole meaning of it. The Gantt labels it P50 (median — not a commitment date) and the status line spells out the consequence: P50 finish (median — 50% chance of missing). It is deliberately drawn in a cool outline rather than the amber commitment styling, because a median presented in commitment colours is a date someone will quote in a review. Useful as a reference: if your plan sits far from P50, the plan is optimistic before any risk is considered.
P80: field-calibrated, the level to commit to
Four runs in five finish by this date. This is the default view and the one the product is built around: P50 and P80 are field-calibrated against replayed historical programs, which is why the legend says field-calibrated rather than a bare “calibrated”. When you are asked for a date to defend, this is the one that has been checked against what actually happened on real programs.
P70 / P90 / P95: model estimates, stress views
These are off by default. Tick Extended percentiles (P70 / P90 / P95) in ESRA_Results before you Compute, or the columns are never written and the selector will not offer them. They are labelled model estimate (calibrated priors), truncated to model estimate in the tight legend, and the distinction is deliberate. They come from the same simulation on the same calibrated priors, but the deep tail depends on distribution shape and risk correlation that the field evidence does not yet pin down. Read them as stress views: what a bad run looks like. Do not read them as a calibrated commitment, and do not quote P95 as a date.
6. Why tails appear where they do
A task with no mapped risk still has a tail.
Correct, and it should. A task’s percentile finish comes from the simulated network, not from the risks mapped to that row. If a predecessor’s distribution pushes its finish later, everything downstream inherits the shift through its dependencies. A tail on an unmapped task means its predecessors are uncertain: it is exposed, not risky. To shorten it, look upstream.
At P50, almost nothing has a tail.
Expected. A tail is drawn only when the selected percentile’s finish is later than the planned finish; equal or earlier draws nothing, and the row is counted in the footer as without a P50 tail. P50 is the median, so for much of the network the median outcome lands on or before the plan. That is the 50% frontier, not a missing calculation. It is also the point: at P50 the schedule looks fine, which is exactly why committing to it is a coin flip. Switch to P80 to see the exposure.
Some tasks show no tail, and the status says unlinked.
An unlinked task names nothing in depends_on: it sits in the sheet without sitting in the network. Compute says so plainly: unlinked tasks (2): T07, T09 — dates without depends_on receive no upstream risk. Nothing upstream can reach it, so it shows no tail however risky its predecessors are; it hasn’t got any. It is not a safe task, it is an unmodelled one, and the flat bar is the tool telling you it has nothing to say about that row.
Two things cause it. Either the task’s dates came from a planned_start/planned_finish pair and no dependency was ever given; or it arrived from an MS Project import whose links didn’t translate. An SS link or a lagged link is reported, not converted, so the task lands with an empty depends_on. Check the import report against this list. The fix either way: give it a predecessor in depends_on (typed links are allowed, e.g. T003:SS), or map a risk directly to it.
The last milestone’s tail is huge.
It accumulates, and two things concentrate there. First, propagation: every upstream distribution feeds the terminal milestone through the dependency chain, so its spread is the network’s, not one task’s. Second, scope: a risk marked PROGRAM_WIDE, or left with no mapped tasks at all, resolves onto the terminal sink: the no-successor task whose early finish is the project finish. It lands squarely on your last milestone. Before you argue with the arithmetic, check how many of your risks are program-wide.
7. The Tuesday loop
The schedule stays current because you tell it what happened, not because it guesses. The loop is: log what happened, Compute, read what moved.
Log an event
An event is something that has happened, as opposed to a risk that might. Type it as a row in the ESRA_Events sheet: an event_id (E01 style), a title describing what happened, the date it became known (yyyy-mm-dd), a category from the in-cell dropdown (use other when none fit), the affected_tasks, and magnitude_days: your best believed estimate of the damage in working days. The pane's Events list names each row and follows the sheet.
Nothing moves yet. The event carries a not yet computed marker until you click Compute. Written to the sheet is not the same as folded into the forecast. Scope works like the register: write PROGRAM_WIDE in affected_tasks to land the event on the terminal milestone, or leave it empty to keep it a draft; a draft is explicitly not applied until you fill in tasks. Converted risks arrive here automatically, carrying the source risk's title.
Record the actual
When the damage is known, the event sits under Awaiting actuals with an actual (wd): field and a record button. Record it and the row reads believed 15 → actual 20 wd on T03. (Forecast still uses believed.)
That parenthesis is the important part, and it is not a limitation. The forecast is mutated by the believed magnitude and never by the actual. If a recorded actual retroactively rewrote the forecast, every past run would silently improve itself with information it did not have at the time, and you could never reconstruct what the tool told you in March. No look-ahead, so the run you defended in a review still says what it said.
What SINCE LAST RUN shows
It compares your previous Compute with this one: the project finish and every milestone’s P50 and P80, plus events added, removed, or changed in magnitude, measured in working days from each run’s own start, so a rebased project start does not fake a movement. On a first run it says so: First run — nothing to compare yet. When something moves, it attributes it: Event E3: supplier slip +15d on T13 → finish P80 +11d. The Gantt ghosts read the same snapshot.
What recording actuals does (and does not) do
It builds the believed-versus-actual record in your workbook. That record is what makes the priors improvable: it is the evidence of how far off the estimates were, per category, on a real program.
It does not re-fit anything on your machine. The add-in never edits its own priors, and nothing you record changes your numbers. Priors move only when a new domain pack ships, after that evidence has been replayed offline against the backtest programs. This is a deliberate line: a tool that quietly re-tunes its model under a live program produces forecasts nobody can reproduce or audit.
8. Register basics
The sheet is the register
The register is yours: no import path ever writes or clears it, and you author it directly in the ESRA_Risks sheet. Clicking a risk in the pane's list selects its row; the pane shows status, the cells hold the data. One rule set validates every row however it arrived: typed, imported, or sample.
The columns
- risk_id
- Your identifier, unique per row. R01 style keeps sorting sane, but any non-empty id works.
- title
- What the risk is, in your words. This is the name the pane and reports use.
- category
- One id from the in-cell dropdown; the table below maps ids to their meaning. Choose the nearest category; impact priors are calibrated per category, so the category is what prices the risk. Event-only categories are rejected here: a risk needs a risk-capable, calibrated-prior category.
- likelihood, severity
- Whole numbers 1–5 (ECSS 5×5). The cells reject anything else.
- risk_index
- Derived, never typed: the sheet holds a live =L×S formula and the ECSS heat-map.
- affected_tasks
- Comma-separated task ids from
ESRA_Schedule(for exampleT001, T004), the single wordPROGRAM_WIDE, or empty (unmapped; see the scopes below). - status
- One of
open / closed / realized / retired / removed, from the dropdown. Blank reads as open. - owner, notes
- Free text. Notes feed the mapping suggestions’ keyword matching.
Category ids
Cells store the bare id; this table is what each one means. Risk-capable ids (usable in the register): supplier_slip supplier delivery slip · test_failure test failure and retest · requirement_change requirement change · export_control_delay export control / licensing delay · lli_delay long-lead item delay · customer_delay customer-side delay (GFE, reviews) · funding_event funding event (payment or round delay) · ncr_waiver non-conformance / waiver. Event-only ids (valid in ESRA_Events, rejected in the register): launch_manifest_change launch manifest change · key_person_loss key person loss · team_resize team resize / re-allocation · wp_restructure work package restructure. Events additionally accept other for anything outside the taxonomy.
L × S scoring
The register is ECSS-M-ST-80 5×5: likelihood and severity, each 1–5, and risk_index = likelihood × severity, 1–25. Two details matter in practice. The index is written into your sheet as a live formula (=D2*E2) rather than a number, so it cannot go stale the moment someone edits L or S; and the add-in derives it on read regardless, ignoring whatever the column holds.
The red/amber/green colouring is not a threshold on the index. It is an explicit 5×5 lookup, and it is asymmetric on purpose: high severity turns red sooner than high likelihood does. A 4×2 and a 2×4 both index 8 and do not get the same colour. Do not infer a band from the number.
Importing a register that scores in words works: High maps to 4, Very low to 1, and so on, and the mapping is noted per cell. Values above the scale are clamped to 5.
Mapped, program-wide, unmapped
There are three scopes, not two, and the third is a question rather than an answer.
- mapped
affected_tasksnames real tasks. The risk overlays those rows.- program-wide
- The literal
PROGRAM_WIDEtoken: a decision that the risk bites the whole program. It overlays the terminal sink. - unmapped
affected_tasksis empty. This is an undecided risk, not a program-wide one. It still overlays the terminal sink so it is never silently ignored, but the accounting counts it separately: an empty cell is a question you have not answered yet.
Suggest mappings proposes risk-to-task links from deterministic keyword overlap, for open risks only. Every suggestion is a proposal you accept or reject; it never auto-links. It will sometimes be confidently wrong (a construction-noise risk whose keyword “vibration” hooks the vibration test), which is why it asks.
What overlays, and what does not
Only open risks overlay. A closed, realized, or retired risk is a real outcome and is excluded; a risk with a linked_event has become an event and is counted there instead, never twice. Two categories, supplier slip and long-lead-item delay, are skipped with a stated reason rather than a silent zero: they price from a reported slip duration, which a 5×5 row does not carry. If nothing overlays, the pane says so plainly rather than showing you an unadjusted schedule that looks computed.
Merge or replace on re-import
Re-import is refreshable and non-destructive. Merge (the default) updates matching risks and keeps the rest, preserving your task mappings: a re-import does not undo an afternoon of mapping work. Replace marks open risks that are absent from the source as withdrawn, with the reason recorded.
Neither mode deletes a row, and your source range is never modified; the importer only reads it. A withdrawn risk that reappears in a later import is re-opened by that evidence.
9. Glossary
P50 and P80 dates
A P-value date is a confidence level over simulated outcomes: the P50 date is the median (half the simulated runs finish later), and the P80 date is the date 80% of runs finish on or before. In Epoch SRA, P80 is the commitment level and is field-calibrated against replayed historical program actuals; P50 is drawn as a cool reference, never in commitment styling, because committing to a median is a coin flip.
Merge bias
Merge bias is the systematic lateness that appears where parallel paths merge: the merge point must wait for the slowest incoming path in every simulated outcome, so its expected date is later than any single path suggests. Deterministic CPM schedules cannot show this effect; Monte Carlo simulation reproduces it naturally, which is a large part of why simulated dates sit to the right of the plan.
DCMA 14-point assessment
The DCMA 14-point assessment is a schedule-quality checklist published by the US Defense Contract Management Agency, covering structural health measures such as missing logic, leads and lags, constraints, and float. It scores whether a schedule is well-formed enough to analyze — it is a format for schedule hygiene, not a simulation method.
Discrete risk vs duration uncertainty
Duration uncertainty is the spread around how long planned work takes; a discrete risk is an event that either happens or does not, adding its own impact when it fires. Epoch SRA models discrete risks from the register — each open risk overlays its mapped tasks with a calibrated, category-priced impact at its scored likelihood — and treats occurred events as deterministic schedule mutations, so a realized risk is converted and never double-counted.