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 |
| Prerequisites | What is Sifting? |
1. Skipped approval
A purchase order is fulfilled without the required approval step.
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.
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.
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
| Pattern | What it detects | Stages | Key mechanism |
|---|---|---|---|
| Skipped approval | Missing step in process | 2 + negation | unless between checks for the missing step |
| Out-of-order | Reversed process steps | 2, no negation | Stage ordering = expected sequence; match = reversed |
| SLA timeout | Excessive processing time | 2 + gap + negation | gap 5.. enforces time bound; negation excludes cancellations |
Mapping your data
XES event log entries map to fabula edges as follows:
| Real-world field | Fabula edge |
|---|---|
| case:concept:name (case ID) | source node |
| concept:name (activity) | label value |
| org:resource (performer) | target node |
| time:timestamp | interval 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_notprovides clause-by-clause breakdown of near-misses.
Where to go next
- Getting Started — Build and evaluate patterns in Rust.
- Debugging Patterns — Troubleshoot with gap analysis.
- DSL Reference — Full syntax for patterns, graphs, and compose operators.
- Compliance Checking — Related use case with a regulatory focus.