Home/Unit 14 · The FDE operating system and capstone
Process mining to find AI opportunities
Read an event log for variants, rework, waiting time and touchless rate, find where a process really stalls, and turn the top delay into a measurable problem.
Every time someone creates, changes or releases a document in SAP, the system writes down what happened and when. Put those records in order for each business case, and you get an event log: order 1002001 was created at 03:29, its delivery was created at 04:05, it shipped the next morning and was invoiced at 22:00.
Process mining reads thousands of those logs at once. It shows the process as it really runs, not as the slide says it runs. It answers four questions with numbers:
Variants: how many different paths do cases take, and which are common?
Rework: which steps happen twice or more in the same case?
Waiting time: where do cases sit idle between steps, and for how long?
Touchless rate: what share of cases go through with no one stepping in?
For an FDE, that turns discovery from opinion into evidence. Interviews tell you what people think hurts. The event log tells you where the hours actually go. It also hands you a baseline from the system, before anyone proposes a solution.
Most AI requests point at the step people complain about. That step is often not where the time goes.
Take a made-up quarter at Calder Pumpworks, the fictional pump maker this course uses. The head of order management asks: "Can AI clear our credit blocks faster?" The event log of 600 sales orders gives a sharper picture:
About one order in four stops on a credit block. Those orders take nearly a day longer.
The wait from block to release is about 24 hours at the median. Blocks set on a Friday wait about 63 hours.
Most blocked orders come from distributors, and almost all are released in the end.
That changes the conversation. The pattern is a queue: blocks wait for a once-a-day review, and weekends add two days. A second review slot might fix most of it with no AI at all. If AI helps, the evidence says where: preparing each decision so the queue moves faster, not deciding for the analyst.
Three business wins come from mining first:
Less waste. You don't fund an AI project for a problem a schedule change fixes.
A real baseline. "24 hours at the median, from the system" is a number a sponsor can sign. "It feels slow" is not.
A way to measure later. The same log, rerun after go-live, shows whether the change worked.
The risk is reading too much into the log. It shows elapsed time, not effort, and it shows what happened, not why. Pair it with interviews and shadowing, as the discovery topic teaches.
SAP's process mining product is SAP Signavio Process Intelligence. As of 1 November 2026, its product page describes:
Connectors for SAP and non-SAP systems, with ready-made process flows and metrics.
Process Atoms: reusable business rules used to check whether cases conform.
Simulation: testing a process change against the event data before going live.
AI-assisted root cause analysis and generative AI answers to process questions.
Actions: alerts, workflows or bots, including through SAP Build Process Automation.
A free trial and a request form for a free analysis.
SAP Signavio Process Insights is the faster entry point. SAP Learning says it extracts data from SAP ERP and comes with more than 200 process performance indicators, benchmarks and correction recommendations (SAP Learning). SAP's May 2026 release post says the two products are being brought together into one experience (SAP Signavio). Ask your SAP contact which names and editions your contract covers.
SAP is also adding agents on top. SAP News on 26 October 2026 describes the Process Consulting Agent, which answers process questions in natural language, as part of the Process Transformation Assistant, in a phased general availability. It also names SAP Signavio agent mining for finding where agents could help (SAP News).
Without Signavio, open-source tools such as pm4py do the core analysis on an exported log. That is what the deep layer builds.
"Process mining shows how long people work on a case." It shows elapsed time between recorded steps. Effort comes from shadowing.
"The happy path is the problem to fix." The happy path is usually fine. The time goes into detours: blocks, changes, rework.
"More variants means a worse process." A few variants carry most cases. Count how many cases each covers before worrying.
"Process mining finds AI use cases by itself." It finds where time goes. Deciding whether AI, a rule, or a schedule change fits is a separate step.
"The event log is already in SAP, ready to use." The raw records are there. Building a clean log means choosing the case, joining tables and handling changes. That is most of the effort.
Pick one answer for each question. The explanation appears after you choose.
1What does process mining give an FDE that interviews alone don't?
Answer: D. The event log shows the process as it ran, with waits, rework and variants counted from system data. It shows elapsed time, not people's effort, so it complements interviews and shadowing rather than replacing them.
2Credit-blocked orders at Calder wait about a day, and much longer when blocked on a Friday. What does that pattern most suggest?
Answer: C. A wait that jumps for Friday blocks points at when the work is picked up, not how long it takes. A schedule change, such as a second review slot, may fix most of it before any AI is considered.
3A partner proposes AI after mining your process. Which question tests their finding best?
Answer: A. Some waits are intended, like payment terms or a nightly billing run, and the log can't say why a case waited. A finding confirmed by interviews or shadowing is one you can act on.
4What do SAP Signavio Process Insights and Process Intelligence each offer, as SAP describes them?
Answer: A. SAP Learning describes Process Insights as a fast track with more than 200 performance indicators and recommendations, and Process Intelligence as analysis of event logs across systems. SAP's May 2026 release post says the two are being brought into one experience.
5The mining shows the same price change happening twice in many spare-part orders. What is the usual first move?
Answer: B. Rework usually has a fixable cause in master data or a rule. Removing the cause comes before any automation, and an agent changing SAP prices without approval breaks the course's approval rule.
6Why is "a baseline from the event log" valuable when you frame the problem?
Answer: B. A baseline counted from system data is defensible and repeatable. Rerunning the same query on the next quarter's log shows whether the change worked.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
A process is a set of cases; every case leaves a trail of timestamps.
Process mining is three moves on that trail:
Choose the case. Decide what one instance of the process is: a sales order, an order item, a delivery. Everything else depends on it.
Order the events. Sort each case's events by time. Now each case is a sequence, and each pair of neighbors is a wait.
Count and compare. Group identical sequences into variants. Sum the waits per step pair. Compare cases that take a detour with cases that don't.
The FDE's question sits on top: where does the time go that someone could take back? Not every hour is waste. A nightly billing run makes every order wait until 22:00, by design. A credit block that waits for Monday's review is a queue someone chose not to staff. Mining finds both. Your job is to tell them apart, then measure the one worth fixing.
flowchart LR
S[SAP tables<br/>change documents,<br/>document flow] --> E[Event log<br/>case, activity, time]
E --> V[Variants]
E --> R[Rework]
E --> W[Waiting times]
E --> T[Touchless rate]
V --> D[Detours: extra time<br/>per optional step]
W --> D
D --> P[Top delay pattern<br/>drill down by segment]
P --> F[Problem frame<br/>with a system baseline]
An event log needs three columns: a case ID, an activity name and a timestamp. SAP Learning's Signavio course lists the same three and says choosing the case ID is one of the most important decisions in data transformation (SAP Learning). Extra columns are attributes: of the case (customer, sales channel) or of the event (who did it).
The standard exchange format for such logs is XES, IEEE 1849, revised in 2023 (IEEE 1849). Most tools also read plain CSV.
SAP doesn't keep a ready-made event log. You build one from records that exist for other reasons:
Creation data on the document itself: when the sales order, delivery or invoice was created.
Change documents: SAP records many field changes in the change header and item tables CDHDR and CDPOS. A price change, a block set or removed, or a date change often shows up there. Berti and colleagues use these two tables as the main change tables in their extraction approach for SAP ERP (arXiv 2110.03467).
Document flow: the table VBFA links an order to its deliveries and invoices, as a previous-document and next-document pair (same paper).
Which fields write change documents, and with which values, depends on the system's configuration. Treat any mapping from a table entry to an activity name as something to verify per system.
One sales order can have three deliveries. One invoice can bill two orders. Force everything onto one case ID and two things go wrong, as the same paper describes:
Convergence: one event is copied into several cases. An invoice that bills two orders counts twice.
Divergence: unrelated events land in one case and look like a loop. Three deliveries of one order look like delivery rework.
Object-centric process mining avoids this by letting an event relate to several objects. The OCEL 2.0 standard records events, objects, their relations and object changes, in SQLite, XML or JSON (OCEL 2.0). For a first FDE analysis, a single case notion is usually fine. Pick the object closest to the problem (the sales order, for credit blocks) and say what you lost.
Variants. Group cases by their exact sequence of activities. SAP Learning defines a variant as a set of cases with the same process flow (SAP Learning). Report the top few with their share of cases and median duration.
Rework. A step that happens twice or more in a case. pm4py computes rework per case as the number of activities minus the number of distinct activities (pm4py source).
Waiting time. For each pair of steps that directly follow each other, the time between them. Sum per pair to see where hours pile up; take the median to see the typical wait.
Touchless rate. The share of cases with no manual intervention after creation. There is no universal definition: write down your list of intervention steps and keep it with the number.
Ranking step pairs by total hours finds the busiest edges, which are often the happy path's normal work: picking, packing, the nightly billing run. That is real time, but rarely the problem.
A detour comparison is more useful. For each step only some cases take, compare the median duration of cases with it and without it. Multiply the difference by the number of cases. That is the extra time the detour costs. The detour with the most extra hours is the top delay pattern. Then find the wait into or out of it with the most hours, and drill down: split that wait by weekday, channel or item group to see where it concentrates.
The output is a hypothesis with a system baseline: "credit blocks wait 24 hours at the median; Friday blocks wait 63". Interviews and shadowing then confirm the cause and measure the effort.
You will run real process mining on a made-up but SAP-shaped event log for Calder Pumpworks (fictional). The script finds the variants, rework, waiting times and touchless rate, finds the top delay pattern, and drafts a problem frame that the course's problem framer can check.
flowchart LR
G[sample] --> L[(calder_o2c_events.csv)]
L --> A[analyze]
A --> M[process_map.md]
A --> P[top delay pattern]
P --> F[frame]
F --> T[credit_block_wait.txt]
T --> X[problem_framer.py lint]
Open VS Code, then File > Open Folder and choose orchestrate-course in your home folder.
Open a terminal and activate the virtual environment.
Windows (PowerShell):
.\.venv\Scripts\Activate.ps1
macOS/Linux:
source .venv/bin/activate
Check the version:
python --version
You should see Python 3.11 or higher, for example Python 3.14.4. If it says 3.10, install a newer Python as in the computer setup topic, then recreate .venv.
In VS Code, right-click the unit14 folder, choose New File, name it mine_o2c.py, paste the code below and save.
"""Process mining on an SAP-shaped order-to-cash event log.
Commands:
sample write a made-up event log for Calder Pumpworks (fictional), seed 42
analyze variants, touchless rate, rework, waiting times, detours, drill-down
frame turn the top detour into a problem frame for problem_framer.py
The analysis uses pm4py (open source, AGPL license). No account, no network.
"""
import argparse
import datetime as dt
import random
import sys
from pathlib import Path
import pandas as pd
HERE = Path(__file__).resolve().parent
MINING = HERE / "mining"
FRAMING = HERE / "framing"
DEFAULT_LOG = MINING / "calder_o2c_events.csv"
# Steps where a person steps in after the order exists. A case with none is touchless.
INTERVENTIONS = ["Set Credit Block", "Release Credit Block", "Change Price",
"Change Requested Delivery Date", "Reject Sales Order"]
# ---------------------------------------------------------------------------
# 1. The sample log: 600 orders in CP10, July to September 2026 (made up)
# ---------------------------------------------------------------------------
DIRECT = ["10000001"] * 5 + [str(n) for n in range(10000021, 10000029)]
DISTRIBUTORS = ["10000002"] * 6 + [str(n) for n in range(10000031, 10000037)]
def work_time(t, start=8, end=17):
"""Move t forward to the next moment inside working hours, Monday to Friday."""
while True:
if t.weekday() >= 5:
t = dt.datetime.combine(t.date() + dt.timedelta(days=1), dt.time(start))
elif t.hour < start:
t = t.replace(hour=start, minute=0, second=0)
elif t.hour >= end:
t = dt.datetime.combine(t.date() + dt.timedelta(days=1), dt.time(start))
else:
return t
def next_review(t, rng):
"""The credit analyst works the blocked-order list once a day, 09:00 to 11:00."""
day = t.date()
if t.weekday() < 5 and t.hour < 9:
pass
else:
day += dt.timedelta(days=1)
while day.weekday() >= 5:
day += dt.timedelta(days=1)
return dt.datetime.combine(day, dt.time(9)) + dt.timedelta(minutes=rng.randint(0, 120))
def make_case(order, rng, start):
channel = "20" if rng.random() < 0.55 else "10"
customer = rng.choice(DISTRIBUTORS if channel == "20" else DIRECT)
group = "spare parts" if rng.random() < (0.7 if channel == "20" else 0.35) else "pumps"
value = round(rng.uniform(200, 6000) if group == "spare parts" else rng.uniform(3950, 40000), 2)
events = []
def add(activity, when, user):
events.append((order, activity, when, user, customer, channel, group, value))
# Distributors send orders by EDI at any hour; direct orders are keyed in working hours.
created = start + dt.timedelta(minutes=rng.randint(0, 24 * 60 - 1))
if channel == "10":
created = work_time(created) + dt.timedelta(minutes=rng.randint(0, 90))
add("Create Sales Order", created, "edi" if channel == "20" else "dialog")
t = created
blocked = rng.random() < ({"10000002": 0.55}.get(customer, 0.35 if channel == "20" else 0.06))
price_missing = group == "spare parts" and rng.random() < (0.30 if channel == "20" else 0.10)
reblock = blocked and price_missing and rng.random() < 0.25
if blocked:
t += dt.timedelta(minutes=1)
add("Set Credit Block", t, "system")
if price_missing and not reblock:
t = work_time(t + dt.timedelta(minutes=rng.randint(30, 300)))
add("Change Price", t, "dialog")
if rng.random() < 0.25: # wrong the first time
t = work_time(t + dt.timedelta(minutes=rng.randint(60, 600)))
add("Change Price", t, "dialog")
if blocked:
t = next_review(t, rng)
if rng.random() < 0.2: # the analyst asks sales for context; decided next review
t = next_review(t, rng)
if rng.random() < 0.04:
add("Reject Sales Order", t, "dialog")
return events
add("Release Credit Block", t, "dialog")
if reblock: # price fixed after release: higher value, blocked again
t = work_time(t + dt.timedelta(minutes=rng.randint(30, 240)))
add("Change Price", t, "dialog")
t += dt.timedelta(minutes=1)
add("Set Credit Block", t, "system")
t = next_review(t, rng)
add("Release Credit Block", t, "dialog")
if rng.random() < 0.05: # the customer asks for a later date
t = work_time(t + dt.timedelta(minutes=rng.randint(60, 900)))
add("Change Requested Delivery Date", t, "dialog")
t += dt.timedelta(days=rng.randint(1, 3))
if group == "pumps" and rng.random() < 0.08: # not in stock: wait for production
t += dt.timedelta(days=rng.randint(2, 5))
# The delivery due list runs every hour; the warehouse ships in working hours.
t = t.replace(minute=0, second=0) + dt.timedelta(hours=1, minutes=5)
add("Create Delivery", t, "system")
t = work_time(t + dt.timedelta(minutes=rng.randint(60, 240)), 7, 16)
add("Post Goods Issue", t, "dialog")
t = dt.datetime.combine(t.date(), dt.time(22)) # nightly billing run
add("Create Invoice", t, "system")
return events
def cmd_sample(args):
out = Path(args.log)
if out.exists() and not args.force:
sys.exit(f"Sample log already exists at {show(out)}. Add --force to overwrite it.")
rng = random.Random(42)
first = dt.datetime(2026, 7, 1)
rows = []
for i in range(600):
start = first + dt.timedelta(days=rng.randint(0, 91))
rows += make_case(str(1002001 + i), rng, start)
df = pd.DataFrame(rows, columns=["order", "activity", "timestamp", "user_type",
"customer", "channel", "item_group", "net_value"])
df = df.sort_values(["order", "timestamp"], kind="stable")
df["timestamp"] = df["timestamp"].dt.strftime("%Y-%m-%d %H:%M:%S")
out.parent.mkdir(parents=True, exist_ok=True)
df.to_csv(out, index=False)
print(f"Wrote {len(df)} events for {df['order'].nunique()} orders to {show(out)}")
print("The data is made up (Calder Pumpworks is fictional). Next: python unit14/mine_o2c.py analyze")
# ---------------------------------------------------------------------------
# 2. Loading a log
# ---------------------------------------------------------------------------
def show(path):
try:
return str(Path(path).resolve().relative_to(Path.cwd().resolve()))
except ValueError:
return str(path)
def load(args):
path = Path(args.log)
if not path.exists():
sys.exit(f"No event log at {args.log}. Run 'sample' first, or check the path.")
df = pd.read_csv(path, dtype=str)
for col in (args.case, args.activity, args.time):
if col not in df.columns:
sys.exit(f"Column '{col}' not in {show(path)}. Columns: {', '.join(df.columns)}")
df[args.time] = pd.to_datetime(df[args.time])
import pm4py # imported here so 'sample' works even before pm4py is installed
log = pm4py.format_dataframe(df, case_id=args.case, activity_key=args.activity,
timestamp_key=args.time)
return pm4py, log
def hours(seconds):
return seconds / 3600
def median_hours(series):
return float(series.median()) / 3600 if len(series) else 0.0
def case_durations(log):
g = log.groupby("case:concept:name")["time:timestamp"]
return (g.max() - g.min()).dt.total_seconds()
def pairs(log):
"""Each event with the next event of the same case: one row per directly-follows pair."""
log = log.sort_values(["case:concept:name", "time:timestamp"], kind="stable").copy()
log["next_activity"] = log.groupby("case:concept:name")["concept:name"].shift(-1)
log["next_time"] = log.groupby("case:concept:name")["time:timestamp"].shift(-1)
p = log.dropna(subset=["next_activity"]).copy()
p["wait_s"] = (p["next_time"] - p["time:timestamp"]).dt.total_seconds()
return p
# ---------------------------------------------------------------------------
# 3. The analysis
# ---------------------------------------------------------------------------
def detours(log, durations, max_share=0.5):
"""For each step that only some cases have: how much longer those cases take."""
acts = log.groupby("concept:name")["case:concept:name"].unique()
n_cases = len(durations)
rows = []
for act, cases in acts.items():
if len(cases) > max_share * n_cases: # main path, not a detour
continue
mask = durations.index.isin(cases)
with_, without = durations[mask], durations[~mask]
excess = (median_hours(with_) - median_hours(without)) * len(with_)
rows.append((act, len(with_), median_hours(with_), median_hours(without), excess))
return sorted(rows, key=lambda r: -r[4])
def top_detour(log):
"""The detour costing the most hours, and the wait into or out of it with the most hours."""
durations = case_durations(log)
rows = [r for r in detours(log, durations) if r[0] != "Reject Sales Order" and r[4] > 0]
if not rows:
sys.exit("No detour adds time in this log.")
act = rows[0][0]
p = pairs(log)
touching = p[(p["concept:name"] == act) | (p["next_activity"] == act)]
totals = touching.groupby(["concept:name", "next_activity"])["wait_s"].sum()
a, b = totals.idxmax()
return a, b, touching[(touching["concept:name"] == a) & (touching["next_activity"] == b)]
def chosen_pattern(log, args):
"""The wait to drill into: --pattern "A > B" if given, otherwise the top delay pattern."""
if not args.pattern:
return top_detour(log)
a, sep, b = args.pattern.partition(">")
a, b = a.strip(), b.strip()
p = pairs(log)
waits = p[(p["concept:name"] == a) & (p["next_activity"] == b)]
if not sep or waits.empty:
sys.exit(f"No case goes directly from '{a}' to '{b}'. Write it as \"Step A > Step B\", "
"using the names from the waiting-time list.")
return a, b, waits
def cmd_analyze(args):
pm4py, log = load(args)
n_cases = log["case:concept:name"].nunique()
durations = case_durations(log)
print(f"Cases {n_cases}, events {len(log)}, activities {log['concept:name'].nunique()}, "
f"{log['time:timestamp'].min():%Y-%m-%d} to {log['time:timestamp'].max():%Y-%m-%d}")
print(f"Median throughput time {median_hours(durations):.1f} h "
f"(first event to last event of a case)")
print("\nTop variants (one variant = one sequence of steps)")
variants = pm4py.get_variants(log)
by_case = log.groupby("case:concept:name")["concept:name"].agg(tuple)
for i, (variant, count) in enumerate(sorted(variants.items(), key=lambda kv: -kv[1])[:args.top], 1):
med = median_hours(durations[by_case[by_case == variant].index])
print(f" {i}. {count:4d} cases {count / n_cases:6.1%} median {med:6.1f} h "
+ " > ".join(variant))
print(f" {len(variants)} variants in total")
touched = set(log.loc[log["concept:name"].isin(INTERVENTIONS), "case:concept:name"])
print(f"\nTouchless rate {1 - len(touched) / n_cases:.1%} "
f"({n_cases - len(touched)} of {n_cases} cases have none of: {', '.join(INTERVENTIONS)})")
print("\nRework (cases where a step happens more than once)")
rework = pm4py.get_rework_cases_per_activity(log)
for act, count in sorted(rework.items(), key=lambda kv: -kv[1]):
print(f" {act:32s} {count:4d} cases")
if not rework:
print(" none")
print("\nWaiting time between steps (directly follows), by total hours")
p = pairs(log)
edges = p.groupby(["concept:name", "next_activity"])["wait_s"].agg(["count", "median", "sum"])
edges = edges.sort_values("sum", ascending=False).head(args.top)
for (a, b), row in edges.iterrows():
print(f" {a + ' > ' + b:58s} {int(row['count']):4d}x median {hours(row['median']):6.1f} h"
f" total {hours(row['sum']):7.0f} h")
print("\nDetours: steps only some cases have, and the extra time those cases take")
print(f" {'step':32s} {'cases':>5s} {'median with':>12s} {'without':>8s} {'extra hours':>12s}")
for act, n, w, wo, excess in detours(log, durations):
print(f" {act:32s} {n:5d} {w:10.1f} h {wo:6.1f} h {excess:12.0f}")
act, nxt, waits = chosen_pattern(log, args)
total = waits["wait_s"].sum()
label = "Chosen pattern" if args.pattern else "Top delay pattern"
print(f"\n{label}: {act} > {nxt}, {len(waits)} times, "
f"median {median_hours(waits['wait_s']):.1f} h, total {hours(total):.0f} h")
days = ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"]
waits = waits.assign(weekday=pd.Categorical(waits["time:timestamp"].dt.day_name().str[:3],
categories=days, ordered=True))
# Segment by case attributes: columns with one value per case and at most 12 values.
skip = {args.case, args.activity, args.time, "case:concept:name", "concept:name", "time:timestamp"}
per_case = log.groupby("case:concept:name").nunique()
segment_cols = ["weekday"] + [c for c in per_case.columns if c not in skip
and not c.startswith("@@") and per_case[c].max() == 1
and 1 < log[c].nunique() <= 12]
for col in segment_cols:
print(f" by {col}:")
for value, grp in waits.groupby(col, observed=True):
print(f" {str(value):12s} {len(grp):4d}x median {median_hours(grp['wait_s']):6.1f} h"
f" share of hours {grp['wait_s'].sum() / total:6.1%}")
write_map(p, n_cases, args)
def write_map(p, n_cases, args):
"""A process map as a Mermaid flowchart: edges used by at least --min-share of cases."""
edges = p.groupby(["concept:name", "next_activity"])["wait_s"].agg(["count", "median"])
keep = edges[edges["count"] >= args.min_share * n_cases]
names = sorted(set(keep.index.get_level_values(0)) | set(keep.index.get_level_values(1)))
ids = {name: f"A{i}" for i, name in enumerate(names)}
fence = "`" * 3 # a Markdown code fence
lines = ["# Process map", "", f"Edges used by at least {args.min_share:.0%} of "
f"{n_cases} cases. Label: times, median hours.", "", fence + "mermaid", "flowchart LR"]
lines += [f" {ids[n]}[{n}]" for n in names]
for (a, b), row in keep.iterrows():
lines.append(f" {ids[a]} -->|{int(row['count'])}x, {hours(row['median']):.1f} h| {ids[b]}")
lines += [fence, ""]
MINING.mkdir(parents=True, exist_ok=True)
out = MINING / "process_map.md"
out.write_text("\n".join(lines), encoding="utf-8")
print(f"\nWrote the process map to {show(out)} ({len(keep)} edges)")
# ---------------------------------------------------------------------------
# 4. From the top delay pattern to a problem frame
# ---------------------------------------------------------------------------
def cmd_frame(args):
_, log = load(args)
act, nxt, waits = chosen_pattern(log, args)
where = "a delay found by process mining" if args.pattern else "the largest delay in the process"
base = round(median_hours(waits["wait_s"]), 1)
target = round(base * (1 - args.cut), 1)
first, last = log["time:timestamp"].min(), log["time:timestamp"].max()
late_week = waits[waits["time:timestamp"].dt.weekday >= 4] # Friday to Sunday
text = f"""# Drafted by mine_o2c.py from the event log. Review every line with the owner.
ASK: {args.ask}
PROBLEM: Orders wait {base:g} hours (median) between '{act}' and '{nxt}', {where}
ACTOR: {args.actor}
PROCESS: Order-to-cash, from '{act}' to '{nxt}'
UNIT: One case that goes from '{act}' to '{nxt}'
METRIC: Median hours from '{act}' to '{nxt}'
DIRECTION: down
UNITS: hours
BASELINE: {base:g} | system, event log {first:%Y-%m-%d} to {last:%Y-%m-%d}, {len(waits)} cases
TARGET: {target:g}
BY: {args.by}
ADDRESSABLE: {args.addressable:g} | estimate, share of the wait that is queue time, not decision work; time it while shadowing
GUARDRAIL: Released orders overdue after 30 days rise no more than 1 point
CONSTRAINT: Read-only access to SAP under the {args.actor.lower()}'s own authorizations
CONSTRAINT: The {args.actor.lower()} makes every release or rejection decision
OUT: Automatic release of blocked orders
OUT: Changing credit limits or credit check rules
OUTPUT: A ranked list of waiting cases each morning, oldest first, with the facts for each decision
ACTION: The {args.actor.lower()} works the list from the top in the standard app
CHANGES_SAP: no
BENCHMARK: A Friday afternoon and Monday 08:00 review slot ({len(late_week)} of {len(waits)} waits start Friday to Sunday)
OWNER: {args.owner}
STOP: If the median wait falls less than {max(1, round(base * args.cut / 2)):g} hours after four weeks, rethink
"""
FRAMING.mkdir(parents=True, exist_ok=True)
out = FRAMING / args.out
if out.exists() and not args.force:
sys.exit(f"{show(out)} already exists. Add --force to overwrite it.")
out.write_text(text, encoding="utf-8")
print(f"Pattern: {act} > {nxt}, median {base:g} h over {len(waits)} cases")
print(f"Wrote the draft frame to {show(out)}")
print(f"Next: python unit14/problem_framer.py lint {show(out)}")
# ---------------------------------------------------------------------------
# 5. Command line
# ---------------------------------------------------------------------------
def main():
parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
sub = parser.add_subparsers(dest="command", required=True)
def common(p):
p.add_argument("--log", default=str(DEFAULT_LOG), help="event log CSV")
p.add_argument("--case", default="order", help="case ID column")
p.add_argument("--activity", default="activity", help="activity column")
p.add_argument("--time", default="timestamp", help="timestamp column")
p.add_argument("--pattern", default="", help='a wait to drill into, "Step A > Step B"')
s = sub.add_parser("sample", help="write the made-up Calder event log")
s.add_argument("--log", default=str(DEFAULT_LOG))
s.add_argument("--force", action="store_true")
s.set_defaults(func=cmd_sample)
a = sub.add_parser("analyze", help="mine the log")
common(a)
a.add_argument("--top", type=int, default=6, help="rows to show in each list")
a.add_argument("--min-share", type=float, default=0.05, help="smallest edge share on the map")
a.set_defaults(func=cmd_analyze)
f = sub.add_parser("frame", help="draft a problem frame from the top delay pattern")
common(f)
f.add_argument("--ask", default="Can AI clear our credit blocks faster?")
f.add_argument("--actor", default="Credit analyst")
f.add_argument("--owner", default="Head of Credit Management")
f.add_argument("--cut", type=float, default=0.5, help="share of the median wait to remove")
f.add_argument("--addressable", type=float, default=0.7)
f.add_argument("--by", default="2027-03-31")
f.add_argument("--out", default="credit_block_wait.txt")
f.add_argument("--force", action="store_true")
f.set_defaults(func=cmd_frame)
args = parser.parse_args()
args.func(args)
return 0
if __name__ == "__main__":
sys.exit(main())
Wrote 2830 events for 600 orders to unit14/mining/calder_o2c_events.csv
The data is made up (Calder Pumpworks is fictional). Next: python unit14/mine_o2c.py analyze
Open unit14/mining/calder_o2c_events.csv. The first lines are:
First, pm4py prints a welcome box with its license notice. That is expected. Then you should see:
Cases 600, events 2830, activities 9, 2026-07-01 to 2026-10-05
Median throughput time 26.4 h (first event to last event of a case)
Top variants (one variant = one sequence of steps)
1. 380 cases 63.3% median 13.9 h Create Sales Order > Create Delivery > Post Goods Issue > Create Invoice
2. 121 cases 20.2% median 37.0 h Create Sales Order > Set Credit Block > Release Credit Block > Create Delivery > Post Goods Issue > Create Invoice
3. 36 cases 6.0% median 25.3 h Create Sales Order > Change Price > Create Delivery > Post Goods Issue > Create Invoice
4. 17 cases 2.8% median 37.2 h Create Sales Order > Set Credit Block > Change Price > Release Credit Block > Create Delivery > Post Goods Issue > Create Invoice
5. 14 cases 2.3% median 107.1 h Create Sales Order > Change Requested Delivery Date > Create Delivery > Post Goods Issue > Create Invoice
6. 10 cases 1.7% median 38.9 h Create Sales Order > Change Price > Change Price > Create Delivery > Post Goods Issue > Create Invoice
12 variants in total
Touchless rate 63.3% (380 of 600 cases have none of: Set Credit Block, Release Credit Block, Change Price, Change Requested Delivery Date, Reject Sales Order)
Rework (cases where a step happens more than once)
Change Price 15 cases
Release Credit Block 6 cases
Set Credit Block 6 cases
Waiting time between steps (directly follows), by total hours
Post Goods Issue > Create Invoice 596x median 10.3 h total 6615 h
Create Delivery > Post Goods Issue 596x median 3.2 h total 5171 h
Set Credit Block > Release Credit Block 138x median 24.1 h total 4112 h
Create Sales Order > Create Delivery 380x median 0.7 h total 2168 h
Change Requested Delivery Date > Create Delivery 21x median 49.1 h total 1078 h
Create Sales Order > Change Price 47x median 8.4 h total 782 h
Detours: steps only some cases have, and the extra time those cases take
step cases median with without extra hours
Release Credit Block 155 39.4 h 18.6 h 3232
Set Credit Block 159 37.8 h 18.7 h 3042
Change Requested Delivery Date 21 109.2 h 24.6 h 1778
Change Price 76 36.8 h 23.9 h 984
Reject Sales Order 4 7.8 h 26.6 h -75
Top delay pattern: Set Credit Block > Release Credit Block, 138 times, median 24.1 h, total 4112 h
by weekday:
Mon 28x median 24.1 h share of hours 15.8%
Tue 17x median 16.9 h share of hours 8.2%
Wed 22x median 15.6 h share of hours 10.1%
Thu 22x median 18.0 h share of hours 9.1%
Fri 17x median 63.3 h share of hours 24.5%
Sat 18x median 49.2 h share of hours 22.1%
Sun 14x median 26.4 h share of hours 10.3%
by channel:
10 18x median 24.6 h share of hours 11.3%
20 120x median 23.1 h share of hours 88.7%
by item_group:
pumps 56x median 24.8 h share of hours 44.9%
spare parts 82x median 22.5 h share of hours 55.1%
Wrote the process map to unit14/mining/process_map.md (8 edges)
Read it from top to bottom, as an FDE would:
Section
What it tells you here
Variants
The happy path covers 63% of orders. Variant 2, the credit block, is the next biggest and takes about a day longer
Touchless rate
63%: the same 380 orders. Every other order had at least one person step in
Rework
15 orders had their price changed twice. Six were blocked twice: a price fixed after release raised the value
Waiting time
The two biggest edges are normal work and the nightly billing run. The third, block to release, is a queue
Detours
The credit block costs about 3,200 extra hours a quarter, more than any other detour
Drill-down
Blocks set Friday to Sunday hold 57% of the hours. Distributor orders hold 89%
Open unit14/mining/process_map.md. It holds a Mermaid flowchart of every step pair used by at least 5% of cases, labelled with the count and median hours. GitHub shows it as a diagram. In VS Code, the preview needs a Mermaid extension; or paste the code block into mermaid.live.
An "empty" result is also informative. If the detour list shows only negative extra hours, no optional step slows cases down, and the script stops with No detour adds time in this log.
Pattern: Set Credit Block > Release Credit Block, median 24.1 h over 138 cases
Wrote the draft frame to unit14/framing/credit_block_wait.txt
Next: python unit14/problem_framer.py lint unit14/framing/credit_block_wait.txt
Open unit14/framing/credit_block_wait.txt. The key lines are:
METRIC: Median hours from 'Set Credit Block' to 'Release Credit Block'
BASELINE: 24.1 | system, event log 2026-07-01 to 2026-10-05, 138 cases
TARGET: 12.1
ADDRESSABLE: 0.7 | estimate, share of the wait that is queue time, not decision work; time it while shadowing
BENCHMARK: A Friday afternoon and Monday 08:00 review slot (49 of 138 waits start Friday to Sunday)
The baseline is labelled system: it was counted from data. The addressable share is labelled estimate, because the log can't tell queue time from decision work. Only shadowing can.
== unit14/framing/credit_block_wait.txt
OK Target asks for a 50% change (24.1 to 12.1 hours).
OK Ceiling: best case is 7.2; the target needs 71% of the addressable part.
WARN ADDRESSABLE is an estimate. Time real cases before you promise the target.
-> FRAMED: 0 to fix, 1 to consider
1 of 1 frames ready for the opportunity assessment.
The warning is correct. It stays until someone times real reviews.
Save your work:
git add unit14/mine_o2c.py unit14/mining/ unit14/framing/ requirements.txt
git commit -m "Unit 14: process mining on the order-to-cash log"
Timestamps must be in a format pandas reads, such as 2026-09-21 07:00:00. Remove names and other personal data before the file reaches your laptop. To drill into a wait you choose, add --pattern "Step A > Step B", using names from the waiting-time list.
As of 1 November 2026, SAP's Signavio site describes Process Intelligence with pre-built connectors for SAP and non-SAP systems, ready-made process flows and metrics, SIGNAL queries, Process Atoms for conformance rules, data-driven simulation, AI-assisted root cause analysis and actions through SAP Build Process Automation. The page offers a free trial.
Getting data in runs through a process data pipeline. SAP Learning describes its parts: a connection to the source system, source data (which tables and columns), extraction with scheduling, pseudonymization and partitioning, transformation as SQL scripts that define business objects and data model tables, and a load into a process (SAP Learning). Value Accelerators provide transformation templates for common processes. The same lesson says they need the Data Modelling feature set enabled by a workspace administrator.
Analysis then uses SiGNAL, which SAP Learning calls SQL-oriented. It queries one table of cases with nested events; case_id, event_name and end_time are always present, and it reads data without changing it (SAP Learning). The detour comparison in this topic's script is the kind of question you would write in SiGNAL there.
Process Insights is the prebuilt route for SAP ERP. SAP Learning lists SAP ECC 6.0 and SAP S/4HANA on-premise among its sources. The ST-PI and ST-A/PI plug-ins collect the data and send it over an encrypted HTTPS outbound connection to SAP BTP, where Process Insights runs (SAP Learning). It comes with more than 200 process performance indicators and correction recommendations.
Check the status for your edition and region before promising any of these to a client. Licensing is a contract question; the product pages show no prices.
When you build an event log outside Signavio, read through released interfaces. SAP's published release-state list (checked 1 November 2026) marks the tables CDHDR and CDPOS as not to be released for ABAP Cloud, with the classes CL_CHDO_READ_TOOLS and CL_CHDO_WRITE_TOOLS as successors. It lists the CDS view I_CHANGEDOCUMENTFORCHGDOCOBJ (change documents) and I_SDDOCUMENTMULTILEVELPROCFLOW (sales document flow) as released (SAP on GitHub). Which view a given extraction tool may call depends on your edition and authorizations.
For credit blocks, the analyst's decision happens in the Manage Documented Credit Decisions app, where blocked documents are released or rejected (SAP Learning). A credit-block event log in a real system comes from those decisions and the order's status changes. The order-to-cash topic traces the documents.
Where it sits. Event data leaves the system that records it and lands where the analysis runs. Nothing writes back to SAP.
flowchart LR
S4[S/4HANA CP10<br/>orders, change docs,<br/>document flow] -->|extraction, read only| PI[Signavio<br/>process data pipeline]
ECC[ECC CP20] -->|ST-A/PI plug-in| PS[Process Insights<br/>on SAP BTP]
S4 -->|released views,<br/>pseudonymized extract| X[CSV extract]
X --> PM[pm4py on<br/>FDE laptop]
PI --> F[Findings and baseline]
PS --> F
PM --> F
F --> FR[Problem frame]
Non-functional requirements.
Requirement
Typical target
What breaks if missed
Personal data
Pseudonymized or removed before export
Data protection breach; workers' council objections
Data volume
One process, one quarter, all cases
Sampling hides rare but costly variants
Freshness
Daily or weekly for monitoring; once for discovery
Stale baseline after a process change
Source load
Extraction outside peak hours
Slow dialog response during month-end
Repeatability
The same query reruns after go-live
No proof the change worked
Integration and identity. Extraction runs as a technical user with read-only access to the needed tables or views, not under a person's account. Event logs carry user IDs; pseudonymize them unless the analysis needs roles. Findings that name individuals' speed are a works-council topic in many countries, so mine by role and team.
Context: Calder (fictional) wants to know why credit-blocked distributor orders are late; Signavio isn't licensed; the extract is one quarter of CP10 orders.
Options: A a pm4py analysis on a pseudonymized extract; B a Process Insights trial or analysis; C a Process Intelligence project.
Decision: A for discovery, with B requested in parallel to cross-check the baseline.
Consequences: the FDE owns the extraction query and its definition; the baseline must be rerun the same way after go-live; any ongoing monitoring moves to C or to a scheduled job.
What a design review will ask.
What is the case, and which events converge or diverge under that choice?
Which fields are in the extract, and how is personal data removed?
Which waits are by design, and how did you exclude them?
How was the finding confirmed beyond the log?
Who reruns the baseline after go-live, and with what query?
Authorizations. Reading change documents across a whole process needs broad read access. Use a technical user scoped to the needed views, and log what was extracted and when.
Data protection. Event logs hold user IDs, customer names and amounts. Export only the columns the analysis needs. SAP Learning lists pseudonymization as part of Signavio extraction; do the same in your own extracts.
Works councils and trust. Process mining can look like monitoring people. Agree up front that results are by process and role, not by person.
Evaluation. A finding is a hypothesis. Check it against interviews and a sample of cases you open in SAP. If the log says "Friday blocks wait 63 hours", open five Friday blocks and see.
Cost. pm4py is free to run but licensed under the AGPL. Signavio is a licensed product; ask what your contract covers before planning around it.
Operations. A one-off analysis is cheap. A monitored KPI needs an owner, a schedule and a stable definition.
Clean core. Mining only reads. Use released views and standard extractors, not custom code in the SAP system.
Frame another detour: orders waiting for a price fix. It belongs to customer service, not credit, and unlike a customer's date change, Calder controls its cause. Your frame feeds the opportunity assessment next in Unit 14.
Drill into the wait before the price change:
python unit14/mine_o2c.py analyze --pattern "Create Sales Order > Change Price"
In the drill-down, note the median (8.4 h), the share of hours from channel 20, and the weekdays with the longest waits.
Draft the frame:
python unit14/mine_o2c.py frame --pattern "Create Sales Order > Change Price" --actor "Customer service agent" --owner "Head of Customer Service" --ask "Can AI fix missing prices on spare-part orders?" --out price_fix_wait.txt
Open unit14/framing/price_fix_wait.txt. The guardrail, constraint, out-of-scope and benchmark lines are written for credit blocks. Rewrite each one for customer service. For example:
GUARDRAIL: Orders invoiced at a wrong price do not increase
CONSTRAINT: Read-only access to SAP under the customer service agent's own authorizations
CONSTRAINT: The customer service agent confirms every price before it is saved
OUT: Changing prices in SAP without a person's approval
OUT: Changing price conditions or price lists
BENCHMARK: Maintain the missing aftermarket prices for the spare parts most often ordered
Check that no line still mentions credit or release.
Add one line at the top of the file, starting with #, saying which of these you think fits first: a master data fix, a rule, or AI. Give the reason from the drill-down.
Commit:
git add unit14/framing/price_fix_wait.txt
git commit -m "Unit 14: price-fix wait framed from process mining"
Done whenprice_fix_wait.txt shows FRAMED in the lint, the search for credit or release finds nothing, and the first line says which fix comes first and why.
For readers who want the theory and standards behind event logs and object-centric mining.
An Event Data Extraction Approach from SAP ERP for Process Mining (Berti, Park, Rafiei, van der Aalst). How change tables and document flow become events, and why a single case notion causes convergence and divergence. Read it before you write your own extractor.
OCEL 2.0 Specification. The object-centric event log standard: events related to many objects, object changes over time, and object-to-object relations, in SQLite, XML or JSON. It is the format to reach for when orders, deliveries and invoices must be analyzed together.
pm4py rework source. A short, readable example of how a process mining measure is defined in code. Reading the source of the measures you report is the fastest way to know exactly what a number means.
Three ideas worth exploring from these:
Directly follows vs. eventually follows. This topic's waits use directly-follows pairs: the next event. A credit block followed by a price change and then a release has no direct block-to-release pair, so it drops out of the 138 waits. Eventually-follows relations keep it, at the cost of counting overlapping spans.
Discovery algorithms. A process map of directly-follows edges is the simplest model. Algorithms such as the inductive miner produce models with explicit choices and loops, which conformance checking needs.
Object-centric drill-downs. With OCEL, "orders whose deliveries split" is a query, not a data-preparation project. Process Networks in Signavio is SAP's object-centric view.
Pick one answer for each question. The explanation appears after you choose.
1Why does the choice of case ID matter so much when building an event log from SAP?
Answer: B. One order can have several deliveries and one invoice can bill several orders. Forcing all of them onto one case copies events into several cases or creates fake loops, which distorts variants and rework.
2In the script, how does the detour comparison decide which step costs the most time?
Answer: C. detours compares median case duration with and without each optional step and multiplies the difference by the number of cases that took it. Total hours per step pair, the most tempting choice, mostly finds normal work such as the nightly billing run.
3Post Goods Issue to Create Invoice has the most total hours in the log. Why isn't it the problem to frame?
Answer: A. Every order waits until the 22:00 billing run, so the wait is a scheduling choice that applies to all cases. The detour comparison exists to separate such waits from the friction only some cases suffer.
4The drafted frame labels the baseline system but the addressable share estimate. Why?
Answer: A. The median wait is counted from timestamps, so it is a system number. How much of that wait is queue versus the analyst's actual work isn't recorded, so it stays an estimate until someone times real reviews.
5You want to build an event log from change documents on S/4HANA. What does SAP's release-state list suggest?
Answer: D. The list marks CDHDR and CDPOS as not to be released for ABAP Cloud and lists the change-document CDS view as released. Reading through released interfaces keeps the core clean.
6A client wants process mining built into a product they sell. You prototyped with pm4py. What should you raise first?
Answer: C. pm4py is licensed under the AGPL, and its maintainers offer a commercial license. Shipping it inside a product is a legal decision, which is why the Build vs. SAP table lists licensing as the deciding factor there.
7The log shows Friday credit blocks wait 63 hours. What do you do before proposing a solution?
Answer: C. A mined pattern is a hypothesis. Opening real cases and talking to the analysts confirms the cause, such as a once-a-day review, before anyone builds anything.
8Which status statement about SAP Signavio is accurate as described in this topic?
Answer: D. SAP News on 26 October 2026 describes a phased general availability for the Process Transformation Assistant, which includes the Process Consulting Agent. The May post only planned root cause analysis for June, and it moved Process Networks to GA.
Ask a question
Testing: only staff see this
Stuck on something in this layer? Ask it here. Questions are answered in the order they arrive, and the answer appears under My questions.
Sign in (free) to ask a question. You can ask anonymously.
Related topics
Picked from the terms, links and tags this topic shares with others.
SAP Signavio Process Intelligence (signavio.com product page)— pre-built connectors for SAP and non-SAP systems; SIGNAL queries; Process Atoms and conformance; data-driven simulation; AI-assisted root cause analysis; actions through SAP Build Process Automation; free trial. Opened 2026-11-01
May 2026 product release update (SAP Signavio)— Process Insights and Process Intelligence brought into one experience; Process Networks (object-centric) to GA; AI-assisted root cause analysis and context analyzer planned for GA in June 2026
Introducing to Process Data Pipelines (SAP Learning)— connection, source data, extraction with pseudonymisation and partitioning, transformation in SQL scripts, load into a process; Value Accelerators templates