Skip to main content

Event models

An event model scores one event from that event's own fields. It reads no entity history and no entity state: a payment, a login or a claim is scored from the fields your contract approves as features. Use it when the decision is about the event itself and an event-level outcome exists to learn from (a chargeback that names the payment it disputes, a fraud review that names the claim it judged). Use a sequence model when the decision is about the entity and its behavior over time.

Event models follow the same lifecycle as sequence models. They have labels that can be corrected, archived and restored; point-in-time dataset builds; cancellable training; an evaluation report with gates G1–G6 under your promotion policy; and fingerprinted bundles on deployment slots. Synchronous scoring with exact per-field attributions follows in a later release (see Activate a bundle). They are reached through their own operations, so the released model, label, dataset and bundle responses are unchanged.

StepOperationsWhat it does
Event labelsPOST /event-labels, GET, PATCH, DELETE, :restoredeclare an event-level target fed by a connected table
Event dataset buildsPOST /event-datasets, GETassemble the labeled events, point in time, with a frozen feature layout
TrainingPOST /event-models, GET, :cancel, :archivetrain one deterministic model per label, calibrated
EvaluationGET /event-models/{model_id}/eval-reportthe report and gates G1–G6, produced automatically after training
BundlesPOST /event-models/{model_id}/bundles, GET, GET /event-model-bundles/{bundle_id}the immutable, fingerprinted artifact a slot serves
ActivationPOST /slots/{slot_id}:activateactivate the bundle on a slot of its entity type

Event labels​

An event label is a supervision target attached to individual events. Its observations come from a connected batch table whose rows each name the event they are about:

{
"name": "disputed",
"task": "binary",
"event_types": ["payment.authorized"],
"source": {
"kind": "label_table",
"source_id": "src_00000000000000000000000000",
"event_id_column": "payment_event_id",
"value_column": "disputed",
"observed_at_column": "disputed_at"
},
"maturation_days": 30
}
  • event_types: the only event types this label supervises. A dataset row of another type is masked event_type_not_labeled for this label. It is never assumed negative.
  • maturation_days (required, at least 1): the label horizon. An event's target is the label as known at timestamp + maturation_days: the latest observation at or before that instant. Later observations are ignored, and a build warns horizon_too_short when they outnumber the observations its targets used.
  • task: binary, multiclass (2 to 16 classes) or regression.
  • null_semantics: by default an event with no observation within its horizon is masked (unobserved), never negative. The opt-in assume_negative_after_maturation makes it negative, and needs a guard_min_positive_rate that fails the build when the positive rate falls below it.
  • Names are unique across your entity and event labels (409 already_exists), so score maps keyed by label name stay unambiguous.
  • Labels are never inputs. The value column is registered with the leakage policy, so a same-named event attribute is target_only.

Each table row becomes an observation keyed by its content, so re-syncing a table never duplicates one. A row with an empty event id, or one longer than 128 characters, is rejected as label_event_id_missing. Rows observed more than an hour in the future are rejected, as for entity labels.

PATCH changes description, maturation_days, null_semantics, backdating_limit_days and column renames at any time. name, task, classes and event_types can change only until the first observation is ingested. DELETE archives the label: it is hidden from the default list and refused by new builds (label_archived), its name stays reserved, and builds that already pinned it are unaffected. :restore un-archives it.

Event dataset builds​

{
"entity_type": "account",
"event_types": ["payment.authorized"],
"labels": ["lbl_00000000000000000000000000"],
"time_range": {
"start": "2026-01-01T00:00:00Z",
"end": "2026-09-30T23:59:59Z"
},
"split": {
"method": "time",
"val_start": "2026-07-01T00:00:00Z",
"test_start": "2026-08-15T00:00:00Z"
}
}

A build pins every input: the event-store snapshot, the approved contract, the leakage policy, each label's observation store and the ingest-lag policy. The same request twice is one build. It runs as a Job, and its rows are the snapshot's events of event_types for entity_type with a timestamp inside time_range.

Targets, point in time. For each event and label, the build reads the observations as of the snapshot and applies these rules in order:

  1. an event type the label does not supervise is masked event_type_not_labeled;
  2. an event_id shared by different events is masked ambiguous_event_id;
  3. a horizon after the snapshot is masked immature, and a horizon after the split's cut point is masked matures_after_cut;
  4. otherwise the latest observation within the horizon is the target. It is masked observed_before_event when it predates the event, and unparseable when its value does not parse;
  5. with no observation in the horizon the event is masked unobserved, or is negative under the opt-in.

The manifest counts every mask reason per label and split.

Splits.

  • entity_hash (the default) puts every event of an entity in one split. Evaluation is then in time, and the report says so.
  • time cuts out-of-time validation and test windows at val_start and test_start. Each split's targets are attached as of its own cut point, so an event whose horizon crosses the next cut is masked: an embargo of maturation_days before each cut. No train or validation target uses label knowledge from a later window.

Every binary label needs at least one positive and one negative target in every split (insufficient_label_support).

Duplicates and size. Events that share an event_id, entity, type and timestamp are one event delivered twice: the earliest received is kept. A build that matches more than max_events (default and maximum 10,000,000) fails typed (event_dataset_too_large) rather than sampling silently.

Backfilled events. Features are the event's fields as accepted, and acceptance is not proof that a field was known at the event's timestamp. An event loaded from a warehouse long after it happened may carry fields written after its outcome. Each build records, per event type, how long events took to arrive (received_at − timestamp). It counts the events above ingest_lag.max_days (default 7) as backfilled_rows.

When the type also has at least 100 timely events, the build compares the two populations column by column. It names a coded column whose values appear only in backfilled events (backfill_only_values), and a numeric column whose distribution shifts between them (backfill_distribution_shift). With ingest_lag.action: exclude, backfilled events are dropped from the rows.

The feature layout. The build freezes the columns every later step uses:

  • numeric attributes become their quantile-encoded value;
  • categorical, boolean and text attributes become codes into a vocabulary of their most frequent training values (up to 254 per column). A value needs at least 20 occurrences in the train split to get its own code; any other value is out of vocabulary, and an absent field has its own code;
  • timestamp attributes become UTC hour and weekday columns;
  • the event's own type, hour and weekday are columns as well.

Only attributes your contract approves as features are projected. Identifiers, targets, metadata, denied fields and direct PII never become columns. Encoders and vocabularies are fit on the train split only.

Training​

{
"dataset_id": "eds_00000000000000000000000000",
"seed": 0
}

POST /event-models creates the model (status: training) and one training on the cpu-standard pool. The recipe event_gbm_v1 trains one deterministic model per label on that label's targets only, with early stopping on the validation split's primary metric: AUC, macro-F1 or R² by task.

  • Calibration: binary and multiclass outputs are calibrated by isotonic regression fit on the validation split.
    • A curve whose class has fewer than 50 validation positives is the identity, recorded per label as calibration_status.
    • Multiclass probabilities are renormalized to sum to 1.
  • Reproducibility: the same dataset, recipe, labels and seed reproduce byte-identical label models and calibration. An identical request returns the existing model while it is training, evaluated or promoted. The size checks below apply to a new model only, so a retried request returns its model even if it would no longer be admitted.
  • Limits: a training counts toward your plan's concurrent training limit, like a sequence run. Admission refuses a model whose heads exceed 32 (one per binary or regression label, one per class of a multiclass label), or whose work exceeds 500,000,000 units. A unit is one cell of training plus validation rows × columns × heads. The refusal is event_model_too_large.
  • Servable models only: admission also refuses a training whose bundle could, at its largest, be too big for a serving process to load. This is the same bundle_exceeds_serving_memory refusal that bundle creation makes, so you learn it before training.
    • The check uses your dataset's actual feature layout and row count, and the recipe's largest possible models.
    • Over a small feature layout, it admits at most 11 binary labels, one multiclass label of at most 7 classes, or one binary label over about 504 numeric attributes seen in training. A wider layout admits fewer.
  • Cancellation: POST /event-models/{model_id}:cancel cancels a queued training at once, and stops a running one at its next training round.
  • Recipes: the hybrid recipe event_gbm_seq_v1 (the entity's sequence state as extra features) is declared but not yet available; it is refused with recipe_not_available.

A failed training or evaluation sets the model failed with a typed error. Neither leaves a model you can bundle.

Evaluation and gates​

Evaluation starts automatically when training succeeds. The report validates against the same eval_report.json schema as a sequence model's. Each label block is computed on the test split with a bootstrap clustered by entity. The gates are:

GateFor an event model
G1per label, quality versus your previous promoted event-model bundle covering that label and entity type, minus 0.005; versus the prevalence predictor when there is none
G2calibration error (ECE, 15 bins) at most 0.05 on the served scores; not applicable to regression
G3not applicable: event models produce no entity embedding
G4input parity: 1,000 held-out test events rebuilt from the event store, featurized by the serving path, must match the stored columns byte for byte and score exactly as evaluated
G5the masked-supervision audit: the target masks recomputed from the pinned inputs must equal the stored targets; the ingest-lag measurements and warnings are in its metrics
G6the drift baseline is produced

A failing gate emits the existing gate.failed webhook with the event model's id, and it appears in GET /monitors/gate-failures. Resolve that id with GET /event-models/{model_id} and its report with GET /event-models/{model_id}/eval-report. The released model operations (GET /models/{model_id} and the rest) answer 404 for an event model.

Bundles​

{
"acknowledge_gates": {
"gates": ["G2"],
"reason": "calibration accepted for the pilot; refit after the next build"
}
}

POST /event-models/{model_id}/bundles promotes an evaluated model under your promotion policy, as createModelBundle does:

  • a failed blocking gate refuses (G4 input parity, G5 and G6 by default);
  • failed advisory gates need an acknowledgement covering exactly the failed set, with a reason;
  • strict mode refuses every acknowledgement.

The bundle is recorded with a bundle.create audit event and a gate.override per acknowledged gate. The model becomes promoted, and the existing bundle.created webhook fires.

The bundle manifest pins everything the model was built from: contract version, leakage policy, encoders, tokenizer, dataset, evaluation report, feature layout and every model artifact. Each pin is fingerprinted. GET /event-model-bundles/{bundle_id} resolves a bundle id that a slot or a webhook names.

Explanations are budgeted per bundle. Exact TreeSHAP costs grow with the trees' size. When the bundle is created, its label models' structure is checked against the single-threaded 50 ms target for one score with 20 attributions:

  • a bundle within that budget explains, with runtime.explanation {top_k_default: 5, top_k_max: 20};
  • a bundle over it gets {0, 0}. When scored, it returns scores with their exact base_value and margin but no attributions, and a request for top_k above 0 is refused with the reason.

Serving memory. A serving process must be able to load the bundle, so creation refuses one it could not load within its memory share.

  • The refusal is 400 failed_precondition with reason bundle_exceeds_serving_memory. Its details carry estimate_mib, the bundle's estimated load memory, and limit_mib, the share.
  • The estimate is the memory peak of swapping the bundle in while a bundle of the same size is still loaded: the serving process's own memory, what its requests in flight hold meanwhile, the bundle's loaded memory twice, and the larger of two load transients, parsing the encoder set or parsing the largest label model. Activation checks the swap it actually makes (see Activate a bundle).
  • The serving process's own memory is measured on a live serving process under load, serving up to 1,000 projects. What its requests in flight hold is measured over repeated swaps under 40 concurrent scoring and explanation requests.
  • The loaded memory grows with the encoder set's numeric attributes seen in training, the feature layout's columns and vocabulary entries, and the label models' trees and leaves. The label models dominate for a multiclass label: its single model holds a tree per class for every training round.

Only bundles near the training limits are refused. Taken alone, the thresholds are about 542 numeric attributes seen in training, about 1,025 categorical columns with full vocabularies, or 12 binary labels at the recipe's largest models.

Activate a bundle​

Activate an event bundle on a slot exactly as a sequence bundle, with POST /slots/{slot_id}:activate. Activation and rollback move a slot between the two families in either direction, with the usual history, generation and webhooks. An event bundle needs a slot of its own entity type (entity_type_mismatch), and allow_mismatch is refused (mismatch_override_not_supported).

Activation checks the swap it makes. A serving process loads the new bundle while the slot's current one is still loaded. Activation (and rollback) refuses a swap the serving process could not hold, with the same 400 bundle_exceeds_serving_memory refusal as bundle creation, its estimate_mib and limit_mib:

  • the estimate is the serving process's own memory and what its requests in flight hold, plus what the slot's current event bundle keeps loaded (nothing when the slot serves a sequence bundle or nothing), plus the new bundle's loaded memory and load transient;
  • each bundle is measured from its own artifacts;
  • a large bundle replacing another large one of a different shape (many binary labels replaced by many numeric attributes, for example) can be refused even though each bundle alone was created. It can still be activated on a slot that serves a smaller bundle, or none.

Scoring follows in a later release. Real-time scoring of events against an event-active slot, with exact TreeSHAP reasons for each label, ships with synchronous event scoring (WP-V17). Until then, an event-active slot serves no scores, and batch scoring refuses it (see below). The serving image already carries the native runtime that will score these bundles. Your evaluation report shows how the model scores your held-out events. G4 checks that the serving path reproduces those features and scores exactly.

What an event-active slot refuses. While a slot serves an event bundle, the operations that need entity state, or that would set up work that cannot run, answer 400 failed_precondition, reason model_family_not_supported:

  • every per-entity read: entity state, per-entity bands, markers, observation, per-entity history, the evidence timeline, the feature view, trajectories, and top entities;
  • batch scores and trajectory readiness;
  • refreshes, exports and warehouse exports;
  • creating an export schedule, and updating one other than to disable it;
  • segment rebuilds;
  • creating, previewing and materializing an audience, and changing its definition;
  • similar-entity jobs;
  • creating an alert rule, installing the went-quiet rule, configuring monitors and setting the freshness objective. Existing rules, monitors and objectives remain readable;
  • enabling observation time.

Batch jobs refuse an event-active slot the same way. That covers refreshes, segment builds, projections, audience materializations, similar-entity jobs and observation-time sweeps.

Operations that resolve the slot from the registry refuse immediately after a switch. Cached reads see the switch within the usual 30-second slot-cache bound.

What keeps working:

  • slot lifecycle and metadata (list, get, history, activate, rollback, decommission, restore);
  • slot-level bands (read, set and derive);
  • experiments;
  • deletes, and the remaining updates of existing configuration;
  • every stored record computed under an earlier sequence bundle, which stays readable and names that bundle.

Scheduled exports do not run while the slot is event-active: each tick is recorded failed with model_family_not_supported and mints no export. They resume when a sequence bundle is active again.

Archive and audit​

POST /event-models/{model_id}:archive archives a model that is not training and not active in any slot. Every write is audited: event_label.create, event_label.update, event_label.archive, event_label.restore, event_dataset.create, event_model.create, event_model.cancel, event_model.archive, bundle.create and gate.override. Builds meter build_cpu_seconds and trainings meter training_cpu_seconds, each with a job dimension naming the event job. Training completion has no webhook of its own; read it with GET /event-models/{model_id}.

Model cards and governance dossiers for event models come in a later release.