
The Schemas page where joins are configured.
/api/v1/schema-joins is the CRUD surface for join definitions — declarative links between two registered schemas on a primary-key/foreign-key pair. The API accepts the same join spec the visual schema-joins builder produces in the UI. Joins drive customer-360 enrichment, segment overlap calculations, and reach-estimate queries that span more than one schema.
What it does
Each join definition records the primary and foreign schema, the key field on each side, the join type, the auto-enrich flag, an active/inactive status, and an optional list ofaggregations — one-to-many rollup rules that collapse multi-row joins into scalar decision-context values. Before persisting a join, the platform validates that both schemas exist for the tenant, that the named key fields exist on each schema, and that every aggregated field exists on the foreign schema. Invalid combinations are rejected with a 400 and a precise error message rather than allowed to fail at query time.
The route handles four operations:
- List — return every join for the tenant, each enriched with the display names of the two schemas it links.
- Create — accept a new join after schema and field validation.
- Update — change fields on an existing join, re-validating any key combinations that changed.
- Delete — hard-delete a join.
Quick start
Create a join fromcustomers to accounts so customer reads automatically pull account fields:
accounts.total_balance (the summed balances) and accounts.count (3) to the assembled decision context instead of silently picking one row.
How it works
Authentication and roles
Listing joins requires theviewer, editor, or admin role. Create, update, and delete each require editor or admin. Every handler binds the request to a tenant and scopes all database operations by tenant id — no cross-tenant joins are possible.
Field validation
On create, the platform validates each side once: it confirms the schema exists for the tenant, then confirms the named field exists on that schema. The error messages are precise:Schema {id} not found— when the schema id does not exist for the tenant.Field "{name}" not found in schema "{schemaName}"— when the schema exists but the named key column is missing.
One-to-many rollups (aggregations)
A join from customers to accounts typically matches multiple foreign rows per customer. The optional aggregations array configures how those rows collapse into scalar values in the assembled decision context (the <entity>.<key> namespace read by decisioning gates, formulas, and enrichment):
field(required) — a column on the foreign schema.countalso accepts"*"(or an empty string) for a plain row count; every other aggregator requires a real column.aggregator(required) — one ofsum,count,avg,min,max,any,all,first.alias(optional) — the output key under the entity namespace. Defaults to<field>_<aggregator>(e.g.balance_sum), orcountfor acounton"*".
Validation on create and update rejects, with a
400 and a precise message:
- an aggregated
fieldthat is not a registered column on the foreign schema —Aggregation field "{name}" not found in schema "{schemaName}"; "*"(or an empty field) with any aggregator other thancount—Aggregation field is required for aggregator "{name}" (only "count" accepts "*");- an unknown aggregator value (schema validation).
foreignSchemaId without resending aggregations re-validate the stored aggregations against the new schema, so a join can never end up with rollups pointing at columns that don’t exist.
Default rollup. When a join has no aggregations (the default, []), a one-to-many match falls back to <entity>.count plus the first row’s scalar fields passed through as <entity>.<field> — and the assembler records a warning about the implicit first-row reduction. Configure aggregators to make the rollup explicit.
The same rollups are configurable visually in the Aggregations section of the join form at Data → Schema Joins: pick a foreign-schema column, an aggregator, and an optional alias per row. The join list shows a compact summary of each join’s rollups (e.g. sum(balance)→total_balance, count), or “Default rollup” when none are configured.
List enrichment
The list response attachesprimarySchemaName and foreignSchemaName to each row, using the schema’s display name first, falling back to its internal name, then to "Unknown". This avoids a second round trip per row from the UI.
Reference
GET /api/v1/schema-joins
List all schema joins for the tenant, ordered by createdAt descending.
Response
Status codes
POST /api/v1/schema-joins
Create a new join after schema/field validation.
Request body
The platform validates the body and rejects invalid join specs with400.
string
required
Display name. Must be at least 1 character.
string
required
Id of the primary schema. Must belong to the tenant.
string
required
Field name on the primary schema. Must exist as a registered field on the primary schema.
string
required
Id of the foreign schema. Must belong to the tenant.
string
required
Field name on the foreign schema. Must exist as a registered field on the foreign schema.
string
default:"left"
"left" (default) or "inner".boolean
default:"true"
When
true, customer-360 reads and enrichment stages auto-pull this join. When false, the join exists in the catalog but is opt-in per query.array
Optional one-to-many rollup rules:
[{ field, aggregator, alias? }]. field must exist on the foreign schema ("*" allowed only with count); aggregator is one of sum, count, avg, min, max, any, all, first. Defaults to [] (default rollup). See One-to-many rollups.Response
201 Created with the created join row (including the stored aggregations).
Status codes
PUT /api/v1/schema-joins
Update fields on an existing join. Re-runs validation for any side whose schema or key changed.
Request body
The platform validates the body and rejects invalid updates with400.
string
required
Id of the join to update.
string
New display name.
string
New primary schema id. When set, key validation re-runs against
primaryKey.string
New primary key field name.
string
New foreign schema id. When set, key validation re-runs against
foreignKey.string
New foreign key field name.
string
"left" or "inner".boolean
Toggle the auto-enrichment flag.
string
"active" or "inactive". inactive joins stay in the catalog but are skipped by enrichment paths.array
Replace the join’s one-to-many rollup rules. The full array is replaced (not merged); send
[] to clear all aggregations and fall back to the default rollup. Validated against the effective foreign schema. See One-to-many rollups.Response
200 OK with the updated join row.
Status codes
DELETE /api/v1/schema-joins?id={joinId}
Hard-delete a join. The id is read from the query string, not the path.
Query parameters
string
required
Id of the join to delete.
Response
Status codes
Required headers
Honest limits
- The route does not enforce a uniqueness constraint on
{ primarySchemaId, primaryKey, foreignSchemaId, foreignKey }. Operators can register multiple joins with the same key pair (e.g., oneactiveand oneinactive) — the consumer-side enrichment code picks the firstactiverow it finds. joinTypeis restricted to"left"and"inner". Right and outer joins are not supported in V1 — enforce them by inverting the primary/foreign relationship instead.- Deletes are unconditional — no foreign-key guard prevents removing a join that an active enrichment stage relies on. Callers should set
status: "inactive"for a phased deprecation rather than deleting in production. - The delete endpoint reads
idfrom the query string, not a path param, to allow batch tools to pass the id outside the URL path.
Related
- Schemas — register the schemas referenced by
primarySchemaIdandforeignSchemaId. - Customers — Unified Profile — consumes auto-enrich joins for customer-360 reads.
- Segment Overlap — reads cross-schema joins to compute audience intersections.
- Reach Estimate — uses joins for cross-schema rule reach calculations.