Schemas
A schema declares what an event payload looks like: which fields it carries, and what type each one is. Schemas are workspace-scoped and reusable — one schema can describe the traffic from many event sources.
A schema on its own describes; it does not reject anything. Deciding what happens when an event disagrees with the declaration is a contract’s job. The two are separate objects and it is worth keeping them apart in your head:
| Schema | Contract | |
|---|---|---|
| Scope | Workspace-wide, attached to sources | One event source, one event name |
| Says | ”These fields exist, with these types" | "Reject / warn / allow when they do not” |
| Effect on traffic | None by itself | Can block an event at ingest |
| Versioned | Yes — saving bumps the version | No |
Everything downstream reads the schema’s flattened field list: the forwarder’s column resolution, the audience filter builder’s property picker, and the warehouse table shape all come from it.
The two ways to write one
Both tabs of the schema editor describe the same definition, and switching between them converts. Save whichever you were working in.
Field list
A flat list of dot-paths, each with a type and a Required checkbox:
| Path | Type | Required |
|---|---|---|
order_id | String | ✓ |
cart.total | Number | ✓ |
cart.items | Array of Objects | |
placed_at | Timestamp |
A dot-path addresses a nested field, so cart.total declares total inside the cart
object. This is usually the faster way in, and it is the representation the rest of the
platform reads.
JSON Schema
A constrained subset of JSON Schema, which you can paste or upload as a file:
{
"type": "object",
"properties": {
"order_id": { "type": "string" },
"cart": {
"type": "object",
"properties": {
"total": { "type": "number" },
"items": {
"type": "array",
"items": { "type": "object", "properties": { "sku": { "type": "string" } } }
}
}
},
"placed_at": { "type": "string", "format": "date-time" }
},
"required": ["order_id"]
}Supported keywords are type, properties, items, required, and format: date-time.
Composition and reference keywords — $ref, allOf, anyOf, oneOf, not,
patternProperties, if/then/else, $dynamicRef — are rejected with an error rather
than ignored, because the derived field list would not represent what the schema meant.
Two bounds apply to both representations: at most 200 fields, and at most 6 levels of nesting.
Field types
| Type | JSON it describes | Notes |
|---|---|---|
String | "abc" | |
Number | 42, 99.99 | JSON Schema integer folds into this |
Boolean | true, false | |
Timestamp | "2026-03-15T14:30:00Z" | A string with format: date-time |
JSON / Object | { … } | An object whose inner fields you are not declaring |
Array | [1, 2, 3] | An array of scalars |
Array of Objects | [{ … }, { … }] | Declare the element’s fields with dot-paths |
Leaving Required unchecked marks the field nullable: it may be absent, or present as
null.
Attaching a schema to a source
An event source points at one schema through its Attached schema setting.
- Standard SDK sources always use the built-in standard schema and cannot take a custom one. It is read-only, and the editor renders it as a field catalogue rather than a form.
- Custom sources either attach a schema you wrote, or let auto-discovery infer one.
- A Braze Currents source can attach the prebuilt Braze Currents schema.
Pending schema review
A custom source with no schema attached sits in pending_schema. This state is worth
understanding precisely, because it is neither healthy nor idle:
Events are accepted and observed, and not delivered downstream — no forwarding rule sees them and nothing is written to the warehouse.
You leave it by promoting the discovered schema, attaching one you wrote, or activating the source without a schema. Traffic starts flowing within about five minutes of activation.
Auto-discovery
While a custom source is sending, ingest infers a schema from live traffic and merges what it
sees per source. Arrays are sampled by their first element, so an array of objects is
discovered as Array of Objects.
From the source’s page you can:
- Review the discovered fields and their observed types;
- Promote the discovery into a real schema, attached to that source;
- Reset it, discarding what has been observed so far and starting the inference again — useful after fixing an instrumentation bug that taught it the wrong shape.
Drift
Once a schema is attached, incoming traffic is compared against it and the source reports one of three states:
| State | Meaning |
|---|---|
| None | Traffic matches the declaration |
| New fields | Events carry fields the schema does not declare |
| Type conflict | A declared field is arriving as a different type |
Both drift states are informational — a schema does not block. The schema’s detail page
renders the difference as a diff: the declared type beside the observed one, per path, with
conflicts above merely-undeclared fields. A path that only ever arrives as its declared type
is not drift, and integer satisfies a declared Number.
To make disagreement actually reject an event, add a contract with enforcement set to Block.
Versions and status
Saving a schema bumps its version, and downstream consumers re-sync against the new one — so a change to a widely-attached schema is not a local edit. A schema can be Active or Archived; archiving keeps it readable and out of the attach dropdown.
Related
- Event Sources — where a schema gets attached
- Contracts — enforcing a shape, rather than declaring one
- Enrichment — adding fields from a Store before delivery
- Transformations — rewriting the payload in JavaScript
- Forwarding — where the declared fields become columns