Lesson 03 / 12
Value Stream
Mapping the path from idea to production: in a nine-step flow, 20 work items' average 182.0-step lead time breaks down into 70.2 steps of process and 111.8 steps of wait (value-added share 38.5%); of the total 2237 steps of wait, 1485 sit in a single step — the release window; with the release window at 20 steps, no single-step improvement shortens lead time by more than 2 steps; once the window is cut to 5 steps, the same improvements pull apart, with writing dropping from 10 to 5 gaining 32 steps but acceptance approval dropping from 2 to 1 losing 4 steps; and the bottleneck shifts from the release window to writing, then to integration testing.
Contents
The previous lesson ran the same path through two arrangements and counted the context that dropped at the boundary, but held the path itself fixed: seven steps, fixed durations. On a change’s path from idea to production, the real question is not how many steps there are, but how long each of those steps spends standing still.
A value stream map is a table that writes three numbers for every step on this path: process
time, wait time, and percent complete and accurate. It measures the same object as the Process,
Team and Delivery course’s work item — the scale differs: there, what was measured was a work
item’s path across teams; here, what is measured is a change’s path from idea to production.
Building the Map
The example is again the fictional regional measurement network; the network and the flow are both fictional. CF13: the path is nine steps — request queuing, analysis, writing, review, integration test, acceptance approval, release window, production setup, production verification. CF14: every step has a concurrent-executor count, a process time, and a percent complete and accurate; an item that is not complete and accurate the first time goes back to the step’s bounce target and runs forward again from there. CF15: twenty work items enter the flow twelve steps apart. Wait is not an input, it is measured: it accumulates in the queue if the executor is busy, and in the window if the release window is closed. CF16: the release window opens once every twenty steps. CF17: randomness is written with a generator, the seed is 20260801; each item is given its own sequence of rolls, so when a step’s duration changes an item’s luck does not change and runs stay comparable. CF18: lead time is the steps from entering the flow to production verification finishing; value-added time is the total process time an item spends across the steps.
The map and the flow are a process-internal model; there is no real flow record or release calendar.
// flow/map.mjs — the value stream map from idea to production (model): there is no real flow record or // release calendar, steps are a data structure. Randomness is written with a generator, the seed is visible. export const N = 20, ARRIVAL = 12, LIMIT = 4000, RELEASE_WINDOW = 20, SEED = 20260801; // step: name, concurrent executors, process time, percent complete and accurate, bounce target export const STEP = [ ["request-queuing", 1, 1, 1.00, 0], ["analysis", 1, 4, 0.85, 0], ["writing", 2, 10, 0.80, 1], ["review", 1, 3, 0.75, 2], ["integration-test", 1, 8, 0.70, 2], ["acceptance-approval", 1, 2, 0.90, 1], ["release-window", 1, 1, 1.00, 0], ["production-setup", 1, 3, 0.95, 2], ["production-verification", 1, 2, 0.90, 2], ].map(([name, cap, process, pca, bounceTo], i) => ({ name, cap, process, pca, bounceTo, i })); // linear congruential generator: each item gets its own sequence of rolls, so when a step's // process time changes an item's luck does not change, and runs stay comparable. export function rng(seed) { let x = seed >>> 0; return () => ((x = (x * 1664525 + 1013904223) >>> 0) / 2 ** 32); } export function run({ process = {}, window = RELEASE_WINDOW } = {}) { const step = STEP.map((a) => ({ ...a, process: process[a.name] ?? a.process })); const roll = rng(SEED); const K = Array.from({ length: N }, (_, i) => ({ i, arrival: i * ARRIVAL, step: 0, ready: i * ARRIVAL, running: false, remaining: 0, done: false, finishAt: null, processTotal: 0, wait: 0, rework: 0, error: false, errorAt: null, recovery: null, rolls: Array.from({ length: 80 }, roll), rollAt: 0 })); const busy = step.map(() => 0); const S = { wait: step.map(() => 0), process: step.map(() => 0), visits: step.map(() => 0), bounces: step.map(() => 0), deploys: 0 }; const finish = (d, t) => { const a = step[d.step]; busy[a.i]--; d.running = false; if (a.pca < 1 && d.rolls[d.rollAt++] >= a.pca) { // not complete and accurate the first time S.bounces[a.i]++; d.rework++; if (a.name === "production-verification" && !d.error) { d.error = true; d.errorAt = t; } d.step = a.bounceTo; d.ready = t; return; } if (a.name === "production-setup") S.deploys++; if (a.i === step.length - 1) { d.done = true; d.finishAt = t; if (d.error) d.recovery = t - d.errorAt; return; } d.step = a.i + 1; d.ready = t; }; for (let t = 0; t < LIMIT; t++) { for (const a of step) { if (a.name === "release-window" && t % window !== 0) continue; // batched release window const queue = K.filter((d) => !d.done && !d.running && d.step === a.i && d.ready <= t) .sort((x, y) => x.ready - y.ready || x.i - y.i); for (const d of queue) { if (busy[a.i] >= a.cap) break; S.wait[a.i] += t - d.ready; d.wait += t - d.ready; S.process[a.i] += a.process; S.visits[a.i]++; d.processTotal += a.process; d.running = true; d.remaining = a.process; busy[a.i]++; } } for (const d of K) if (d.running && --d.remaining === 0) finish(d, t + 1); } return { K, S, step }; } export const avg = (a) => (a.length ? (a.reduce((s, v) => s + v, 0) / a.length).toFixed(1) : "-"); export function measure(config) { const { K, S, step } = run(config); const lead = K.map((d) => d.finishAt - d.arrival); const windowEnd = Math.max(...K.map((d) => d.finishAt)); const valueAdded = K.reduce((s, d) => s + d.processTotal, 0), totalLead = lead.reduce((s, v) => s + v, 0); return { K, S, step, leadTime: avg(lead), valueAddedShare: (100 * valueAdded / totalLead).toFixed(1) + "%", longestWaitStep: step[S.wait.indexOf(Math.max(...S.wait))].name, rolledFpy: (100 * K.filter((d) => d.rework === 0).length / N).toFixed(0) + "%", bounces: K.reduce((s, d) => s + d.rework, 0), deployFrequency: (100 * S.deploys / windowEnd).toFixed(1), changeFailureRate: (100 * K.filter((d) => d.error).length / N).toFixed(0) + "%", recovery: avg(K.filter((d) => d.error).map((d) => d.recovery)), unfinished: K.filter((d) => !d.done).length }; }
// flow/measure.mjs — reading the map: value-added share, longest wait, single-step improvement. import { STEP, N, RELEASE_WINDOW, SEED, measure, avg } from "./map.mjs"; const print = (g, ...s) => console.log(s.map((v, i) => (g[i] < 0 ? String(v).padEnd(-g[i]) : String(v).padStart(g[i]))).join("")); const T = measure({}), P = measure({ window: 5 }); console.log(`${N} work items, ${STEP.length} steps, release window ${RELEASE_WINDOW} steps, ` + `seed ${SEED}; unfinished ${T.unfinished}`); const A = [-25, 6, 10, 12, 9, 9, 9, 10]; console.log("\n1. value stream map (measured)"); print(A, "step", "cap", "process", "avg. wait", "visits", "%C&A", "bounces", "wait"); for (const a of T.step) { const visits = T.S.visits[a.i]; print(A, a.name, a.cap, a.process, (T.S.wait[a.i] / visits).toFixed(1), visits, a.pca < 1 ? `${(100 * (1 - T.S.bounces[a.i] / visits)).toFixed(0)}%` : "-", T.S.bounces[a.i], T.S.wait[a.i]); } const B = [-35, 16]; console.log("\n2. the map's totals (baseline)"); for (const [ad, v] of [["avg. lead time (step)", T.leadTime], ["process per item (step)", avg(T.K.map((d) => d.processTotal))], ["wait per item (step)", avg(T.K.map((d) => d.wait))], ["value-added share", T.valueAddedShare], ["step where the longest wait sits", T.longestWaitStep], ["rolled first-pass yield", T.rolledFpy], ["total bounces", T.bounces]]) print(B, ad, v); const C = [-25, 11, 12, 10, 12, 10]; console.log("\n3. if a single step's process time is halved (at two window settings)"); print(C, "improved step", "process", "window 20", "change", "window 5", "change"); print(C, "(baseline)", "-", T.leadTime, "-", P.leadTime, "-"); for (const a of STEP.filter((a) => a.process >= 2)) { const y = Math.floor(a.process / 2), i = { [a.name]: y }; const R = measure({ process: i }), Q = measure({ process: i, window: 5 }); print(C, a.name, `${a.process} -> ${y}`, R.leadTime, (R.leadTime - T.leadTime).toFixed(1), Q.leadTime, (Q.leadTime - P.leadTime).toFixed(1)); } const D = [-32, 10, 9, 16, 10, 12]; const K = { "baseline (window 20)": T, "bottleneck: writing 10->5": measure({ process: { writing: 5 } }), "non-bottleneck: analysis 4->2": measure({ process: { analysis: 2 } }), "window 20->5": P, "window 5 + writing 10->5": measure({ window: 5, process: { writing: 5 } }) }; console.log("\n4. four delivery metrics and value-added share"); print(D, "run", "lead time", "deploy", "change failure", "recovery", "value added"); for (const [ad, R] of Object.entries(K)) print(D, ad, R.leadTime, R.deployFrequency, R.changeFailureRate, R.recovery, R.valueAddedShare); const E = [-32, 16, 16]; console.log("\n5. where the wait sits"); print(E, "run", "longest wait", "total wait"); for (const [ad, R] of Object.entries(K)) print(E, ad, R.longestWaitStep, R.S.wait.reduce((s, v) => s + v, 0));
20 work items, 9 steps, release window 20 steps, seed 20260801; unfinished 0 1. value stream map (measured) step cap process avg. wait visits %C&A bounces wait request-queuing 1 1 0.0 28 - 0 0 analysis 1 4 1.0 48 83% 8 50 writing 2 10 8.6 61 70% 18 524 review 1 3 0.0 43 74% 11 0 integration-test 1 8 5.6 32 78% 7 178 acceptance-approval 1 2 0.0 25 92% 2 0 release-window 1 1 64.6 23 - 0 1485 production-setup 1 3 0.0 23 100% 0 0 production-verification 1 2 0.0 23 87% 3 0 2. the map's totals (baseline) avg. lead time (step) 182.0 process per item (step) 70.2 wait per item (step) 111.8 value-added share 38.5% step where the longest wait sits release-window rolled first-pass yield 20% total bounces 49 3. if a single step's process time is halved (at two window settings) improved step process window 20 change window 5 change (baseline) - 182.0 - 118.0 - analysis 4 -> 2 182.0 0.0 114.0 -4.0 writing 10 -> 5 180.0 -2.0 86.0 -32.0 review 3 -> 1 182.0 0.0 115.5 -2.5 integration-test 8 -> 4 180.0 -2.0 106.0 -12.0 acceptance-approval 2 -> 1 182.0 0.0 122.0 4.0 production-setup 3 -> 1 180.0 -2.0 117.0 -1.0 production-verification 2 -> 1 181.0 -1.0 118.0 0.0 4. four delivery metrics and value-added share run lead time deploy change failure recovery value added baseline (window 20) 182.0 4.5 15% 213.3 38.5% bottleneck: writing 10->5 180.0 4.5 15% 213.3 30.5% non-bottleneck: analysis 4->2 182.0 4.5 15% 220.0 35.9% window 20->5 118.0 6.0 15% 130.0 59.4% window 5 + writing 10->5 86.0 6.7 15% 73.3 63.8% 5. where the wait sits run longest wait total wait baseline (window 20) release-window 2237 bottleneck: writing 10->5 release-window 2502 non-bottleneck: analysis 4->2 release-window 2333 window 20->5 writing 957 window 5 + writing 10->5 integration-test 622
The numbers are of the measurement kind; their inputs are the assumptions above. The model defines value-added as “process,” and rework counts as process too; that is why 38.5% is a ceiling value.
What is Inside Lead Time
Average lead time is 182.0 steps: 70.2 steps of process, 111.8 steps of wait. The value-added share is 38.5% — an item spends two-thirds of the path standing still. Rolled first-pass yield is more striking: only four of the twenty items, that is 20%, get through the flow without ever bouncing back. The remaining sixteen items produce a total of 49 bounces.
The first table distributes these 49 bounces across the steps: writing 18, review 11, analysis 8, integration test 7, production verification 3, acceptance approval 2. The visit counts are the result of this — twenty items visit the writing step 61 times.
The distribution of wait, however, is not even. Five of the nine steps have zero wait. Of the total 2237 steps of wait, 1485 sit in a single step: the release window. That is 64.6 steps per visit. The second-largest pileup is at the writing step (524 steps, 8.6 per visit). Where the wait sits is not where the work gets done.
Why Improving a Step Does not Show
The third table halves every step’s process time and measures the change in total lead time. With the release window at 20 steps, the result is uniform: no improvement gains more than 2 steps. Cutting analysis from 4 to 2 gains 0.0 steps, cutting writing from 10 to 5 gains 2.0 steps. Halving a ten-step piece of work does not change even one percent of the 182-step lead time, because the time gained is waited back in the release window.
Once the window is cut to 5 steps, the same improvements pull apart. Writing dropping from 10 to 5 shortens lead time by 32.0 steps; integration test dropping from 8 to 4 gains 12.0; analysis dropping from 4 to 2 gains only 4.0. Cutting acceptance approval from 2 to 1, on the other hand, lengthens lead time by 4.0 steps — items arrive at the bottleneck more often and more bunched up. An improvement outside the bottleneck is not merely invisible; it can even flip sign.
The Bottleneck Shifts
The fifth table tracks, run by run, where the longest wait sits. At baseline it is the release window. Once the window is cut to 5, it shifts to writing (total wait falls from 2237 to 957). Once writing is also halved, it shifts to integration test (622). Three runs, three separate steps.
The fourth table gives the delivery-metric counterpart of this. The change failure rate is 15% in all five runs; since the percent complete and accurate values never change, this metric never moves. Deployment frequency climbs from 4.5 to 6.0, then to 6.7; time to restore falls from 213.3 to 130.0, then to 73.3 steps; the value-added share rises from 38.5% to 59.4%, then to 63.8%. Against that, in the run that improves only the bottleneck, the value-added share falls from 38.5% to 30.5%: process time got shorter, wait stayed exactly where it was.
Where the Difference Hides
In this lesson, the difference hides in the distribution of wait, and its number is this: five of the nine steps have zero wait, and one holds 66% of the total. The declared part is the process times — anyone looking at the map reads that writing takes 10 steps and integration test takes 8. The undeclared part is the wait; it is not an input, it is only born in the run, and it is what decides the improvement call.
How many steps it takes the signal to reach whom also reads from the bounce targets. A drift found at integration test returns two steps back to writing, and 7 of the bounces come from there. If the same class of drift is found at production verification, it returns six steps back to writing; the time to restore for those 3 items is 213.3 steps. The signal is the same; what is expensive is the length of the path back.
Summary
- In a nine-step flow, average lead time is 182.0 steps: 70.2 steps of process, 111.8 steps of wait; the value-added share is 38.5% (since rework counts as process too, this is a ceiling value).
- Only 20% of the twenty items get through without ever bouncing back; of the 49 bounces, 18 come from writing, 11 from review, 8 from analysis.
- Of the total 2237 steps of wait, 1485 sit in a single step (the release window, 64.6 per visit); five of the nine steps have zero wait.
- With the release window at 20 steps, no single-step improvement shortens lead time by more than 2 steps; once the window is cut to 5, writing gains 32.0 steps, while acceptance approval loses 4.0 steps.
- The bottleneck shifts to three separate steps across three runs; deployment frequency goes 4.5 → 6.0 → 6.7 and time to restore goes 213.3 → 130.0 → 73.3 steps, while the change failure rate stays at 15% across all five runs.
Next Step
This lesson read the four metrics as a result: the map changed, and the metrics followed. But the metrics themselves are a set, and they move one another. The next lesson runs the four delivery metrics — deployment frequency, lead time, change failure rate, time to restore — as a linked set: the change each of the four measures for shrinking batch size, adding a quality gate, and automatic rollback is written down, and the decisions that improve one metric while breaking another are named.
To keep your progress and take notes, Log in
My notes
Log in to take notes.