Track 10 · Discovery & handoverLesson 01 of 12

Shadowing the work as it's really done

Observe the process, not the documentation of it.

What you'll be able to do

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

Before you start

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

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

Mental model

04

Think 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.

Diagram
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 --> CAND

The ordering matters. If you go straight from the SOP to candidate tasks, you automate a process nobody runs.

Core concepts

Core concepts

05

Shadowing 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 answeredBetter 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.

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.

CaptureWhat it meansHalden example
StepsEach discrete action, in order, with the trigger that started it"Opens shared mailbox item, reads customs hold notice"
Systems touchedEvery application, file or device, including unofficial onesSAP GUI, carrier portal, Outlook, Excel "known bad shippers", WhatsApp on phone
Swivel-chair momentsData read in one system and re-keyed into another by a personInvoice number and declared value copied from PDF to SAP and to the carrier portal
WorkaroundsSteps that exist because the official path fails or is too slowSkips triage for shippers on the spreadsheet; goes straight to requesting documents
ExceptionsCases that leave the main path, with how they were spottedCustoms query on resubmission; shipper sends the wrong document
WaitsTime a case sits idle, and what it is waiting forWaiting on shipper response; waiting for the senior operator who knows HS codes
Decision pointsWhere 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 knowledgeExpertise 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:

yaml
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 session

Work 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.

Diagram
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" --> O7

Exception 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.

IDException typeDetected byShare of volume (log)Handling as doneWorkarounds seenDecision ownerEvidence
EX-01Commercial invoice mismatch, customs holdCustoms hold email~50%Request corrected invoice, re-key in SAP, resubmitSkip triage for listed shippers; interim WhatsApp updatesOperator; senior operator for HS questionsOBS-07, OBS-11, Q-03
EX-02Carrier delayCarrier portal status~20%Check portal, notify customerPortal checked before SAP because SAP lags a dayOperatorOBS-04, Q-01
EX-03Missing documentsPre-alert check~18%Request documents, notifyPersonal email templates per languageOperatorOBS-09
EX-04Wrong HS codeCustoms query or broker~12%Reclassify, re-key, resubmitRouted informally to one senior operatorSenior operatorOBS-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.

SuppliersInputsProcess (5–7 steps)OutputsCustomers
Shipper, carrier, customs, SAPHold notice, commercial invoice, booking data, HS codeDetect → triage → request documents → correct record → resubmit → release → notifyReleased shipment, corrected SAP record, customer updateConsignee, 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 patternTypical candidateUsual autonomy starting point
Swivel-chair re-keying between systemsExtract fields, propose the update for approvalAgent drafts; operator approves the write
Long waits on an external partyValidate inputs on arrival so they are right first time; chase earlierAgent checks and drafts; operator sends
Rework loops after rejectionPre-submission check against the rejection reasons seen in the logAgent flags; operator decides
Decision points that rely on tacit knowledgeSurface the cues and similar past cases to the operatorAssistive only; keep the decision human
Off-system workarounds (spreadsheets, chat)Often a governance finding first, a feature secondFix the control gap before automating it
Rare, high-judgement exceptionsLeave alone for nowHuman

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

Worked example

06

The 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.

python
"""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):

text
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  op

How 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

In the field

07
Field note

What 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

Decision guide

08
SituationDo thisAvoid
You have an SOP but no access to operators yetTreat the SOP as a hypothesis; request an event-log extract and a shadowing slot in parallelDesigning against the SOP
Event log exists, shadowing not permittedMine the log, then validate 10–20 cases end to end with an operator in a short interviewPresenting log findings as the whole process
Shadowing permitted, no usable event logShadow, add work sampling or operator self-logging for numbers, request a manual timestamp formExtrapolating volumes from 30 observed cases
Works council, union or strong privacy regimeEngage HR and staff representatives first; aggregate, pseudonymise, notes onlyTask mining or resource-level analysis before agreement
Screen recording requested by your own teamDefault to no; record only with written consent, retention limits, storage in customer tenancyRecording "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 resultsForcing one case ID that splits real cases
Top-ranked candidate is a wait on an external partyLook 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

Misconceptions

09

Myth

“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

When it breaks

10
SymptomLikely root causeHow you detect itFix / prevention
Agent works in UAT, operators bypass it in productionBuilt against the SOP; real workarounds and off-system steps not coveredAdoption metrics low; operators still using spreadsheetsShadow before design; walk the swimlane map with operators before build
Claimed savings evaporate at reviewBaseline used elapsed time from the log as if it were touch timeFinance asks how minutes saved were measuredCollect a manual timed baseline; keep elapsed and touch time separate
Staff representatives halt discovery mid-engagementMonitoring sensitivities not addressed up frontFormal objection; sessions cancelledPurpose statement, HR and works council or union engagement before session 1
Variant analysis shows hundreds of variants, nothing actionableWrong case notion, or noisy activity labels and batch timestampsMany variants with one case each; spikes of events at the same minuteFix the case notion, map activity labels, exclude batch-loaded events, validate a sample
Sensitive data appears in discovery notesNo minimisation rules; notes taken on a personal devicePrivacy review finds names or account numbersAgreed capture rules, case-type descriptions, notes stored in the customer tenancy, reviewed before leaving
Tacit decision automated badlyDecision point recorded as a rule without its cuesHigh override rate on one decision typeKeep tacit decisions assistive; capture cues with think-aloud and retrospective probing
Exercise

Exercise

11
Hands-on~75 min

Starter. The worked-example script; you are preparing the Halden discovery pack.

Steps (60–90 minutes):

  1. Run the script and save the output. Confirm you get 4,885 cases and 29 variants.
  2. 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?
  3. Add a third SOP check, for example "customs resubmission happens only after a re-key".
  4. Write five said-vs-done gaps: SOP statement, observed or recorded behaviour, evidence (a rerunnable query or an OBS ID from In the field).
  5. Draft two exception catalogue rows and two candidate task cards (trigger, inputs, systems, decision, output, volume, evidence IDs, proposed autonomy level).
  6. 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

Check yourself

12
1

An 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?

2

Your process-mining output ranks "Docs received" (a shipper action) as the top candidate. What do you tell the sponsor?

3

You plan to analyse an event log by named operator for a team in Germany. What must happen first?

4

Why is the manual timed baseline still needed when you have 90 days of timestamps?

5

Explain the master–apprentice model and one way it differs from a structured interview.

6

During a session the operator works noticeably by the book and slowly. What is happening, and what do you do?

Connections

Connections

13
Further reading

Further reading

14