Skip to main content

Schema contracts

A schema contract is the reviewed, versioned answer to one question: what may the model see? Tessera infers a draft from your data; a project admin approves it; datasets and serving bundles pin the exact approved version. An attribute you never reviewed is never a model input — deny by default.

A worked example: healthcare operations​

A hospital-operations team streams visit entities through their intake system:

{
"entity": { "type": "visit", "id": "v_2024_88213" },
"event_type": "visit.triaged",
"timestamp": "2026-03-14T08:22:00Z",
"attributes": {
"department": "cardiology",
"acuity_level": 3,
"wait_minutes": 41,
"insurer_ref": "INS-55-20931",
"notes": "patient reports chest tightness since morning"
}
}

Profiling sees four very different kinds of columns here, and the inferred draft contract treats them differently:

AttributeInferredWhy
departmentfeature / categorical_hashlow-cardinality string — safe, informative
acuity_levelfeature / quantilenumeric — encoded by its position in the observed distribution
wait_minutesfeature / quantilenumeric
insurer_refidentifier — excludedmatches identifier heuristics (*_ref, unique-per-entity); identity is never a feature
notesexcluded until reviewedfree text touching patient details — a human must decide

The review is where domain judgment enters. The operations team might:

  • keep department, acuity_level, wait_minutes as features;
  • mark notes excluded outright (free-text clinical detail — the model may not see it, in any form);
  • mark a discharged_against_advice attribute target_only — it is the outcome they want to predict, so it may feed label extraction but can never be an input.

Roles: the vocabulary of visibility​

Every attribute of every included event type carries exactly one role:

RoleModel input?Readable by
featureyestokenizer
target_onlyneverlabel extraction only
identifiernevernothing (also value-scanned at build)
metadataneveroperational tooling
excludednevernothing

Transport and integrity metadata defaults to metadata: the service_transport, service_handler, service_md5 and service_uploaded family, IP-address names (ip, ip_address, ipv4, ipv6, client_ip, sender_ip, remote_ip, remote_addr, x_forwarded_for), and hash or checksum names (md5, sha1, sha256, checksum, crc32), each matched as the whole key or as its ending after an underscore. Names that carry a network address are also marked pii: quasi. Bare business fields such as transport_mode or handler are not matched. The default applies wherever a contract is inferred: column mappings, batch catalogs, streaming observations and automatic onboarding. An IP-named field that is unique per entity is inferred as an identifier and still marked pii: quasi. This is only a default: a role you set in an approved contract, a field you pick, or a field of an accepted source mapping always wins.

Two structural rules back this up. First, the tokenizer only ever emits tokens for role: feature attributes of status: included event types — target_only and identifier values cannot reach the model through any code path. Second, there is no allow-by-pattern anywhere: name patterns can deny (see leakage gates), never allow.

How an attribute is recognised as an identifier​

Inference marks an attribute identifier when any of these holds:

  • its name matches an identifier pattern: *_id, *_uuid, email, phone, ssn, card*, or contains a record-key word as a whole word (invoice, order, transaction or txn, receipt, ticket, sku, serial, uuid, guid; invoice_no matches, reorder_level does not). A name that also carries a measure or descriptive word (order_total, invoice_amount, ticket_price, order_date, receipt_status) is not treated as a key;
  • it is text whose values are almost one-per-entity (95% or more);
  • it is an integer whose values are almost all distinct (98% or more of the non-null events, over at least 1,000 events) and whose name is not a measure word (quantity, price, amount_cents, ...), for example an event sequence number.

Things to know about these rules:

  • The record-key words are checked before the leakage name patterns, so a name that contains one and also looks like a label (order_label) is inferred as identifier, not target_only.
  • order is a deliberately conservative match: display_order and sort_order are inferred as identifiers too, and PATCH will not make them features (the guard uses the same name rule). Keep them as metadata or excluded, or rename the column.
  • The integer uniqueness test needs at least 1,000 events in the profile, so it does not apply to the automatic onboarding path, which profiles 256 rows. On streaming and merged sources the counts come from partial observations and the test is more conservative (it marks fewer attributes, never more).
  • The PATCH check reads the profile at the time of the request, so the integer uniqueness test reflects the data seen so far, not the draft's inference time.

A number with decimals, or an integer that repeats (a quantity), is never marked an identifier from how often its values differ.

The same rule is enforced when you edit a draft: PATCH /contracts/{version} refuses role: feature for an attribute whose name matches, or whose integer values meet the uniqueness test above, with 400 failed_precondition and details[0].reason == "identifier_as_feature". It applies whatever the attribute's current role is, so moving an identifier to metadata first does not get around it. A text attribute that was marked an identifier only because its values are unique can be re-roled by a reviewer; its values are still scanned at build.

Versions, approval, and drift​

Contracts are immutable versions with a fingerprint: draft → approved → superseded. When new attributes appear in your stream (a new client release adds triage_source), they accumulate as observations; the next inferred draft includes them — as excluded, until someone approves otherwise. Diff any two versions with GET /contracts/{version}/diff.

Automatic onboarding approves a new version only when what the contract or the source's binding says has changed. A re-proposal that changes neither (more records of the same shapes, or a batch without an optional field) approves nothing, and the binding keeps the evidence_records and role_probability of its first approval. Every version it does approve is in the audit log as contract.approve, by the service actor automatic-onboarding.

Approving a contract is an act with teeth: dataset builds pin the contract version they were built under, and a serving bundle re-verifies that pin before it ever scores. The model that runs in production can only see what was approved — provably, not by convention.

In the quickstart you approve your first inferred contract in step 3.

You can also declare fields before any data arrives — their types, privacy classes and proposed roles — so a draft holds them and you can approve them before the first event: see Field declarations.

Next: Presence — what the model sees when a value is absent.