Skip to main content

Process Mining

Business processes have expected flows: order → payment → shipment → delivery. Deviations — skipped steps, reversed order, timeout violations — hide in event logs. Sifting patterns describe the expected flow and its exceptions, finding every instance where the process broke.

Time~15 minutes
PrerequisitesWhat is Sifting?

1. Skipped approval

A purchase order is fulfilled without the required approval step.

Loading playground...

Result: 1 match — po_100 was fulfilled without approval. po_101 was approved at time 4 before fulfillment at time 6, so the negation kills that match.

What to notice: The join on ?order threads through all three parts (request, negation, fulfillment). An approval for a different order doesn't satisfy the negation — it must be for the same PO.


2. Out-of-order processing

Payment received before the order was confirmed. The process flow should be: confirm → pay. Reversed order is a deviation.

Loading playground...

Result: 1 match — ord_200 received payment (time 1) before confirmation (time 3). ord_201 was confirmed first (time 2) then paid (time 4) — correct order, no match.

What to notice: Stages are time-ordered. Stage 1 must happen before stage 2. When you define payment_received as stage 1 and order_confirmed as stage 2, the pattern matches only when payment comes first. This is how you detect reversed process steps — put the wrong order in the pattern.


3. SLA timeout

An order was placed but not shipped within the SLA window. Use a gap constraint to enforce the time limit.

Loading playground...

Result: 1 match — ord_300 took 7 ticks to ship (1 to 8), exceeding the gap 5.. threshold. ord_301 shipped in 2 ticks (within bounds). ord_302 was cancelled before shipping, so the negation prevents a false positive.

What to notice: The gap 5.. constraint defines the SLA in the pattern itself. Combined with unless between for cancellations, you get a precise definition: "order placed, shipped late, not cancelled." The gap constraint and negation work together — neither alone is sufficient.


Batch auditing

Process mining typically works on complete logs. Use batch evaluation to scan an entire event log for all deviations:

let skipped_approval_pattern = PatternBuilder::<String, MemValue>::new("skipped_approval")
.stage("e1", |s| {
s.edge(
"e1",
"type".into(),
MemValue::Str("purchase_request".into()),
)
.edge_bind("e1", "order".into(), "order")
.edge_bind("e1", "requester".into(), "requester")
})
.stage("e2", |s| {
s.edge("e2", "type".into(), MemValue::Str("fulfillment".into()))
.edge_bind("e2", "order".into(), "order")
})
.unless_between("e1", "e2", |neg| {
neg.edge("mid", "type".into(), MemValue::Str("approval".into()))
.edge_bind("mid", "order".into(), "order")
})
.build();

let payment_before_confirmation_pattern =
PatternBuilder::<String, MemValue>::new("payment_before_confirmation")
.stage("e1", |s| {
s.edge(
"e1",
"type".into(),
MemValue::Str("payment_received".into()),
)
.edge_bind("e1", "order".into(), "order")
.edge_bind("e1", "amount".into(), "amount")
})
.stage("e2", |s| {
s.edge("e2", "type".into(), MemValue::Str("order_confirmed".into()))
.edge_bind("e2", "order".into(), "order")
})
.build();

let mut engine: SiftEngineFor<MemGraph> = SiftEngine::new();
engine.register(skipped_approval_pattern);
engine.register(payment_before_confirmation_pattern);

// Load your event log into the graph.
let mut graph = MemGraph::new();
// po_100: request then fulfillment with no approval (violation).
graph.add_str("r1", "type", "purchase_request", 1);
graph.add_ref("r1", "order", "po_100", 1);
graph.add_ref("r1", "requester", "alice", 1);
graph.add_str("f1", "type", "fulfillment", 3);
graph.add_ref("f1", "order", "po_100", 3);
// ord_200: payment before confirmation (violation).
graph.add_str("p1", "type", "payment_received", 2);
graph.add_ref("p1", "order", "ord_200", 2);
graph.add_num("p1", "amount", 150.0, 2);
graph.add_str("c1", "type", "order_confirmed", 4);
graph.add_ref("c1", "order", "ord_200", 4);
graph.set_time(10);

// Find all deviations.
let matches = engine.evaluate(&graph);
for m in &matches {
println!(
"Deviation: {} — order: {:?}",
m.pattern,
m.bindings.get("order")
);
}

// Check near-misses for each pattern.
for pattern in engine.patterns() {
let gap = gap_analysis(&graph, pattern);
let matched = gap
.stages
.iter()
.filter(|s| matches!(s.status, StageStatus::Matched))
.count();
if matched > 0 && matched < gap.stages.len() {
println!(
"Near-miss: {} — {}/{} stages matched",
pattern.name,
matched,
gap.stages.len()
);
}
}

Register multiple patterns and evaluate once. Every deviation in the log is surfaced, and gap_analysis highlights processes that were one step away from a deviation.

The pattern across all three examples

PatternWhat it detectsStagesKey mechanism
Skipped approvalMissing step in process2 + negationunless between checks for the missing step
Out-of-orderReversed process steps2, no negationStage ordering = expected sequence; match = reversed
SLA timeoutExcessive processing time2 + gap + negationgap 5.. enforces time bound; negation excludes cancellations

Mapping your data

XES event log entries map to fabula edges as follows:

Real-world fieldFabula edge
case:concept:name (case ID)source node
concept:name (activity)label value
org:resource (performer)target node
time:timestampinterval start

In XES, all events in the same case share a case ID -- this becomes the source node, enabling joins across activities in the same process instance.


How fabula compares

  • vs ProM / Disco / Celonis: Petri net conformance checking and process discovery. These tools model the full process as a net and check event logs for conformance. No Allen algebra for temporal relations, no incremental streaming (batch-only replay), no variable-bound negation. Fabula is pattern-first: you describe the deviation, not the entire process.
  • vs Declare / LTLf: Constraint-based process modeling with boolean satisfaction (a trace either satisfies a constraint or does not). No graduated matching -- no gap analysis showing how close a trace came to violating a rule. Fabula's why_not provides clause-by-clause breakdown of near-misses.

Where to go next