Object-Centric Process Mining Mastery
Training Package · 2026
1 case = 1 linear timeline of events.
Multiple objects interact — each with its own life.
| Before — Case-Centric | Now — Object-Centric |
|---|---|
Raw SAP tables (EBAN, EKKO, EKPO, MARA) |
Business objects with friendly names (PurchaseRequisitionItem, PurchaseOrderItem, Material) |
| Single Event Log — one timeline per case | Process Explorer — graph with multiple object types interacting |
CASE_ID as the anchor key |
Relationships between objects (1:N, N:M) modeled in the Knowledge Model |
| Locked into one perspective | Same data, multiple perspectives — switch lenses without re-extracting |
| Custom SQL needed for cross-entity logic | PU functions (PU_FIRST, PU_SUM) walk the graph automatically |
| Term | What it is | Analogy |
|---|---|---|
| Space | Top-level folder grouping multiple Packages | Drive root / department area |
| Package | Versioned bundle of Views, Apps, Knowledge Models & Action Flows | Git repository |
| Knowledge Model | Definition of objects, attributes, KPIs and relationships | Semantic schema / dictionary |
| View | A single screen — KPIs, charts, tables, filters | Dashboard page |
| App | Multiple Views chained with navigation rules | Mini business application |
| Action Flow | No-code automated workflow (triggers + modules) | Zapier / Power Automate |
| Process Orchestration | Engine that coordinates humans, AI, bots and Action Flows end-to-end | Conductor of the orchestra |
Celonis Assistant: generates PQL, explains KPIs, suggests visualizations.
Already available — no setup required. Helps you build faster.
Action Flows with OpenAI / LLM modules · Smart Forms that pre-fill fields based on context.
Example for our app: suggest the 3 most likely Materials for each Free-Text PR — done in PQL + ML signals.
AI that reads context, decides and acts inside Process Orchestration — with human escalation when needed.
Example: an agent that triages 80% of Free-Text PRs autonomously, only escalating ambiguous cases to a Buyer.
| Type | How it works | When to use | Precautions |
|---|---|---|---|
| FULL | Re-extract entire table on each run | Small master tables (T001, LFA1, MARA with filters) | High cost in volume; execution window |
| DELTA | Only new/changed records (filtered by timestamp/CHANGEDATE) | Large volatile tables (EKKO, EKPO, BKPF, EBAN) | Requires reliable delta column; deletes handled separately |
| REPLICATION | Continuous / near real-time copy via Replication Cockpit | Low-latency use cases; real-time monitoring | Requires Replication Cockpit setup & monitoring |
| Signal | What it means | How to react |
|---|---|---|
| Healthy | Last run succeeded, latency within SLA | Monitor periodically |
| Warning | Latency above threshold OR row anomaly | Check logs · validate delta column |
| Failed | Last run errored — table is stale | Open log · identify error · re-trigger |
| Last Run | Timestamp of most recent extraction | If older than SLA → check scheduler |
| Latency | Delay between source change and Celonis | High = downstream views are stale |
| Row Δ | Variation of records since last run | Sudden drop = possible filter issue |
| Pattern | When to use | Example |
|---|---|---|
| CASE WHEN | Conditional logic · categorization | Map document types to categories |
| COALESCE | Handle NULLs defensively | COALESCE(WAERS, 'USD') |
| LEFT JOIN | Enrich with master data | EKPO LEFT JOIN MARA on MATNR |
| WHERE | Filter before Data Model | WHERE LOEKZ = '' |
| GROUP BY + AGG | Pre-aggregate heavy logic | Sum by PO Header before linking Items |
"EBAN"."EBAKZ" AS "IsClosed"
FILTER [PurchaseRequisitionItem].[IsClosed] = 'Y' COUNT(DISTINCT PurchaseRequisitionItem.ID)
BSEG/BKPF
coexist with ACDOCA; MSEG with MATDOC.A business entity with its own attributes.
An activity/occurrence in time, linked to one or more objects.
| Scenario | How to model | MPWA Example |
|---|---|---|
| 1 : N | Foreign Key in "N" object | PO Item → Header (FK EBELN) |
| M : N | Dedicated table (junction) | PO Item ↔ Invoice Item (junction) |
Simple join, no duplicates
Prevents duplicates
❌ Without sort attribute:
3 GRs at 14:30:00 → no tiebreaker → ambiguity → perspective fails
✅ With sort attribute:
3 GRs at 14:30:00 + MIGO # → deterministic → clear sequence
| Sort Attribute | Example | Impact |
|---|---|---|
| ❌ None | 3 events, same timestamp | Perspective fails |
| ✅ Document # | MIGO #5001, #5002, #5003 | Deterministic order |
BADAT + BANFN + BNFPO
1 PO Header ├─ PO Item 1 (FK: EBELN=5001) ├─ PO Item 2 (FK: EBELN=5001) └─ PO Item 3 (FK: EBELN=5001)
PO Item ─┐
├─ Relationship (junction)
Invoice Item ┘
Avoids duplicates on BOTH sides
Override an existing attribute/event from the default template.
New objects/events, created from scratch.
| Check | If it fails | Why it matters |
|---|---|---|
| 1:N Card. | Duplicate keys | Cardinality mismatch |
| Event Sort | Same timestamp | Events can't be ordered |
| Junction | Empty/broken | M:N broken |
Immutable snapshot of the Knowledge Model state.
Activates a version for consumption by end-users.
| Aspect | Native | Augmented |
|---|---|---|
| Source | SAP (Data Pool) | Celonis only |
| Editable | No | Yes — in views |
| Use case | Analysis & metrics | Decisions & annotations |
PR Item
Triage_Decision (Enum)Buyer_Notes (String)Purchase Order Item
Payment Terms Mismatch Justification (Enum)Payment Terms Mismatch Comments (String)Approve, Reject, Need InfoSame timestamp = FK ambiguity
409 Conflict: concurrent access
NOT NULL received a record with a null value.CASE without an
ELSE.WHERE id IS NULLCOALESCE or filter before the INSERT-- Before (problematic) SELECT a.id, b.name FROM ekko a LEFT JOIN lfa1 b ON a.lifnr = b.lifnr; -- After (defensive) SELECT COALESCE(a.id, 'UNKNOWN') AS id, b.name FROM ekko a LEFT JOIN lfa1 b ON a.lifnr = b.lifnr WHERE a.id IS NOT NULL;
DISTINCT or
an appropriate aggregation.
Multiple keys on "1" side
No records after transformation
Chaotic M:N relationships
o_celonis_CurrencyConversion"
The view tries to convert a currency that is not in Celonis' default conversion table.WAERS field coming from SAPo_celonis_CurrencyConversion tableCOALESCE(WAERS, 'USD')COUNT_TABLE(...) query and the FILTER clause| SQL Function | Syntax | PQL Equivalent / Workaround | ⚠️ Issue |
|---|---|---|---|
| LAG (prev value) | LAG(col) OVER (ORDER BY x) | LAG ( column [, ORDER BY ( sort_column [sorting], ... )] [, PARTITION BY ( partition_column, ... )] [, offset ] ) | ✅ Supported |
| ROW_NUMBER | ROW_NUMBER() OVER (ORDER BY x) | INDEX_ORDER ( column [, ORDER BY ( sort_column [sorting], ... )] [, PARTITION BY ( partition_column, ... )] ) | ✅ Supported |
| RANK / DENSE_RANK | RANK() OVER (ORDER BY col DESC) | Manual ranking via PU_SUM |
⚠️ Manual workaround needed PU_COUNT_DISTINCT( [APPROX|EXACT] DOMAIN_TABLE(col), col, col > PU_FIRST(DOMAIN_TABLE(col), col) ) + 1 |
| Running Total | SUM(col) OVER (ORDER BY x) | RUNNING_SUM(col, ORDER BY x) | ✅ Supported |
| Rolling Avg (7 days) | AVG(col) OVER (ROWS BETWEEN 6 PRECEDING...) | WINDOW_AVG ( column, start, end [, ORDER BY ( sort_column [sorting], ... )] [, PARTITION BY ( partition_column, ... )] ) | ✅ Supported |
| SQL Function | Syntax | PQL Equivalent | ⚠️ Difference |
|---|---|---|---|
| Truncate to Month | DATE_TRUNC('month', date) | ROUND_MONTH ( table.date_column ) | ✅ Supported |
| Days Difference | CURRENT_DATE - signup_date | DATEDIFF ( time_unit, table.date_column1, table.date_column2 ) | ✅ Same result (PQL in chosen time unit) |
| Extract Year/Month | EXTRACT(YEAR FROM date) | YEAR ( table.column ) / MONTH(date) | ✅ Supported |
| Add Days | date + INTERVAL '7 days' | ADD_DAYS ( table.base_column, table.days_column ) | ✅ Supported |
| Filter Last N Days | WHERE date >= CURRENT_DATE - INTERVAL 'N days' | FILTER date >= TODAY() - ADD_DAYS ( table.base_column, -N ) | ✅ Supported (with negative values) |
| Month Relative to Event | FLOOR((date - start) / 30) | MONTHS_BETWEEN(date, start) | ⚠️ PQL returns X.0 (float value) |
INDEX_ORDERSLICE was not sufficient.
Applied solution: use INDEX_ORDER to generate a position for each row within
each category, and filter directly in the Get Rows of the Action Flow.
-- Position of the case ordered by date, within the status INDEX_ORDER( "PO_Header"."Document_Date", "PO_Header"."Approval_Status" ) -- In Get Rows, filter relevant records FILTER INDEX_ORDER(...) <= 50;
SLICE is not enough, remember INDEX_ORDER + FILTER — a
much more flexible combination.
RUNNING_SUM & RUNNING_TOTAL-- Cumulative Net Order Value by document date RUNNING_SUM( "PO_Header"."Net_Order_Value", ORDER BY "PO_Header"."Document_Date" ASC ) -- RUNNING_TOTAL — equivalent, with partition RUNNING_TOTAL( "PO_Header"."Net_Order_Value", ORDER BY "PO_Header"."Document_Date" ASC, PARTITION BY "PO_Header"."Vendor" )
PARTITION BY when
possible and consider pre-aggregation in the transformation.
PU Functions + BIND-- Last Invoice Number per PO Item, via m:n relationship PU_LAST( "o_celonis_PurchaseOrderItem", BIND( "o_celonis_RelationshipThreeWayMatch", "o_celonis_IncomingMaterialDocumentItem"."SystemIncomingMaterialDocumentNumber" ) )
Why BIND? The objects PurchaseOrderItem and IncomingMaterialDocumentItem are not directly related on AccountsPayableExtension perspective, but they are indirectly related by RelationshipThreeWayMatch object (via a SQL perspective, this table is the junction table between both previous Celonis objects).
PU_STRING_AGG_DISTINCTPU_STRING_AGG_DISTINCT( "PO_Header", "PO_Item"."Broad_Category", ', ' )
LOOKUPPU_LAST( "o_celonis_PurchaseOrder", LOOKUP( "O_CELONIS_PURCHASEORDER_AUG_BUYER_JUSTIFICATION", "o_custom_CelonisMembers"."NAME", ( "O_CELONIS_PURCHASEORDER_AUG_BUYER_JUSTIFICATION"."CHANGED_BY", "o_custom_CelonisMembers"."ID" ) ) )
LOOKUP assumes the source key is unique. If there are duplicates, the result is
non-deterministic — always ensure uniqueness beforehand.
INTERPOLATEINTERPOLATE( COUNT("User_Activity"."Activity_ID"), "User_Activity"."Activity_Date", '${time_range}' )
FILTERS — one well-placed filter is worth more than several
scattered ones.PARTITION BY in RUNNING_* and window functions to reduce the
calculation scope.DOMAIN_TABLES — they explode the Cartesian product.PU_STRING_AGG_DISTINCTPU_STRING_AGG_DISTINCT( "o_celonis_Material", "o_celonis_PurchaseOrderItem"."CreationDate", ', ' )
FILTER so only POs from the last 90 days count:
FILTER "o_celonis_PurchaseOrderItem"."CreationDate" >= ADD_MONTHS(TODAY(), -3);
Trigger
Get Rows
Operator
INDEX_ORDERDaysSinceAssignment. Same INDEX_ORDER pattern from the MPWA real case.
Material_ID IS NULL, ProcessingStatus='N', DeletionIndicator IS NULL, Triage_Decision IS NULL)FILTER INDEX_ORDER( "o_celonis_PurchaseRequisitionItem"."DaysSinceAssignment" DESC, "o_celonis_PurchaseRequisitionItem"."BuyerID" ) <= 20;
INDEX_ORDER instead of SLICE?Triage_Decision = 'Need Info', today they email the Requester manually and wait. Let's automate it: an orchestration that sends a Form to the Requester, pauses, and on submission writes the answer back to Buyer_Notes — closing our app's end-to-end loop.
Get Process Context → Get Rows (PR details)Create Public Form Link → Submit Form Draft (pre-fill PR ID & Description)Send Email to Requester with the form linkBuyer_Notes on the PR Item with the clarified description + urgencyTriage_Decision to NULL so the PR re-enters the Buyer queueTriage_Decision = 'Need Info' · receive form email · submit · confirm the PR reappears in the Buyer queue with updated Buyer_NotesHas_Active_PR_Last_90d)Triage_Decision · Buyer_Notes)PU_STRING_AGG_DISTINCTINDEX_ORDERINDEX_ORDER, RUNNING_SUM, PU + BIND,
PU_STRING_AGG_DISTINCT, LOOKUP, INTERPOLATE,
Augmented Attributes.