Skip to main content

Datasets

POST /datasets turns the event store into training data: one example per entity — its events in order plus masked supervision targets — assembled under a pinned contract, leakage policy and label set.

The request​

{
"entity_type": "user",
"labels": ["lbl_00000000000000000000000000"],
"window": {
"max_events_per_entity": 1024,
"time_range": { "start": "2025-01-01T00:00:00Z", "end": "2025-07-01T00:00:00Z" }
},
"split": { "method": "entity_hash", "train": 0.8, "val": 0.1, "test": 0.1, "seed": 7 }
}
  • window — keep the last N in-range events per entity. N bounds compute and context length; the platform default is 1,024.
  • split — deterministic entity-hash assignment: an entity is in exactly one split, stable across rebuilds. Every label must have at least one positive and one negative in every split or the build fails with insufficient_label_support.
  • labels: [] builds a self-supervised-only dataset (next-event modeling still learns embeddings without any label).
  • Optional pins: snapshot_id (an exact event-store snapshot) and contract_version — omitted, the build snapshots now and takes the current approved contract.

Lifecycle and idempotency​

POST returns 202 with status queued; poll GET /datasets/{id} through running to succeeded | failed | aborted_leakage. Builds are content-addressed: the fingerprint covers the snapshot, contract and policy fingerprints, label set, window, split and builder version — an identical re-POST returns the existing build instead of recomputing.

The manifest​

A succeeded build's manifest records row counts per split, per-label supervision accounting (targets attached; masked counts by reason — immature, no_supervision_event, after_snapshot, unparseable), the fitted encoder set's fingerprint (encoders fit on the TRAIN split only — no test-set statistics can leak into the representation), the empty violation report, and every input fingerprint. Its token statistics give each split's exact token total and, from dataset builder 1.2.0, each split's longest row in tokens, which sizes a training run's microbatches. From dataset builder 1.3.0 it also records the derived-label censor clock (censor_clock) and, for a holdout build, the holdout (temporal_holdout). Training runs pin the manifest; bundles pin it transitively.

Labels at the end of the data​

A derived label looks ahead from each anchor event for its horizon. An anchor whose horizon ends after the data is censored (horizon_open): masked, never graded (neither a negative nor, for an absence rule such as churn, a positive). The censor clock is the earlier of the snapshot time and the data watermark, the newest event of the build's entity type (and source, when you filter by one) in the pinned snapshot. So an export that ends weeks before you take the snapshot no longer turns its last horizon into silent negatives. An anchor whose horizon ends exactly at the watermark is graded, so with date-only timestamps an export cut part way through its last day grades that day on partial data; end the window a day earlier if your export may be cut mid-day.

A build has one watermark, not one per source. When you filter by a source, it is that source's newest event, while a derived label's look-ahead reads every source of the entity, so a later event in another source does not move the clock. Compared with the same build without the filter, that can only censor more anchors. But the builder does not detect a source that ends earlier than the watermark: if a label's outcome events come from such a source (for example a chargeback feed exported up to January while app events run to February), horizons past that source's end are still graded, as false negatives for a presence rule or false positives for an absence rule. End window.time_range at least one horizon before the earliest data end of any source the label reads. The manifest records the clock:

{
"censor_clock": {
"snapshot_time": "2026-07-01T00:00:00.000000Z",
"data_watermark": "2026-02-28T00:00:00.000000Z",
"censor_time": "2026-02-28T00:00:00.000000Z"
}
}

The eligibility preview uses the same clock, so its censored counts match the build. The label previews (POST /labels:preview, POST /pattern-labels:preview) censor at the earlier of now and the newest event of the entity type in the store (every source), the clock of a build without a source filter taken now, so they never grade an anchor whose horizon ends after the data (the same one-clock caveat applies). Ingested labels (label_events sources) keep the snapshot clock for assume_negative_after_maturation: an entity whose last event matured before the snapshot gets an assumed negative even when the export ended before an outcome could have arrived. If your export ends well before the snapshot, end the window at least one maturation period before the export's end.

An out-of-time holdout​

By default the test split holds out entities (entity_hash), not time: its entities are new, but their events come from the same period as training. Add temporal_holdout to hold out the end of the window too:

{
"entity_type": "user",
"labels": ["lbl_00000000000000000000000000"],
"window": {
"time_range": { "start": "2025-01-01T00:00:00Z", "end": "2025-07-01T00:00:00Z" }
},
"snapshot_id": "esn_00000000000000000000000000",
"contract_version": 3,
"temporal_holdout": { "start": "2025-06-01T00:00:00Z" },
"expected_base_fingerprint": "0000000000000000000000000000000000000000000000000000000000000000"
}
  • Train and validation entities keep only their events before start, then the usual last-N rule. A train or validation label observed at or after start is masked holdout_embargo: no training example sees anything from the holdout period, not even an outcome. An entity with no event before start gets no train or validation row.
  • Test entities keep their full windows, but only labels whose supervision event is at or after start are graded; earlier ones are masked before_holdout_start. The test evaluation is then out-of-time and out-of-entity.
  • start is an RFC 3339 instant with an offset and at most 6 fractional digits, inside the window: window.time_range.start < start <= window.time_range.end. The build stores it in UTC with exactly 6 fractional digits. It cannot be combined with an enabled observation_time.
  • A train or validation entity's assumed negative (assume_negative_after_maturation) is decided as of start: only an entity whose last event before start matured before start gets one, so the holdout period cannot make a training negative either.
  • Encoders still fit on the train split alone, so they never see a value from the holdout period. Every split must still have label support after the masks, or the build fails insufficient_label_support naming the label and split. The supervision audit (audit/supervision_audit.parquet) lists every masked candidate with its reason.

The eligibility preview cannot describe a holdout build and refuses one (temporal_holdout_preview_unsupported). Preview the same request without temporal_holdout, then send the preview's build_fingerprint as expected_base_fingerprint with the holdout, the preview's snapshot_id and contract_version. If anything the preview pinned changed since, the build is refused with the same pin_drift details as expected_fingerprint. The console's build form does this for you.

GET /datasets/{id}/holdout reads a build's holdout kind (entity or time_and_entity), its start, its censor clock and models_evaluated: how many distinct models have been evaluated on its test split. Every look at a holdout makes it a little less fresh; once models_evaluated exceeds fresh_holdout_recommended_after (3), build a new holdout. The released GET /datasets/{id}/recipe and /extended-recipe reads cannot carry the field and refuse a holdout build (temporal_holdout_recipe_unsupported); GET /datasets/{id}/holdout-recipe returns its replay request, which rebuilds the same dataset exactly, or upgrades it with the holdout kept when you drop snapshot_id and expected_fingerprint.

When builds abort​

aborted_leakage means a leakage gate tripped; the violation report names each offending attribute and rule. Fix the contract (usually excluded or target_only), approve, rebuild. failed with insufficient_label_support almost always means a broken label join — check GET /labels/{id}/stats before suspecting the model (with a holdout, it can also mean the holdout left a split without outcomes: move start or widen the window). failed with no_eligible_events means no readable event of the snapshot matches the entity type and source at all. Before dataset builder 1.3.0 such a build without labels succeeded with zero rows, and one with labels failed with a label error (rule_matches_nothing or insufficient_label_support); it now fails no_eligible_events, because there is no data to build or to censor labels at. A snapshot whose matching events all fall outside the window still builds.

Semantic datasets for a model you train​

For an automatically onboarded source you can train your own semantic.v2 model with the same dataset, run, evaluation and promotion steps. First prepare its semantic inputs:

  1. POST /sources/{source_id}/semantic-inputs (admin) with {} — or {"snapshot_id": "esn_…"} (an event-store snapshot) for an exact window — returns 202 and a preparation in queued status. Repeating the request while it is in flight returns the same preparation; the same snapshot_id returns its own unless it ended refused or failed, in which case a new request starts again.
  2. Poll GET /sources/{source_id}/semantic-inputs/{preparation_id}. pending is retried automatically (for example while the description service is unavailable); refused and failed carry a typed reason, such as encoder_recipe_mismatch, snapshot_not_found or snapshot_scan_budget_exceeded. A descriptor_ refusal means the description service can never describe the source as it is, so it is not retried: for example descriptor_no_eligible_descriptions (every field description failed the identifier check, such as a run of 7 or more digits in a field name) or descriptor_entry_budget_exceeded. GET /sources/{source_id}/semantic-inputs lists the source's preparations, newest first.
  3. When it succeeded, result.dataset_request is a base POST /datasets body with tokenizer_format: "semantic.v2", the source, snapshot and both pinned references. Add your labels, window and split, then build, train, evaluate and promote as usual.

Preparation uses the source's most recent events (a bounded, exact time window) and the sealed vocabulary vectors of one encoder recipe. It never trains, selects or activates a model, and automatic model selection never uses it. The console offers the same control on the source page.