Track 10 · Discovery & handoverLesson 01 of 12
Shadowing the work as it's really done
Observe the process, not the documentation of it.
- The FDE path · Stage 1 of 6
- 45 min read
- 75 min hands-on
What you'll be able to do
01- You can explain why shadowing and interviews produce different evidence, and choose between them.
- You can plan a shadowing session that respects consent, privacy, works council and union sensitivities, NDAs and screen-recording rules.
- You can capture observations that become an exception catalogue, swimlane map, SIPOC and timed baseline.
- You can run a first-pass process-mining analysis on an event log (variants, rework loops, handoffs, wait times) and state what it cannot see.
- You can turn triangulated observations into evidence-backed candidate agent tasks and defend the ranking.
- You can diagnose observer effects and sampling bias in your own discovery evidence.
Before you start
- Finding where the data really lives: which systems hold the data operators touch.
- Automating systems with no API: the legacy screens you will watch people re-key into.
- Agent or workflow? Choosing the autonomy level: vocabulary for what a candidate task should be.
- Finding and masking personal data: handling customer data on operators' screens.
- Delivering through an SI: read early; the partner often controls who you may shadow.
Starting here? This is the first lesson on the core path, and you can read it without the prerequisites above: they add depth later, and the path reaches source discovery (Finding where the data really lives) two lessons from now. Two words used below: an exception is a case the normal process can't finish, such as an invoice that doesn't match its booking, so an operator resolves it by hand; an event log has one row per thing that happened to a case (which case, what happened, when), and the section on process mining explains it. Most examples follow Halden Freight, a fictional freight forwarder whose exception desk is the course's running engagement.
The short version
Documentation describes the process someone intended. Interviews describe the process people remember. Shadowing shows the process people actually run, and event logs show the part of it systems record. You sit beside operators as an apprentice, with consent and clear capture rules, and record steps, systems, swivel-chair moments, workarounds, exceptions, waits and tacit decisions. Then you test those observations against months of event-log data. Where the SOP, the operators and the log disagree, you have found the requirement. Only then do you propose candidate agent tasks.
- Said is not done. People describe the SOP; they perform the workarounds.
- Prepare like a security review. Consent, purpose, minimisation, NDA, recording rules and staff representatives come before session one.
- Capture evidence, not impressions. Every finding gets an observation ID or a reproducible query.
- Process mining is the second witness. Logs show volume, variants, rework and waits, but not spreadsheets, phone calls or touch time.
- Candidate tasks come last, each backed by evidence.
Mental model
04Think of three maps of the same territory. The SOP map is the process as documented: tidy, usually out of date. The observed map is the process as you watched it: rich in workarounds, but from a handful of sessions. The recorded map is reconstructed from event logs: large-sample and timestamped, but blind to anything off-system. No single map is the truth. You triangulate, and the gaps between the maps are your findings.
flowchart TB
SOP["SOP / BPMN / SI current-state map<br/>(said: intended process)"]
OBS["Shadowing notes, timed cases, work sampling<br/>(done: small sample, rich context)"]
LOG["Event log process mining<br/>(recorded: large sample, system-only)"]
SOP --> GAP{"Triangulate:<br/>where do they disagree?"}
OBS --> GAP
LOG --> GAP
GAP --> CAT["Exception catalogue<br/>swimlane map, SIPOC"]
GAP --> GAPS["Said-vs-done gap list<br/>(each with evidence ID)"]
GAP --> BASE["Manual timed baseline<br/>(feeds concept 05)"]
CAT --> CAND["Candidate agent tasks<br/>(feeds concepts 02 and 03)"]
GAPS --> CANDThe ordering matters. If you go straight from the SOP to candidate tasks, you automate a process nobody runs.
Core concepts
05Shadowing vs interviews: said vs done
Interviews are efficient and are the right tool for goals, constraints, politics and history. They are poor evidence for how work is performed. Skilled people no longer notice the steps they do without thinking. They describe the official process because it is the safe answer to an outsider whose project might affect their job. And memory flattens: the exception that consumed Tuesday afternoon becomes "sometimes there are issues."
Shadowing observes the work while it happens: the second monitor with the carrier portal, the sticky note with a shipper's tax ID, the WhatsApp message on a personal phone, the three minutes spent copying an invoice number between screens. The cost is access: a 90-minute session covers perhaps 10 to 20 cases, and each needs consent and a willing operator.
| Question you need answered | Better evidence |
|---|---|
| What is the business trying to achieve, and what is the constraint? | Interviews with sponsor and ops lead |
| What steps are performed, in what order, with which systems? | Shadowing, then event logs for volume |
| How long does each step take a person (touch time)? | Timed observation or operator self-logging |
| How long do cases wait between steps (elapsed time)? | Event logs |
| How often does each exception type occur? | Event logs, checked against a sample of cases |
| What judgement does an expert apply, and on what cues? | Shadowing with think-aloud, then a follow-up interview |
| What happens off-system (spreadsheets, phone, chat)? | Shadowing only |
Rule of thumb: ask about goals and the last time something went wrong; watch for how the work is done. Finding the real requirement pairs the two.
Contextual inquiry: the master–apprentice model
Contextual inquiry, the field-research step of Hugh Beyer and Karen Holtzblatt's Contextual Design method, gives shadowing a structure. Its central idea is the master–apprentice relationship. You arrive without a questionnaire, sit with the operator during real work, and learn it as an apprentice learns a craft: watching, asking "why did you do that?" as it happens, and checking your understanding aloud. Four principles:
- Context. Watch live work, not a demonstration. Ask about the case on the screen, not cases in general.
- Partnership. The operator is the expert. Steer gently towards your focus, but let them lead.
- Interpretation. Say your interpretation back: "So you check the portal first because SAP is a day behind?" The correction is often the best data point of the session.
- Focus. Know what you are looking for (here, exception handling on two lanes) without closing off surprises.
Two practical moves help: think-aloud (the operator narrates what and why, until it gets busy) and retrospective probing (after a burst, revisit a case and ask about its decision points). Avoid leading questions ("Wouldn't it be easier if...?"). You are there to learn, not to pitch.
Preparing to shadow: consent, privacy and the people in the room
Watching someone work is a form of monitoring, whatever your intent. Prepare with the discipline of a security review.
Purpose and consent. Write a one-paragraph purpose statement: what you observe, why, what you record, who sees it, how long you keep it, and what it will not be used for (usually individual performance evaluation). Operators must be able to decline without consequence, and their manager should not be the one asking. Agree it with the sponsor, HR and, where relevant, the partner's account team.
Privacy and data minimisation. Screens show customer and personal data. Decide in advance not to write down names, contact details, account numbers or anything you do not need; describe the case type instead. The UK ICO's guidance on monitoring workers is a good model for proportionality and transparency even outside the UK. For health or financial data (Carevia's PHI, Northvale's accounts), agree with the privacy office whether observation is permitted at all.
Works councils and unions. In many European countries, employee representatives have formal rights over monitoring. Germany's Works Constitution Act gives the works council co-determination over introducing and using technical devices designed to monitor employees' behaviour or performance (section 87(1) no. 6). The Federal Labour Court reads "designed to monitor" broadly, as covering devices that are objectively capable of monitoring, so process-mining or task-mining over actions by named users is generally treated as in scope. Take advice before analysing event logs by individual resource. Elsewhere, collective agreements may cover time studies. Even without a legal requirement, discovery that looks like a redundancy study poisons the engagement.
NDA and contract. Confirm the NDA and SOW cover observation of operational data. Through an SI, check whose paper governs your presence on site; the partner's account lead may need to agree the sessions.
Screen recording rules. Recording gives accurate timings but captures everything on screen, including data you are not entitled to, and worsens observer effects. Default to notes only. Record only with written consent, a defined retention period, storage in the customer's tenancy and privacy-office sign-off. This week's Halden brief prohibits it outright.
Logistics. Cover the work's rhythm: a normal morning and a burst (for Halden, the hours before port cut-off). Ask for operators of differing experience, not just the ops lead's pick. Agree how notes are reviewed before they leave the room.
What to capture
Write observations as structured records with IDs that later artefacts can cite.
| Capture | What it means | Halden example |
|---|---|---|
| Steps | Each discrete action, in order, with the trigger that started it | "Opens shared mailbox item, reads customs hold notice" |
| Systems touched | Every application, file or device, including unofficial ones | SAP GUI, carrier portal, Outlook, Excel "known bad shippers", WhatsApp on phone |
| Swivel-chair moments | Data read in one system and re-keyed into another by a person | Invoice number and declared value copied from PDF to SAP and to the carrier portal |
| Workarounds | Steps that exist because the official path fails or is too slow | Skips triage for shippers on the spreadsheet; goes straight to requesting documents |
| Exceptions | Cases that leave the main path, with how they were spotted | Customs query on resubmission; shipper sends the wrong document |
| Waits | Time a case sits idle, and what it is waiting for | Waiting on shipper response; waiting for the senior operator who knows HS codes |
| Decision points | Where a person chooses between paths, and the cues they use | "If the consignee is in the free zone, I check the second invoice first" |
| Tacit knowledge | Expertise nobody wrote down, often revealed by "you just know" | Recognising a shipper's habitual invoice format errors |
A minimal record, stored in the customer's tenancy, not on your laptop:
id: OBS-11
session: S1 (São Paulo, normal morning, operator P2, consented)
case_type: commercial_invoice_mismatch
trigger: customs hold email arrives in shared mailbox
steps:
- opens Excel "known bad shippers", finds shipper -> skips triage form
- drafts document request from a personal template in Portuguese
systems: [Outlook, Excel (local copy), SAP GUI]
swivel_chair: shipper reference copied from email into SAP search
workaround: triage skipped for listed shippers (SOP 4.2 requires triage)
wait: none observed
decision_point: "listed shipper means invoice is wrong, no need to check"
tacit: list maintained by P2 and one colleague; not shared with Jakarta desk
timing: 4 min 10 s touch time (stopwatch)
notes_reviewed_by: Rafaela, end of sessionWork sampling and time-and-motion
Concept 05 needs numbers on how long the work takes a person, and event logs record elapsed time, not touch time. Two classical industrial-engineering methods fill the gap.
Time-and-motion (timed observation). Time each case, ideally by element (read, look up, re-key, write), with a stopwatch or simple form. This produces the manual timed baseline that Baselines and ROI you can defend uses for touch time per exception type. As a heuristic, aim for 20 to 30 timed cases per exception type you intend to baseline; fewer and the spread swamps the mean. Record sample size, dates and observer.
Work sampling. Record what an operator is doing at random instants, then estimate the share of time in each category: exceptions, re-keying, searching, waiting for a system, chasing by phone. It is less intrusive than continuous timing and covers a whole team. The sample size for estimating a proportion p within a margin e at 95% confidence is approximately:
n = 1.96² × p × (1 − p) / e²
If you expect re-keying to be about 30% of time and want ±5 points, n ≈ 3.84 × 0.3 × 0.7 / 0.0025 ≈ 323 observations. Spread them across days and shifts.
A third option, operator self-logging of start and end times for a fortnight, scales well and suffers least from observer presence. It needs the same consent and works council care.
Artefacts: swimlane maps, exception catalogue, SIPOC
Swimlane map of the as-done process. One lane per actor, every hand-over between lanes visible, annotated where it departs from the SOP. Swivel-chair loops and off-system steps should be impossible to miss.
flowchart LR
subgraph SHIP["Shipper"]
S1["Sends corrected invoice<br/>(median wait ~20 h)"]
end
subgraph OP["Exception desk operator"]
O1["Reads customs hold<br/>in shared mailbox"] --> O2{"Shipper on<br/>'known bad' sheet?"}
O2 -- "no (SOP path)" --> O3["Triage form"]
O2 -- "yes (workaround)" --> O4["Request documents"]
O3 --> O4
O5["Re-key invoice fields<br/>into SAP (swivel chair)"] --> O6["Resubmit to customs"]
O7["WhatsApp update from<br/>personal phone (off-system)"]
end
subgraph SYS["SAP S/4HANA and carrier portal"]
Y1["Shipment record<br/>(read via nightly extract)"]
end
subgraph CUS["Customs"]
C1{"Accepts?"}
end
O4 --> S1 --> O5
O5 -.-> Y1
O6 --> C1
C1 -- "query (rework loop)" --> O4
C1 -- "released" --> O7Exception catalogue. One row per exception type: detection, share of volume (from the log), handling, workarounds, decision owner and evidence. Concepts 02, 03 and 04 read this table directly.
| ID | Exception type | Detected by | Share of volume (log) | Handling as done | Workarounds seen | Decision owner | Evidence |
|---|---|---|---|---|---|---|---|
| EX-01 | Commercial invoice mismatch, customs hold | Customs hold email | ~50% | Request corrected invoice, re-key in SAP, resubmit | Skip triage for listed shippers; interim WhatsApp updates | Operator; senior operator for HS questions | OBS-07, OBS-11, Q-03 |
| EX-02 | Carrier delay | Carrier portal status | ~20% | Check portal, notify customer | Portal checked before SAP because SAP lags a day | Operator | OBS-04, Q-01 |
| EX-03 | Missing documents | Pre-alert check | ~18% | Request documents, notify | Personal email templates per language | Operator | OBS-09 |
| EX-04 | Wrong HS code | Customs query or broker | ~12% | Reclassify, re-key, resubmit | Routed informally to one senior operator | Senior operator | OBS-19 |
(Q-numbers are saved event-log queries. Shares here come from the synthetic log in the worked example.)
SIPOC (Suppliers, Inputs, Process, Outputs, Customers) is a scoping device. It forces agreement on where the process starts and ends, and lists the inputs that Feasibility and data access, fast must check.
| Suppliers | Inputs | Process (5–7 steps) | Outputs | Customers |
|---|---|---|---|---|
| Shipper, carrier, customs, SAP | Hold notice, commercial invoice, booking data, HS code | Detect → triage → request documents → correct record → resubmit → release → notify | Released shipment, corrected SAP record, customer update | Consignee, Halden account manager, finance (demurrage) |
Process mining as complementary evidence
Process mining reconstructs how cases flow from the records systems leave behind. The minimum input is an event log: one row per event with a case ID (which process instance), an activity (what happened) and a timestamp (when). A resource column (who or what did it) is needed for handoff analysis; other attributes (exception type, lane) let you slice. The IEEE Task Force on Process Mining's manifesto sets out the foundations. From those columns you get:
- Variants: distinct activity sequences. Usually a few frequent variants and a long tail; the tail is where exceptions and data-quality problems live.
- Rework loops: an activity repeated within a case. Often the strongest signal for an agent task, because it is effort spent twice.
- Wait and throughput times: elapsed time between events and end to end. These locate the queues.
- Handoffs: changes of resource between steps. Each one tends to put the case back in a queue.
- Conformance checks: whether cases follow the documented model, turning "the SOP says triage every case" into a number.
Choosing the case notion is a design decision. A Halden exception touches a shipment, several documents and a customs declaration; with "exception" as the case, you lose the fact that one shipment can raise three exceptions. Object-centric process mining relates each event to several objects of different types, and OCEL 2.0 is the open standard for those logs. Start case-centric and note where the case notion distorts the picture.
Celonis. As of September 2026, Celonis markets the Celonis Process Intelligence Platform: a Process Intelligence Graph built with object-centric process mining, Data Core for zero-copy data-lake integration, and an Orchestration Engine for coordinating people, agents and automations (per its May 2026 press release). If the customer already runs Celonis, ask for read access to the existing data model first. Celonis Task Mining captures desktop interaction data such as clicks and application switches. It can show swivel-chair work at scale, but it is monitoring software on employees' machines, so every consent, privacy and works council point above applies, and its documentation says collection should be restricted and pseudonymised to suit local law.
Open-source and lightweight paths. PM4Py is a Python process-mining library maintained by Process Intelligence Solutions; as of September 2026 it is AGPL-3.0 (with a separate commercial licence) and supports Python 3.9 to 3.14. Check the licence before embedding it in a handover. For a first pass, SQL window functions over an event table (LAG(timestamp) OVER (PARTITION BY case_id ORDER BY timestamp)) give waits and transitions without new software. The worked example does the same in plain Python.
What logs cannot see. Spreadsheets, phone calls, WhatsApp messages and minutes spent reading a PDF. Timestamps also mislead: batch loads stamp hundreds of events at 02:00, time zones get mixed, and "completed" is clicked at shift end for the whole day. Validate a sample of cases end to end against what you observed.
From observations to candidate agent tasks
Only now do you ask what an agent could do. Write a candidate task card for each pattern: trigger, inputs, systems, decision, output, volume and exception rate, evidence IDs and a proposed autonomy level. For anything consequential the default is that the agent proposes and a human approves.
| Observed pattern | Typical candidate | Usual autonomy starting point |
|---|---|---|
| Swivel-chair re-keying between systems | Extract fields, propose the update for approval | Agent drafts; operator approves the write |
| Long waits on an external party | Validate inputs on arrival so they are right first time; chase earlier | Agent checks and drafts; operator sends |
| Rework loops after rejection | Pre-submission check against the rejection reasons seen in the log | Agent flags; operator decides |
| Decision points that rely on tacit knowledge | Surface the cues and similar past cases to the operator | Assistive only; keep the decision human |
| Off-system workarounds (spreadsheets, chat) | Often a governance finding first, a feature second | Fix the control gap before automating it |
| Rare, high-judgement exceptions | Leave alone for now | Human |
Rank candidates on evidence (where elapsed time, rework and handoffs concentrate), then score them in Picking the wedge use case. Volume alone is not a reason: if the wait is a shipper replying to an email, the agent cannot make the shipper faster, but it may stop the second round trip.
Observer effects and other biases
Being watched changes behaviour: people perform the SOP, hide workarounds and work more carefully, and so more slowly. This is often called the Hawthorne effect, though the original studies' conclusions are contested. Mitigations:
- Habituation. Discard or down-weight the first 20 to 30 minutes of a session.
- Repeat visits. A second session is usually more candid than the first.
- Triangulation. If the log shows 7% of cases skip triage and nobody skipped it while you watched, that is a finding about the observation.
- Selection bias. The ops lead offers the best operator on a quiet day. Ask for a newer operator, a burst period, a second site.
- Your own bias. Hoping for agent tasks, you notice re-keying and miss judgement. Note what surprised you; have someone outside the room review your notes.
Worked example
06The engagement pack gives you a 90-day event log from Halden's exception desk for the two candidate lanes. The script below generates a synthetic version (about 4,900 cases, roughly 380 a week) and analyses it. Point it at a real CSV extract and it analyses that instead. It computes variants, rework loops, handoffs and waits, runs two SOP conformance checks, and ranks candidate automation points.
Requirements: Python 3.11+, standard library only. Runs offline with no keys.
"""Tiny process-mining pass over an exception-desk event log.
Standard library only; runs offline. With no arguments it generates a
synthetic 90-day Halden Freight log (two lanes, ~380 exceptions a week).
Pass a CSV path with columns case_id, activity, timestamp, resource to
analyse a real extract instead.
"""
from __future__ import annotations
import csv
import io
import random
import statistics
import sys
from collections import Counter, defaultdict
from dataclasses import dataclass
from datetime import datetime, timedelta
from pathlib import Path
REQUIRED_COLUMNS = ("case_id", "activity", "timestamp", "resource")
OPERATORS = [f"OP-{i:02d}" for i in range(1, 9)]
EXTERNAL = {"shipper": "EXT-SHIPPER", "customs": "EXT-CUSTOMS", "system": "SYSTEM"}
# Mean wait in hours BEFORE each activity starts (synthetic, not real data).
MEAN_WAIT_H = {
"Triage": 3.0, "Request docs from shipper": 1.0, "Docs received": 30.0,
"Re-key invoice in SAP": 4.0, "Customs resubmission": 2.0,
"Customs query": 20.0, "Hold released": 18.0, "Check carrier portal": 2.0,
"Notify customer": 1.5, "Case closed": 6.0,
}
HANDOFF_PENALTY_MEAN_H = 5.0 # extra queue time when a different operator picks the case up
# Ranking weights are a judgement call. Change them and say so in the discovery pack.
W_WAIT, W_REPEAT, W_HANDOFF = 0.4, 0.3, 0.3
@dataclass(frozen=True)
class Event:
case_id: str
activity: str
timestamp: datetime
resource: str
# ---------------------------------------------------------------- synthetic log
def _steps_for(kind: str, rng: random.Random) -> list[tuple[str, str]]:
"""(activity, actor kind) for one case. Actor kind: op, shipper, customs, system."""
steps = [("Exception created", "system")]
if kind == "invoice_mismatch":
if rng.random() >= 0.15: # 15% skip triage: the "known bad shipper" shortcut
steps.append(("Triage", "op"))
fix = [("Request docs from shipper", "op"), ("Docs received", "shipper"),
("Re-key invoice in SAP", "op"), ("Customs resubmission", "op")]
steps += fix
while rng.random() < 0.35: # customs rejects the resubmission: rework loop
steps += [("Customs query", "customs"), *fix]
if rng.random() < 0.10: # customer chased, operator sends an interim update
steps.append(("Notify customer", "op"))
steps += [("Hold released", "customs"), ("Notify customer", "op")]
elif kind == "carrier_delay":
steps += [("Triage", "op"), ("Check carrier portal", "op"), ("Notify customer", "op")]
elif kind == "missing_docs":
steps += [("Triage", "op"), ("Request docs from shipper", "op"), ("Docs received", "shipper")]
if rng.random() < 0.20: # shipper sends the wrong document first time
steps += [("Request docs from shipper", "op"), ("Docs received", "shipper")]
steps.append(("Notify customer", "op"))
else: # wrong_hs_code
steps += [("Triage", "op"), ("Check carrier portal", "op"), ("Re-key invoice in SAP", "op"),
("Customs resubmission", "op"), ("Hold released", "customs"), ("Notify customer", "op")]
return steps + [("Case closed", "op")]
def synthetic_log_csv(seed: int = 7, days: int = 90, cases_per_week: int = 380) -> str:
rng = random.Random(seed)
start = datetime(2026, 5, 4, 0, 0)
kinds = ["invoice_mismatch", "carrier_delay", "missing_docs", "wrong_hs_code"]
mix = [0.50, 0.20, 0.18, 0.12]
out = io.StringIO()
writer = csv.writer(out)
writer.writerow(REQUIRED_COLUMNS)
for n in range(days * cases_per_week // 7):
case_id = f"EXC-{n + 1:05d}"
ts = start + timedelta(minutes=rng.uniform(0, days * 24 * 60))
owner = rng.choice(OPERATORS)
last_op = ""
for activity, actor in _steps_for(rng.choices(kinds, mix)[0], rng):
if actor == "op":
resource = owner if rng.random() < 0.7 else rng.choice(OPERATORS)
else:
resource = EXTERNAL[actor]
if activity != "Exception created":
wait = rng.expovariate(1 / MEAN_WAIT_H[activity])
if actor == "op" and last_op and resource != last_op:
wait += rng.expovariate(1 / HANDOFF_PENALTY_MEAN_H)
ts += timedelta(hours=wait)
writer.writerow([case_id, activity, ts.isoformat(timespec="minutes"), resource])
if actor == "op":
last_op = resource
return out.getvalue()
# ---------------------------------------------------------------- loading
def load_events(text: str) -> list[Event]:
reader = csv.DictReader(io.StringIO(text))
missing = [c for c in REQUIRED_COLUMNS if c not in (reader.fieldnames or [])]
if missing:
raise ValueError(f"event log is missing columns: {missing}")
events: list[Event] = []
for line_no, row in enumerate(reader, start=2):
case_id, activity = row["case_id"].strip(), row["activity"].strip()
if not case_id or not activity:
raise ValueError(f"line {line_no}: empty case_id or activity")
try:
ts = datetime.fromisoformat(row["timestamp"].strip())
except ValueError as exc:
raise ValueError(f"line {line_no}: bad timestamp {row['timestamp']!r}") from exc
events.append(Event(case_id, activity, ts, row["resource"].strip() or "UNKNOWN"))
return events
def build_traces(events: list[Event]) -> dict[str, list[Event]]:
by_case: dict[str, list[Event]] = defaultdict(list)
for e in events:
by_case[e.case_id].append(e)
for trace in by_case.values():
trace.sort(key=lambda e: e.timestamp) # stable: ties keep file order
return dict(by_case)
# ---------------------------------------------------------------- analysis
def p90(values: list[float]) -> float:
return statistics.quantiles(values, n=10)[-1] if len(values) >= 2 else values[0]
def is_operator(resource: str) -> bool:
return resource.startswith("OP-")
def analyse(traces: dict[str, list[Event]]) -> None:
n_cases = len(traces)
n_events = sum(len(t) for t in traces.values())
first = min(t[0].timestamp for t in traces.values())
last = max(t[0].timestamp for t in traces.values())
print(f"Cases: {n_cases} Events: {n_events} Opened: {first:%Y-%m-%d} to {last:%Y-%m-%d}\n")
# 1. Variants: distinct activity sequences, most frequent first.
variants = Counter(tuple(e.activity for e in t) for t in traces.values()).most_common()
print(f"Distinct variants: {len(variants)}. Variants covering 80% of cases:")
covered = 0
for rank, (seq, count) in enumerate(variants, start=1):
covered += count
print(f" V{rank:<2} {count:5d} ({count / n_cases:5.1%}, cum {covered / n_cases:5.1%}) "
f"{len(seq)} steps: {' > '.join(seq[1:-1])}")
if covered / n_cases >= 0.80:
break
# 2. Rework: an activity executed more than once in the same case.
repeats: Counter[str] = Counter()
rework_cases = 0
for t in traces.values():
extra = {a: c - 1 for a, c in Counter(e.activity for e in t).items() if c > 1}
if extra:
rework_cases += 1
repeats.update(extra)
print(f"\nRework: {rework_cases} cases ({rework_cases / n_cases:.1%}) repeat at least one activity")
for activity, count in repeats.most_common(4):
print(f" {activity:<28} {count:5d} extra executions")
# 3. Handoffs (the next human to touch the case is not the last human who did)
# and waits per transition (elapsed time between consecutive events).
handoffs_into: Counter[str] = Counter()
handoffs_per_case: list[int] = []
waits: dict[tuple[str, str], list[float]] = defaultdict(list)
wait_before: dict[str, float] = defaultdict(float)
same_op_wait: list[float] = []
new_op_wait: list[float] = []
for t in traces.values():
n_handoffs, last_op = 0, ""
for prev, cur in zip(t, t[1:]):
hours = (cur.timestamp - prev.timestamp).total_seconds() / 3600
waits[(prev.activity, cur.activity)].append(hours)
wait_before[cur.activity] += hours
if is_operator(cur.resource):
if last_op and cur.resource != last_op:
n_handoffs += 1
handoffs_into[cur.activity] += 1
if is_operator(prev.resource):
new_op_wait.append(hours)
elif last_op and is_operator(prev.resource):
same_op_wait.append(hours)
last_op = cur.resource
handoffs_per_case.append(n_handoffs)
with_handoff = sum(1 for h in handoffs_per_case if h)
print(f"\nHandoffs: mean {statistics.mean(handoffs_per_case):.2f} per case; "
f"{with_handoff / n_cases:.1%} of cases change operator at least once")
print(f" Median wait, operator step to same operator: {statistics.median(same_op_wait):5.1f} h")
print(f" Median wait, operator step to different operator: {statistics.median(new_op_wait):5.1f} h")
# SOP conformance spot checks: the 2019 SOP says every case is triaged first
# and the customer is notified once, at resolution.
no_triage = sum(1 for t in traces.values() if all(e.activity != "Triage" for e in t))
multi_notify = sum(1 for t in traces.values()
if sum(e.activity == "Notify customer" for e in t) > 1)
print(f"\nSOP checks: {no_triage} cases ({no_triage / n_cases:.1%}) never triaged; "
f"{multi_notify} cases ({multi_notify / n_cases:.1%}) notify the customer more than once")
print("\nTransitions by total wait (where the elapsed time goes):")
by_total = sorted(waits.items(), key=lambda kv: sum(kv[1]), reverse=True)
for (a, b), hs in by_total[:6]:
print(f" {a} > {b}: n={len(hs)}, median {statistics.median(hs):.1f} h, "
f"p90 {p90(hs):.1f} h, total {sum(hs):,.0f} h")
# 4. Candidate automation points: activities ranked on normalised signals.
actor_of: dict[str, Counter[str]] = defaultdict(Counter)
for t in traces.values():
for e in t:
actor_of[e.activity]["op" if is_operator(e.resource) else e.resource] += 1
activities = [a for a in wait_before if a != "Case closed"]
max_wait = max(wait_before[a] for a in activities) or 1.0
max_rep = max((repeats[a] for a in activities), default=0) or 1
max_hand = max((handoffs_into[a] for a in activities), default=0) or 1
scored = []
for a in activities:
score = (W_WAIT * wait_before[a] / max_wait + W_REPEAT * repeats[a] / max_rep
+ W_HANDOFF * handoffs_into[a] / max_hand)
scored.append((score, a))
print("\nCandidate automation points (a list of places to look, not a decision):")
print(f" {'activity':<28}{'score':>6}{'wait-before h':>15}{'repeats':>9}{'handoffs':>10} done by")
for score, a in sorted(scored, reverse=True)[:6]:
who = actor_of[a].most_common(1)[0][0]
print(f" {a:<28}{score:6.2f}{wait_before[a]:15,.0f}{repeats[a]:9d}{handoffs_into[a]:10d} {who}")
print("\nNot visible in this log: touch time, re-keying effort, spreadsheets, WhatsApp, phone calls.")
print("Triangulate every candidate with shadowing observation IDs before it reaches the scorecard.")
def main(argv: list[str]) -> int:
if len(argv) > 1:
path = Path(argv[1])
if path.suffix.lower() != ".csv" or not path.is_file():
print(f"expected an existing .csv file, got {path}", file=sys.stderr)
return 2
text = path.read_text(encoding="utf-8")
else:
text = synthetic_log_csv()
try:
events = load_events(text)
except ValueError as exc:
print(f"cannot read event log: {exc}", file=sys.stderr)
return 2
if not events:
print("event log has no rows", file=sys.stderr)
return 2
analyse(build_traces(events))
return 0
if __name__ == "__main__":
raise SystemExit(main(sys.argv))Run it with python3 mine_exceptions.py. The seed is fixed, so everyone in the cohort gets the same numbers. The output, abridged (variant lines omit the first and last steps, "Exception created" and "Case closed", which every case shares):
Cases: 4885 Events: 43547 Opened: 2026-05-04 to 2026-08-01
Distinct variants: 29. Variants covering 80% of cases:
V1 1238 (25.3%, cum 25.3%) 9 steps: Triage > Request docs from shipper > Docs received > Re-key invoice in SAP > Customs resubmission > Hold released > Notify customer
V2 993 (20.3%, cum 45.7%) 5 steps: Triage > Check carrier portal > Notify customer
V3 684 (14.0%, cum 59.7%) 6 steps: Triage > Request docs from shipper > Docs received > Notify customer
V4 549 (11.2%, cum 70.9%) 8 steps: Triage > Check carrier portal > Re-key invoice in SAP > Customs resubmission > Hold released > Notify customer
V5 475 ( 9.7%, cum 80.6%) 14 steps: Triage > Request docs ... > Customs query > Request docs ... > Hold released > Notify customer
Rework: 1199 cases (24.5%) repeat at least one activity
Handoffs: mean 2.25 per case; 79.7% of cases change operator at least once
Median wait, operator step to same operator: 1.6 h
Median wait, operator step to different operator: 6.3 h
SOP checks: 364 cases (7.5%) never triaged; 257 cases (5.3%) notify the customer more than once
Transitions by total wait (where the elapsed time goes):
Request docs from shipper > Docs received: n=4785, median 19.8 h, p90 69.3 h, total 141,241 h
Customs resubmission > Hold released: n=2793, median 12.1 h, p90 39.6 h, total 48,689 h
...
Candidate automation points (a list of places to look, not a decision):
activity score wait-before h repeats handoffs done by
Docs received 0.70 141,241 1442 0 EXT-SHIPPER
Re-key invoice in SAP 0.61 26,912 1284 1998 op
Request docs from shipper 0.60 14,437 1442 1956 op
Customs resubmission 0.58 18,442 1284 1961 op
Notify customer 0.41 19,021 257 2258 opHow to read it in the discovery pack:
- Five variants cover 80% of cases (README criterion AC1). V5, the customs rework loop, is nearly 10% of volume on its own. Sample a few tail variants by hand: some are real exceptions, others data-quality problems.
- A quarter of cases contain rework, mostly the customs loop on invoice mismatches (864 of the 1,199 rework cases; the rest are repeat document requests and second customer updates). That is effort spent twice and days added.
- Handoffs cost queue time. When a different operator takes the next step, the median wait rises from 1.6 h to 6.3 h. That is a routing finding as much as an automation one.
- The SOP checks quantify two said-vs-done gaps. 7.5% of cases are never triaged, matching OBS-11 (the "known bad shippers" spreadsheet). 5.3% notify the customer twice, a hint of the off-system WhatsApp updates the log cannot see.
- The top candidate is a wait you cannot automate. Shippers will not reply faster because of an agent. But checking the corrected invoice against the booking on arrival can prevent the second customs round trip. Re-keying, document requests and resubmission preparation are the operator-side candidates, matching the week's slice: classify, retrieve read-only, draft, queue for approval.
- The last two lines matter most. The ranking uses elapsed time. Touch time needs the manual timed baseline before concept 05 can price anything.
- The generator's rates are teaching parameters, not Halden's measurements. The customs loop is deliberately heavy (about 35% of invoice-mismatch cases) so it shows up as a top variant. Concept 05 baselines Halden's real 90-day extract at an 11.2% resubmission rate. When you baseline, use the log you actually have and state its source.
In the field
07What the customer says. Halden's VP of Global Operations wants "an AI agent that handles all exceptions end to end" and attaches the 2019 SOP as the process. The SI account lead says their SAP programme already produced current-state BPMN maps. Rafaela, who runs the São Paulo desk, allows two 90-minute sessions, notes only, which she reviews before you leave.
What is actually going on. Across a normal morning and the four hours before the Santos cut-off, you record 23 observations over 31 cases with two consenting operators. The ones that matter:
- OBS-07: invoice number, declared value and HS code are re-keyed from the PDF into SAP, then into the carrier portal; about 4 minutes of touch time per case.
- OBS-11: a local Excel list of "known bad shippers" lets operators skip the triage form. The log's 7.5% never-triaged figure matches it.
- OBS-14: during the burst, operators send interim customer updates on WhatsApp from personal phones. Nothing is logged or retained.
- OBS-19: HS-code queries wait for one senior operator who "just knows" two shippers' tariff patterns.
The SI's BPMN matches the SOP, not the log. The SAP integration is a nightly Snowflake extract, which is why operators check the carrier portal first.
What the FDE does. You present the BPMN as the "said" map beside the mined variants, without criticising it, and write the gap list with an evidence ID on every line. You raise OBS-14 privately with Rafaela and the sponsor as a records and data-protection issue, not a feature request. You pseudonymise the resource column before analysis leaves Halden's tenancy, and note that extending it to Halden's German operations needs works council consultation first. The top findings become three candidate task cards for concept 03, each marked "agent proposes, operator approves".
Stakeholder objection. Rafaela, at the start of session 1: "If this is to decide how many of my people you can replace, say so now."
The answer: "It is not, and the purpose statement you signed says so. I am recording how cases flow, not how fast anyone works. Nothing leaves this room without your review, and system data is analysed with operator IDs pseudonymised. I expect to find where your team does the same work twice. That is the part an agent can draft for them to approve." Then keep the promise: never show a per-operator chart.
What you say in the review. "Half the volume is one exception type: invoice mismatches causing customs holds. About one invoice-mismatch case in nine goes back through customs at least once, and each loop adds days. When the next step goes to a different operator, the case waits about five hours longer at the median. The longest wait is the shipper's reply, so the value is in getting the corrected invoice right first time and removing re-keying. Separately, customer updates go out on personal WhatsApp during bursts; that control gap closes before we automate any customer messaging. Our timed baseline covers 31 cases; we will reach 30 per exception type before we put a number on savings."
Decision guide
08| Situation | Do this | Avoid |
|---|---|---|
| You have an SOP but no access to operators yet | Treat the SOP as a hypothesis; request an event-log extract and a shadowing slot in parallel | Designing against the SOP |
| Event log exists, shadowing not permitted | Mine the log, then validate 10–20 cases end to end with an operator in a short interview | Presenting log findings as the whole process |
| Shadowing permitted, no usable event log | Shadow, add work sampling or operator self-logging for numbers, request a manual timestamp form | Extrapolating volumes from 30 observed cases |
| Works council, union or strong privacy regime | Engage HR and staff representatives first; aggregate, pseudonymise, notes only | Task mining or resource-level analysis before agreement |
| Screen recording requested by your own team | Default to no; record only with written consent, retention limits, storage in customer tenancy | Recording "just for accuracy" |
| Process spans several objects (shipment, documents, declaration) | Start case-centric; move to object-centric (OCEL 2.0 or Celonis OCPM) if the case notion distorts results | Forcing one case ID that splits real cases |
| Top-ranked candidate is a wait on an external party | Look for the upstream fix (validate on arrival, chase earlier) | Claiming the agent removes the wait |
Before leaving each session, ask: Which step today was unusual? What do you do when this system is down? What would a new starter get wrong?
Misconceptions
09Myth
“The SOP is the process”
The SOP is the process someone intended at the time it was written. Halden's predates its S/4HANA migration. Use it as the "said" map and test it.
Myth
“Process mining replaces shadowing”
Logs give volume, variants and elapsed time, but not off-system work, touch time or the judgement behind a decision. You need both witnesses, and interviews alone are weaker still.
Myth
“Shadow the best operator and you have the process”
The best operator has the most workarounds and the most tacit knowledge, and is the least typical. Sample a range of experience, sites and times, including bursts.
Myth
“Screen recording is more accurate, so always record”
It captures data you are not entitled to, needs consent and retention controls, raises works council issues and worsens observer effects. Notes plus timed samples usually suffice.
Myth
“The highest-volume step is the best automation candidate”
Volume without rework, waits or re-keying may already be efficient. And if the big wait belongs to a third party, an agent cannot remove it.
When it breaks
10| Symptom | Likely root cause | How you detect it | Fix / prevention |
|---|---|---|---|
| Agent works in UAT, operators bypass it in production | Built against the SOP; real workarounds and off-system steps not covered | Adoption metrics low; operators still using spreadsheets | Shadow before design; walk the swimlane map with operators before build |
| Claimed savings evaporate at review | Baseline used elapsed time from the log as if it were touch time | Finance asks how minutes saved were measured | Collect a manual timed baseline; keep elapsed and touch time separate |
| Staff representatives halt discovery mid-engagement | Monitoring sensitivities not addressed up front | Formal objection; sessions cancelled | Purpose statement, HR and works council or union engagement before session 1 |
| Variant analysis shows hundreds of variants, nothing actionable | Wrong case notion, or noisy activity labels and batch timestamps | Many variants with one case each; spikes of events at the same minute | Fix the case notion, map activity labels, exclude batch-loaded events, validate a sample |
| Sensitive data appears in discovery notes | No minimisation rules; notes taken on a personal device | Privacy review finds names or account numbers | Agreed capture rules, case-type descriptions, notes stored in the customer tenancy, reviewed before leaving |
| Tacit decision automated badly | Decision point recorded as a rule without its cues | High override rate on one decision type | Keep tacit decisions assistive; capture cues with think-aloud and retrospective probing |
Exercise
11Starter. The worked-example script; you are preparing the Halden discovery pack.
Steps (60–90 minutes):
- Run the script and save the output. Confirm you get 4,885 cases and 29 variants.
- Add a cycle-time analysis: median and p90 hours from first to last event, per variant, for the variants covering 80% of cases. Which variant has the worst p90?
- Add a third SOP check, for example "customs resubmission happens only after a re-key".
- Write five said-vs-done gaps: SOP statement, observed or recorded behaviour, evidence (a rerunnable query or an OBS ID from In the field).
- Draft two exception catalogue rows and two candidate task cards (trigger, inputs, systems, decision, output, volume, evidence IDs, proposed autonomy level).
- Write a half-page shadowing plan for the Jakarta desk: purpose statement, consent, capture rules, sessions (one in a burst), who reviews notes, what you will not record.
Done looks like:
- The script still runs offline and the cycle-time output reproduces from the fixed seed.
- Every gap and every task card cites evidence that a reviewer can trace (README acceptance criterion AC2).
- The plan names who consents, who reviews notes and what is excluded, with no screen recording.
- At least one candidate task is explicitly kept assistive because it depends on tacit knowledge.
Stretch. Reproduce the variant and wait analysis in PM4Py or in SQL (DuckDB or your warehouse). Then sketch the log object-centric, with shipment, commercial invoice and customs declaration as object types, and name one finding the case-centric view hides.
Check yourself
12An ops lead says in an interview that "every exception is triaged within an hour." What two pieces of evidence would you gather, and why both?
Your process-mining output ranks "Docs received" (a shipper action) as the top candidate. What do you tell the sponsor?
You plan to analyse an event log by named operator for a team in Germany. What must happen first?
Why is the manual timed baseline still needed when you have 90 days of timestamps?
Explain the master–apprentice model and one way it differs from a structured interview.
During a session the operator works noticeably by the book and slowly. What is happening, and what do you do?
Connections
13- Builds on: Finding where the data really lives, Automating systems with no API, Agent or workflow? Choosing the autonomy level
- Used by: Finding the real requirement, Picking the wedge use case, Feasibility and data access, fast, Baselines and ROI you can defend, Scoping a shippable slice, Handover, run state and the next use case
- Related: Finding and masking personal data, Approval gates and bounded autonomy, From requirements to eval criteria, Delivering through an SI
Further reading
14- Contextual Design, by Karen Holtzblatt and Hugh Beyer (Interaction Design Foundation encyclopedia): the method's authors on contextual inquiry and the master–apprentice relationship.
- Process Mining Manifesto (IEEE Task Force on Process Mining): guiding principles and open challenges, including event-log quality.
- OCEL 2.0 standard: the open standard for object-centric event logs.
- PM4Py on GitHub: the open-source Python process-mining library; read the licensing section before you embed it in a deliverable.
- Celonis: Leader in the 2026 Gartner Magic Quadrant for Process Intelligence (press release, May 2026): vendor material; read it for current product names, not claims.
- ICO: Employment practices and data protection, monitoring workers: a practical model for transparency and proportionality when observing staff.
- Works Constitution Act (BetrVG), official English translation: section 87 sets out the works council's co-determination rights, including over monitoring devices.