Labels
A label declares an outcome to predict: name, entity type, task shape, a source of observations, and its temporal rules. The concepts are covered in Labels and masking; this page is the operational reference.
The definition
{
"name": "repeat_purchase_30d",
"entity_type": "user",
"task": "binary",
"source": {
"kind": "label_table",
"source_id": "src_00000000000000000000000000",
"entity_id_column": "entity_id",
"value_column": "value",
"observed_at_column": "observed_at"
},
"maturation_days": 7
}
task—binary,multiclass(withclasses), orregression. Binary values parse astrue/false/1/0; unparseable rows are rejected with typed reasons, never coerced.source— where observations come from:label_table(rows of a batch source),label_events(outcomes arrive on the stream as events), orderived(synthesized from the entity's own future events with zero labeling effort — see the label-free quickstart; template presets filloutcome/min_count,maturation_daysis bound to the rule'shorizon_days, andPOST /labels:previewdry-runs a rule before you create it). The preview grades the anchors in the newest days of the entity type's events:2 × horizon_days + 1UTC days (at least 28) back from that type's newest event, never later than today. It samples the entities seen on that window's days up to the onehorizon_daysbefore the censor clock (below), so their older anchors' outcomes can be judged. The sample is spread evenly by a hash of the entity key over all of them, rather than taking the newest day's entities, which are the ones still active. Without such a day it samples the newest entities. It reads further back only to find enough entities, and stops early when its read budget runs out. A store written as many small parts can hold more files in one day than the budget has left; the preview then reads that day's newest files that fit and starts the window inside the day, at the first instant from which it holds every file, so older anchors can still be graded. Its counts and examples cover only that window; the dataset build counts the whole history. Each anchor in the window gets the same value as a full-history build without a source filter taken now: its censor clock is the earlier of now and the newest event of the entity type in the store (dataset builder 1.3.0 and later). The examples show every outcome found: positive, negative and not yet observed (censored, never graded). On a store whose events ended long ago, the anchors of the last horizon before that end show as not yet observed: their horizon ends after the data, and they stay that way until newer events arrive. When the window has no events of the entity type, the preview says so with the warningentity_type_has_no_recent_events. When the preview's 12 s read limit stops the window early, it adds the warningpreview_read_limit_reached: the counts cover fewer days, and every sampled anchor can be censored because no older day was read. With nothing sampled, that warning replacesentity_type_has_no_recent_events, since the older days it did not read may hold the entity type. Running the preview again usually reads further. The warningrule_matches_nothing_in_sampleneeds at least one closed horizon (observationsabove 0). When every sampled anchor is newer than the horizon, the preview has no outcome to show and gives no rule warning: the rule has not been judged. That happens, for example, when the store holds less history than the horizon, when the read budget stops before older days, or when the sampled entities have no events older than the horizon (new entities, or an entity type ingested recently on an older store). The console says so; create the label and read its positive rate from the dataset build's label support, or preview a shorter horizon.maturation_days— the supervision waiting period.null_semantics— defaultunobserved(absence = masked). The explicit opt-inassume_negative_after_maturationrequiresguard_min_positive_rate, and the dataset build fails when the guard trips — a silently broken label join must not become "everyone is negative".
Ingestion and stats
label_table observations ingest when their source syncs; label_events
observations route at stream time. Either way they land in the
point-in-time label store keyed by (entity, observed_at), deduplicated —
re-syncing a label file never double-counts.
The entity_id_column is read with the same rule as event entity ids
(how identifiers become entity ids),
so a warehouse NUMERIC customer id 12.000000000 labels the entity 12
that the events of the same column created. A value that is not an
identifier (a boolean, a fractional number) is rejected as
label_entity_id_missing, and the sync's details count such rows per label,
column and reason (entity_ids_refused with the label_id). An event-label
table reads its event-id column the same way; give it an integer or text
column when the labeled events come from a Pub/Sub subscription,
whose event ids keep their earlier form (12.0). A batch source that stored
its event ids as 12.0 before this rule keeps that spelling
(numeric ids stored before the canonical rule):
an observation naming 12 that matches no event attaches to that source's
event 12.0, never to a text id 12.0 of another source, and an exact match
always wins (even with an event the dataset leaves out as a backfill). Each
event dataset decides which sources keep the earlier spelling when it is
built and keeps that decision, so its evaluation attaches labels exactly as
its build did. Labels stored before this
rule keep their ids (for example 12.000000000) and do not join new events
under 12. The first sync of such a label table after the rule lands those
observations again under the canonical id, which is what lets them join new
events. The earlier forms are kept as stored, so GET /labels/{id}/stats
counts both (distinct entities and observations), and the sync's details
count the re-landed rows as {type: "label_legacy_relanded", label_id, count}.
A sync that fails after storing its label rows keeps them stored, but no
sync counts those re-landed rows: a retry skips rows already stored.
GET /labels/{id}/stats serves counts, observed_at range, weekly
positive rates, and maturation coverage — check it before building.
Backdated observations (received long after observed_at) are accepted
but flagged.
Immutability
Once a label has ingested observations, name, entity_type and task
are frozen (definitions with data behind them don't quietly change
meaning). A derived label's rule is likewise replaceable only until its
first materialized dataset build — afterwards it is frozen
(derived_rule_frozen): a changed rule is a different outcome, so create a
new label. Description-class metadata, maturation_days, null_semantics
and source column renames stay patchable. Dataset builds pin label
definitions by fingerprint — a patched label is a new fingerprint, and
old builds remain exactly reproducible.
Structural safety
Creating a label registers its name and value column into the leakage
policy's deny scanning: an attribute with the label's name can never be a
model feature. The label's value_attribute cannot reference an attribute
the approved contract marks role: feature — targets and inputs are
disjoint by construction, not convention.