The workflow score
How deep and connected the seller's business processes are in the delivered data: can a buyer follow an order from
quote to payment across systems? Built in the package (workflow/), run by the reference runner
(jobs/workflow.py) at the end of a scrub. Off by default: it runs only when the claim asks for it
(workflow: true or a systems map), which also turns on business-id tokenization for the deal.
Business-object graph
A record is one table row, JSON object, or mail or chat message, tagged with the system it came from. Systems:
the claim's map (longest raw path prefix wins), else one per database or platform source, one for mail and chat,
and one per top-level item for uploads and buckets. Labels stay on the machine; reports number systems s1,
s2, ... by record count.
Business objects are the business-id tokens in a record (internal and public classes from bizids.json), plus
conversations (a mail or chat thread). Parties (person stand-ins), SKUs and actors ride along for the event log
but never join objects. Objects in one record are related.
Case: a connected group of related business objects (union-find), containing at least one business object (a thread alone isn't a case), followed across every system its records came from. Hubs: an object related to more than 200 others links nothing, and a record carrying more than 200 links is skipped, so one catch-all project or digest mail can't fold a deal into one case.
Trace: a case's timed events sorted by time then activity:
- one per time column a row has whose name says what happened (
paid_at→ paid; bookkeeping and planned dates such asupdated_atordue_datemake none; a baredateisrecorded; an unrecognized name isother); - one per terminal status value (
status: shipped), timed at the row's latest time, unless a time column already gave that activity; - one per message (
emailorchat), plus outcomes a message states in words ("payment received"), which are marked not machine-checkable.
Times parse from ISO dates and datetimes, US m/d/Y, and epoch seconds or milliseconds between 2000 and 2100.
- Hub cut-off: an object related to more than 200 others links nothing. At most 3,000,000 linked records per run; traces cut at 64 steps.
- Activities from time columns:
approved,assigned,cancelled,churned,closed,completed,created,delivered,disputed,escalated,failed,invoiced,lost,opened,paid,received,refunded,rejected,reopened,resolved,returned,sent,shipped,signed,started,submitted,won, plusrecorded(a bare date) andother. - Time columns that never make an event (any token):
accessed,birth,born,cached,changed,deadline,deleted,dob,due,effective,estimated,eta,etl,expected,expiration,expires,expiry,exported,imported,indexed,last,loaded,login,modified,next,period,planned,removed,renewal,renews,scheduled,seen,sync,synced,trial,until,updated,valid,viewed. - Terminal statuses:
approved,cancelled,churned,closed,completed,delivered,disputed,escalated,failed,lost,paid,refunded,rejected,resolved,returned,shipped,won. - Outcomes (activity → outcome type):
paid→ paid,delivered→ delivered,resolved→ resolved,closed→ resolved,completed→ resolved,refunded→ refunded,cancelled→ cancelled,returned→ returned,won→ won,lost→ lost,churned→ churned. - Exceptions:
cancelled,disputed,escalated,failed,refunded,rejected,reopened,returned. - Outcomes read from message text (not machine-checkable):
refunded,delivered,paid,resolved,cancelled. - OCEL relations: up to 20 object-to-object relations per object.
- Breadth nudge order:
finance,sales,support,supply_chain,operations,hr.
Business-id canonicalization
bizids.py finds ids in named columns and free text (a class word or a known prefix next to an id-shaped value)
and gives each object one token, [<LABEL>_<10 hex>], over its canonical core: a short letter prefix, #,
separators and leading zeros removed, so SO-001234, #1234 and Order 1234 are one order. Classes sharing a
number share the hex; the label keeps them apart and the graph joins on both. The hex is an HMAC of the core under
the deal salt, or a random token from the identity map under random stand-ins.
The format map decides only what free text is recognized, never what a token is: the defaults below, the deal's own formats from the claim, and prefixes learned from the deal's id columns after a few sightings. Learning mid-job can't split an object; ids missed before a prefix was learned show up in linkage kept. Learned prefixes never leave the machine.
bizids.json 1.0.0. Functions: sales, finance, operations, support, supply_chain, hr. A prefix is learned from a deal's id columns after 2 sightings.
| Class | Policy | Token label | Function | Words (first 6) | Prefixes | Keyed prefixes |
|---|---|---|---|---|---|---|
order |
internal | ORDER |
sales | order, sales order | SO |
– |
quote |
internal | QUOTE |
sales | quote, quotation, estimate | QT |
– |
deal |
internal | DEAL |
sales | deal, opportunity | OPP |
– |
invoice |
internal | INVOICE |
finance | invoice | INV |
in |
payment |
internal | PAYMENT |
finance | payment, receipt, transaction | RCPT |
ch, pi, py |
refund |
internal | REFUND |
finance | refund, credit note, credit memo | – | re |
subscription |
internal | SUBSCRIPTION |
finance | subscription | – | sub |
ticket |
internal | TICKET |
support | ticket, case, support request, incident | TKT |
– |
return |
internal | RETURN |
support | return, RMA | RMA |
– |
project |
internal | PROJECT |
operations | project, work order | PRJ |
– |
po |
internal | PO |
supply_chain | purchase order, PO | PO |
– |
shipment |
internal | SHIPMENT |
supply_chain | shipment, fulfillment, fulfilment, packing slip, ASN | SHP, ASN |
– |
timesheet |
internal | TIMESHEET |
hr | timesheet, payroll run | – | – |
tracking |
public | TRACKING |
supply_chain | tracking, waybill, air waybill, airway bill, AWB, consignment | – | – |
sku |
product | SKU |
supply_chain | – | – | |
customer |
personal | ID |
sales | – | cus |
Metrics
All from workflow.score.metrics. (n) is the number of cases. Ranges are the nearest-rank median and 90th
percentile: for sorted values (v_1 \le \dots \le v_m), (p_q = v_{\lceil q m \rceil}), reported with (m).
Shares are reported with their denominator.
| Metric | Formula |
|---|---|
depth.systemsPerCase |
range over cases of the number of distinct systems among the case's records |
depth.stepsPerCase |
range over cases of the number of timed events |
depth.horizonDays |
range over cases with a timed event of (last event time − first) / 86 400 |
depth.handoffs |
range over cases with at least two events that have an actor, of the number of adjacent actor changes |
linkage.multiSystemShare |
records with a business id that sit in a case spanning 2+ systems, over records with a business id |
linkage.joinedShare |
the same records in a case of 2+ records, over records with a business id |
linkage.largestComponentShare |
records in the largest case, over records with a business id |
linkage.pairs |
for systems x and y: cases containing both, over min(cases containing x, cases containing y); top 50 |
outcomes.share |
cases with at least one outcome event (paid, delivered, resolved/closed/completed, refunded, cancelled, returned, won, lost, churned), over (n) |
outcomes.types |
cases per outcome type |
variety.variants |
distinct activity sequences (consecutive repeats collapsed, cut at 64 steps) |
variety.entropyBits |
(-\sum_v p_v \log_2 p_v) over variant shares |
variety.reworkShare |
cases whose collapsed sequence repeats an activity, over (n) |
variety.exceptionShare |
cases with any exception event (refunded, cancelled, returned, rejected, reopened, escalated, disputed, failed), over (n) |
variety.functions, breadth, of |
business functions (sales, finance, ...) of the object classes appearing in cases, listed when they appear in at least 11 cases; their count; out of how many |
episodes.total |
cases with an outcome, at least episode.min_steps timed events and at least episode.min_systems systems |
episodes.machineCheckable / needsJudge |
episodes with an outcome from a time column, status or message event / only from words in a message |
episodes.gradableShare |
machine-checkable episodes over episodes |
nudge |
the first function, in the order finance, sales, support, supply chain, operations, HR, not yet present |
Linkage kept
Joins before the scrub against joins after it, on the same files. The raw pass reads the raw files (links only,
in memory) using raw (class, core) pairs as objects; the clean pass reads the delivered files using tokens.
Basis: cross-system joins (multiSystem) when the raw data has any, else any join (joined).
[ \text{kept} = \min\left(1, \frac{\text{clean}[\text{basis}]}{\text{raw}[\text{basis}]}\right) ]
met is kept at or above the target in bands.json. The raw pass has a 4 GiB budget; files beyond it are skipped
and the result says sampled: true. No joins before the scrub: kept is null with the reason.
Sampling, ranges and confidence
The clean pass reads every delivered table, JSON, NDJSON, mail and chat file, up to 3,000,000 linked records
(records.truncated past that); single JSON documents over 256 MiB are skipped. Ranges and shares carry their
sample sizes; no confidence intervals are computed. A share over a small denominator is withheld (below), not
qualified.
Bands
workflow/bands.json 1.0.0: at least 11 cases to score; counts under 11 withheld; linkage-kept target 0.95; an episode needs 3 steps, 2 systems and an outcome.
| Metric | Meaning |
|---|---|
cases |
cases (connected groups of business objects) |
systems_p50 |
median systems per case |
steps_p50 |
median timed events per case |
horizon_days_p50 |
median days from a case's first event to its last |
outcome_share |
share of cases ending in a verifiable terminal state |
multi_system_share |
share of records carrying a business id that join a case spanning two or more systems |
episodes |
cases with at least episode.min_steps steps, episode.min_systems systems and an outcome |
linkage_kept |
joins that survive the scrub: joined records after the scrub over joined records before it, capped at 1 |
| Band | cases |
systems_p50 |
steps_p50 |
horizon_days_p50 |
outcome_share |
multi_system_share |
episodes |
linkage_kept |
|---|---|---|---|---|---|---|---|---|
| Deep | 11 | 3 | 8 | 14 | 0.5 | 0.5 | 1000 | 0.95 |
| Connected | 11 | 2 | 4 | – | 0.25 | 0.2 | 100 | – |
| Shallow | 11 | – | – | – | – | – | – | – |
Every listed threshold is metric >= value; – means the band doesn't test that metric. Confidential-deal buckets:
| Value | Bucket edges |
|---|---|
| systems | 1, 2, 4, 7 |
| steps | 1, 4, 10, 25 |
| horizon_days | 0, 7, 30, 90, 365 |
| count | 11, 100, 1000, 10000, 100000 |
| share_step | 0.1 |
band is the first band, top to bottom, whose every listed threshold holds (metric >= value, null fails). Under
the minimum case count the band is "Not measured" with reason "too few cases"; when none applies, "Not measured"
with "no band applies".
OCEL 2.0 export
clean/__workflow__/ocel.json.gz, written as chunks through the residual gate like every delivered byte (a hit
holds it), next to metrics.json (the privacy pass's copy below). JSON following the OCEL 2.0 JSON format:
| Key | Content |
|---|---|
objectTypes |
[{name, attributes: []}], one per object class used: bizids classes, conversation, party, actor, and other id classes that ride along (product codes) |
eventTypes |
[{name, attributes: [{system: string}, {checkable: boolean}]}], one per activity used |
objects |
[{id, type, attributes: [], relationships: [{objectId, qualifier: "related"}]}]; relationships are co-occurrence, hubs excluded, at most 20 per object |
events |
[{id: "e1"..., type, time (UTC ISO, seconds), attributes: [{system: "s<k>"}, {checkable}], relationships: [{objectId, qualifier: <object class or "actor">}]}] |
Object ids are the stand-ins the delivered files already use (ORDER_<hex>, [ID_<hex>], user_<hex>@...invalid,
or the actor's stand-in); conversations are conversation_<n>. Activities come from the fixed vocabulary only, never
from headers or values. Events are emitted in record order, only those with a time. Times are what the scrub left,
so under deal_offset they're shifted like the delivered files.
Estimate model
Estimate v2 is not built. Nothing in this package or the reference runner computes a pre-scrub estimate of the workflow score. One control plane has a v1 estimate from what the seller declares in a form, not from data; it has no calibration against measured scores. Calibration status: none. Not live.
Privacy controls
workflow.score.private makes the only copy that leaves the runner (scrub report, buyer summary, bundle):
- counts and samples under 11 are withheld (null); system pairs, outcome types and functions under 11 are dropped;
- systems are numbered with their function, never named; activities and outcomes come from a fixed vocabulary;
- confidential deals get buckets only (edges above): no pairs, no per-system rows, no outcome types, no function names;
- linkage kept over fewer than 11 joins is withheld with "too few joins to report".
The raw pass keeps only object keys in memory for the run; nothing raw is written.
Known limitations
- Some id formats in free text are only partly recognized (coverage row
hipaa.R).
- Hubs are dropped wholesale, so a genuinely central object (a big account's master project) links nothing.
- Activity names come from column names; a column named in the seller's own terms (
zeta_at) becomesother. - A status event takes the row's latest time, which may be an update time rather than when the status happened.
- Under
dates: year(Safe Harbor) dates become bare years, which don't parse as times: traces lose their timed events and depth metrics are near zero. Inferred from the code, not measured. - No confidence intervals; no check that the same buyer can reproduce a figure from the OCEL file beyond the shared code path.
- Machine-checkable means the outcome came from a structured field, not that it was verified against anything.