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 withinsufficient_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) andcontract_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 afterstartis maskedholdout_embargo: no training example sees anything from the holdout period, not even an outcome. An entity with no event beforestartgets no train or validation row. - Test entities keep their full windows, but only labels whose
supervision event is at or after
startare graded; earlier ones are maskedbefore_holdout_start. The test evaluation is then out-of-time and out-of-entity. startis 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 enabledobservation_time.- A train or validation entity's assumed negative
(
assume_negative_after_maturation) is decided as ofstart: only an entity whose last event beforestartmatured beforestartgets 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_supportnaming 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:
POST /sources/{source_id}/semantic-inputs(admin) with{}— or{"snapshot_id": "esn_…"}(an event-store snapshot) for an exact window — returns202and a preparation inqueuedstatus. Repeating the request while it is in flight returns the same preparation; the samesnapshot_idreturns its own unless it endedrefusedorfailed, in which case a new request starts again.- Poll
GET /sources/{source_id}/semantic-inputs/{preparation_id}.pendingis retried automatically (for example while the description service is unavailable);refusedandfailedcarry a typedreason, such asencoder_recipe_mismatch,snapshot_not_foundorsnapshot_scan_budget_exceeded. Adescriptor_refusal means the description service can never describe the source as it is, so it is not retried: for exampledescriptor_no_eligible_descriptions(every field description failed the identifier check, such as a run of 7 or more digits in a field name) ordescriptor_entry_budget_exceeded.GET /sources/{source_id}/semantic-inputslists the source's preparations, newest first. - When it
succeeded,result.dataset_requestis a basePOST /datasetsbody withtokenizer_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.