Create Audience Criteria
Create an audience criteria from its complete definition (query blocks, filters, time window, aggregations, and compositions).
POST /api/audience-criterias
Creates an Audience Criteria from its complete definition. It is the central endpoint of the resource: it receives the entire definition tree —query blocks, projection, filters, time window, joins, aggregations, and compositions with other audiences— and persists it. The operation is transactional: the criteria, its query blocks and its compositions are created together, or nothing is created.
Auth: Required — audience-criteria:create permission
Request Body
Root level (CreateAudienceCriteriaDto):
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Criteria name. Cannot be empty. |
description | string | No | Free-text description. If omitted, the response returns it as null. |
queryBlocks | QueryBlockDto[] | Yes | Minimum 1 element. Each block ≈ a complete SQL statement. |
composedWith | CreateCompositionDto[] | No | Composes this criteria with other audiences via set operations. |
queryBlocks[] — query block
Each element of queryBlocks (QueryBlockDto) is a complete SQL statement:
| Field | Type | Required | Description |
|---|---|---|---|
order | number (integer) | Yes | Block position; integer ≥ 0. |
setOperator | SetOperatorEnum | No | Combines this block with the previous one. Absent in the first block; required in the rest (see rules). |
from | FromDto | Yes | Data source: main table + joins. |
select | SelectDto | Yes | Projection (columns and/or aggregations). |
timeWindow | TimeWindowDto | No | Block time window (discriminated union by timeOperator). |
where | WhereDto | Yes | Predicate tree. Required even if it is an empty group. |
groupBy | ColumnRefDto[] | No | Grouping columns. |
having | HavingDto | No | Filter over aggregates. |
A query block's setOperator combines blocks within the same criteria; it must
not be confused with a composition's
setOperator, which combines entire criteria. They are two distinct levels of set operation.
ColumnRefDto — column reference
Used throughout the tree (select, filters, group by, joins, aggregates, time window):
| Field | Type | Required | Description |
|---|---|---|---|
table | string | Yes | Catalog table. Non-empty. |
column | string | Yes | Column. Non-empty. Can be a STRUCT path (e.g. "pricing.final_amount"). |
Every column is always qualified with its table to avoid ambiguity.
from — data source (FromDto)
| Field | Type | Required | Description |
|---|---|---|---|
table | string | Yes | Main catalog table. Non-empty. |
joins | JoinDto[] | No | Joins with other tables. Default []. |
unnests | ColumnRefDto[] | No | Repeated columns declared as a row source (one row per element). An audience does not accept them: omit the field or send [] (the default); a non-empty list gives 400. |
Each JoinDto:
| Field | Type | Required | Description |
|---|---|---|---|
type | JoinTypeEnum | Yes | INNER | LEFT | RIGHT | FULL. |
table | string | Yes | Table to join (from the catalog). Non-empty. |
on | JoinPredicateDto[] | Yes | Join conditions. |
Each JoinPredicateDto is a pair of columns: { left: ColumnRefDto, right: ColumnRefDto }.
select — projection (SelectDto)
| Field | Type | Required | Description |
|---|---|---|---|
distinct | boolean | Yes | Applies SELECT DISTINCT. |
items | (SelectColumnItem | SelectAggregateItem)[] | Yes | Non-empty. Discriminated union by the kind field. |
The items are discriminated by kind:
-
Column —
kind: "column":Field Type Required Description kind"column"Yes Discriminator. columnColumnRefDtoYes Projected column. aliasstring No Must match ^[A-Za-z_][A-Za-z0-9_]*$. -
Aggregate —
kind: "aggregate":Field Type Required Description kind"aggregate"Yes Discriminator. aggregateAggregateDtoYes Aggregation expression. aliasstring Yes Here the alias is required. Same identifier regex.
AggregateDto:
| Field | Type | Required | Description |
|---|---|---|---|
aggregationOperator | AggregationOperatorEnum | Yes | COUNT | SUM | AVG | MIN | MAX. |
operand | object | Yes | What is aggregated. Discriminated union on kind: column, star or arithmetic (see below). |
distinct | boolean | No | Applies DISTINCT within the aggregate. |
operand — what is aggregated — is discriminated by kind:
kind | Additional fields | Meaning |
|---|---|---|
column | column: ColumnRefDto | A catalog column. |
star | (none) | *: counts rows. Only valid with COUNT and without distinct (COUNT(DISTINCT *) is not allowed). |
arithmetic | arithmeticOperator: ArithmeticOperatorEnum, left, right | Binary expression. Each side is another operand: a column or a nested arithmetic (never star). |
ArithmeticOperatorEnum: ADD | SUBTRACT | MULTIPLY | SAFE_DIVIDE (a division that yields
NULL when the denominator is zero instead of failing the query). The columns of an arithmetic
expression must be numeric (INT64, NUMERIC, BIGNUMERIC, FLOAT64), and expressions nest up to
3 levels of arithmetic, counting the outermost one.
where — predicate tree (WhereDto)
The where is a recursive tree. The root node is always a group, and its leaves come in
two kinds:
kind | What it filters |
|---|---|
group | Not a leaf: it groups children with AND / OR. Groups can be nested. |
predicate | A scalar column of a table in the from. |
recordPredicate | A RECORD REPEATED column: at least 1 element satisfies the sub-filter. |
Group — kind: "group"
| Field | Type | Required | Description |
|---|---|---|---|
kind | "group" | Yes | Discriminator. |
logicalOperator | LogicalOperatorEnum (AND | OR) | Yes | Connective between the children. |
children | (Group | Leaf)[] | Yes | Group children; they can be nested groups. |
logicalOperator is always required in a group, but it only takes effect with
2 or more children. With fewer than 2 children its value is irrelevant (it generates the
same SQL), but it must be sent anyway. A where with no filters is expressed as an
empty group: { "kind": "group", "logicalOperator": "AND", "children": [] }.
Column leaf — kind: "predicate"
Compares a scalar column qualified by its table.
| Field | Type | Required | Description |
|---|---|---|---|
kind | "predicate" | Yes | Discriminator. |
column | ColumnRefDto | Yes | Predicate column. |
comparisonOperator | ComparisonOperatorEnum | Yes | Comparison operator. |
value | PredicateValue | Depends | Its shape depends on the operator. |
{
"kind": "predicate",
"column": { "table": "user_properties", "column": "country" },
"comparisonOperator": "IN",
"value": ["CL", "MX"]
}Record leaf — kind: "recordPredicate"
Filters by a RECORD REPEATED column (an array of structs, such as attributes).
It means at least one element of the array satisfies the sub-filter.
| Field | Type | Required | Description |
|---|---|---|---|
kind | "recordPredicate" | Yes | Discriminator. |
column | ColumnRefDto | Yes | RECORD REPEATED column of a table in the from. |
subWhere | Relative sub-tree | Yes | Conditions over the element's sub-fields. It cannot be left empty. |
{
"kind": "recordPredicate",
"column": { "table": "event_transactions", "column": "attributes" },
"subWhere": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{ "kind": "predicate", "field": "key", "comparisonOperator": "EQUALS", "value": "canal" },
{ "kind": "predicate", "field": "value", "comparisonOperator": "EQUALS", "value": "web" }
]
}
}The subWhere conditions are evaluated over a single element of the array, not over
the whole array. The example above selects rows that have an attribute whose key is
canal and whose value is web — not rows that have an attribute with that key
and another, different one, with that value.
Which columns accept a record filter is stated by the
catalog: the ones that come with
kind: "record".
Relative sub-tree
The subWhere is an AND/OR group whose leaves are resolved against the sub-fields the
record declares.
There is a single difference with the top-level tree, and it is the most common source of errors:
Inside a relative sub-tree the operands carry no table: you write
"field": "key", not "column": { "table": "attributes", "column": "key" }. The leaf uses
the field key (a string), not column.
Leaves allowed inside a relative sub-tree:
- Scalar leaf —
{ "kind": "predicate", "field": string, "comparisonOperator": …, "value"?: … }. Thefieldmust be a scalar sub-field of the record. - Nested record —
{ "kind": "recordPredicate", "field": string, "subWhere": … }. A record inside another record; it nests with no declared limit, as long as the catalog declares each level. - Group — the same group shape, to combine the previous leaves with
AND/OR.
Shape of value by comparisonOperator
| Operators | Shape of value | Example |
|---|---|---|
IS_NULL, IS_NOT_NULL | No value (omitted) | (omitted) |
IN, NOT_IN | Non-empty list ScalarValue[] | ["CL", "MX"] |
BETWEEN | Pair [min, max] | [10, 20] |
The rest (EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, CONTAINS, NOT_CONTAINS, STARTS_WITH, ENDS_WITH) | Single scalar | "activo" · 42 |
ScalarValue = string | number | boolean. Additionally, each operator must be
compatible with the column's type (see validation rules).
groupBy — grouping columns
Optional list of ColumnRefDto[]. When the block aggregates (declares groupBy or has an
aggregate in the select), the groupBy must match exactly the non-aggregated
columns of the select (see validation rules).
having — filter over aggregates (HavingDto)
Same recursive structure as where (nested AND/OR groups and the same value rule per
operator), but the leaf operates over an aggregate, not over a column:
- Predicate (leaf) —
{ kind: "predicate", aggregate: AggregateDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }. - Group — identical to the one in
where.
timeWindow — time window (TimeWindowDto)
Discriminated union by timeOperator. Constrains the block to a time period over a
TIMESTAMP column from the catalog (reference). It is a sibling field of where, not
a node of the filter tree.
timeOperator | Additional fields |
|---|---|
ALL_TIME | (none) — no time constraint. |
IN_THE_LAST | reference: ColumnRefDto, unit: TimeUnitEnum, value: integer > 0 |
IN_THE_FIRST | reference: ColumnRefDto, unit: TimeUnitEnum, value: integer > 0 |
SINCE | reference: ColumnRefDto, from: string (YYYY-MM-DD) |
BETWEEN | reference: ColumnRefDto, from: string (YYYY-MM-DD), to: string (YYYY-MM-DD) |
TimeUnitEnum: DAYS | WEEKS | MONTHS | YEARS.
Business semantics of each operator:
ALL_TIME— no window; considers the entire history.IN_THE_LAST— rolling window relative to "now": the lastvalueunit(e.g. last30 DAYS). Recomputed on each run.IN_THE_FIRST— window relative to the origin of the user's lifecycle (currently, their signup date): the firstvalueunitof each user's life. It is anchored per user, not to a fixed date.SINCE— from the civil datefrom(inclusive) onward.BETWEEN— betweenfromandto, both civil dates inclusive of their full day.
Civil dates YYYY-MM-DD (SINCE, BETWEEN) are resolved to the start of the day in
the tenant's timezone. The timezone detail is internal; it is enough to send the civil
date.
composedWith[] — composition with other audiences
Each element (CreateCompositionDto) combines this criteria with another audience
via a set operation:
| Field | Type | Required | Description |
|---|---|---|---|
operandCriteriaId | string (UUID) | Yes | Id of the other audience (operand). Must exist. |
setOperator | SetOperatorEnum | Yes | UNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT. |
Validation Rules
The request goes through two layers of validation, both returning 400 Bad Request:
- DTO validation (structure, types, enums, required fields). In this case
messageis an array of strings and unknown properties are rejected. - Business validation (the criteria must generate valid SQL that is coherent against
the catalog). In this case
messageis a single string with the message of the violated rule. Business validation stops at the first violated invariant.
Business rules verified (all return 400, with the exact message from the API):
| Rule | Message |
|---|---|
| The table must belong to the catalog. | Table "<t>" is not in the catalog |
Each (table, column) must belong to the catalog. | Column "<t>.<c>" is not in the catalog |
| A repeated column cannot be used outside a record filter. | La columna "<t>.<c>" es una columna repetida y no puede usarse en <contexto> |
| A column leaf requires a scalar column. | La columna "<t>.<c>" es una columna repetida: no admite un filtro de columna |
| A record filter requires a repeated column. | La columna "<t>.<c>" no es una columna repetida: no admite un filtro de registro |
| The sub-field of a record filter must exist in the record. | El sub-campo "<f>" no existe en el registro |
| A scalar leaf of the sub-tree cannot point at a repeated sub-field. | El sub-campo "<f>" es una columna repetida: usá un filtro de registro anidado |
| A nested record requires a repeated sub-field. | El sub-campo "<f>" no es una columna repetida |
| The sub-tree of a record filter cannot be left empty. | Un filtro de registro no puede quedar sin condiciones |
| A leaf of the tree must declare its operand. | Una hoja del filtro no declara una columna |
The star operand (*) is only valid with COUNT. | '*' is only valid with COUNT, not <op> |
COUNT(DISTINCT *) is not allowed. | COUNT(DISTINCT *) is not allowed |
star cannot be a side of an arithmetic expression. | '*' sólo vale como operando directo de COUNT, no como lado de una expresión aritmética |
| The columns of an arithmetic expression must be numeric. | El operando "<t>.<c>" es de tipo <type>; una expresión aritmética sólo admite operandos numéricos (INT64, NUMERIC, BIGNUMERIC, FLOAT64) |
| An arithmetic expression nests at most 3 levels. | La expresión aritmética supera la profundidad máxima de 3 niveles |
Every referenced column must belong to a table in the from (main ∪ joins). | Column "<t>.<c>" references a table that is not in the query block FROM |
When aggregating, every non-aggregated column of the select must be in the groupBy. | Column "<t>.<c>" must appear in GROUP BY when the operation aggregates |
Every column in the groupBy must be a non-aggregated column of the select. | Column "<t>.<c>" in GROUP BY must be a non-aggregated column of the SELECT |
having requires an aggregation context (groupBy or an aggregate in the select). | HAVING requires an aggregation context (GROUP BY or an aggregate in the SELECT) |
The criteria must project its identity column (e.g. user_id) as a column. | Criteria requires at least one operation that selects its identity column (e.g. user_id) |
The first block (order=0) does not carry a setOperator. | First operation (order=0) must not have a setOperator |
Every subsequent block (order>0) must carry a setOperator. | Operation with order=<n> must have a setOperator |
| The operator must be compatible with the column/aggregate type. | Operator <op> is not allowed for <type> operands |
Scalar operator with a missing value or a list. | Operator <op> requires a single value |
IN/NOT_IN with a non-list or empty value. | Operator <op> requires a non-empty list of values |
BETWEEN without exactly two values. | Operator <op> requires exactly two values |
The timeWindow reference must be in the catalog. | timeWindow reference "<t>.<c>" is not in the catalog |
The timeWindow reference must be a TIMESTAMP column. | timeWindow reference "<t>.<c>" must be a TIMESTAMP column |
The timeWindow reference cannot be a repeated column. | La referencia temporal "<t>.<c>" es una columna repetida y no puede anclar una ventana de tiempo |
The timeWindow value must be a positive integer. | timeWindow value must be a positive integer |
In timeWindow BETWEEN, from must be on or before to. | timeWindow BETWEEN requires "from" to be on or before "to" |
The timeWindow dates must be valid calendar dates. | timeWindow date "<d>" is not a valid calendar date |
Rules specific to composedWith (also 400):
| Rule | Message |
|---|---|
| A criteria cannot reference itself. | An audience criteria cannot reference itself |
| The operand must exist. | Operand audience criteria <id> not found or deleted |
| Circular references between criteria are not allowed. | Circular reference detected |
Example
Audience of users who in the last 90 days completed transactions worth more than
100000, joining event_transactions with user_properties to filter by country, and
intersected with another existing audience that also projects only user_id.
curl -X POST https://api.reten.ai/api/audience-criterias \
-H "Authorization: Bearer <token>" \
-H "x-tenant-id: <tenant-id>" \
-H "x-project-id: <project-id>" \
-H "Content-Type: application/json" \
-d '{
"name": "Compradores VIP (últimos 90 días)",
"queryBlocks": [
{
"order": 0,
"from": {
"table": "event_transactions",
"joins": [
{
"type": "INNER",
"table": "user_properties",
"on": [
{
"left": { "table": "event_transactions", "column": "user_id" },
"right": { "table": "user_properties", "column": "user_id" }
}
]
}
]
},
"select": {
"distinct": false,
"items": [
{ "kind": "column", "column": { "table": "event_transactions", "column": "user_id" } }
]
},
"timeWindow": {
"timeOperator": "IN_THE_LAST",
"reference": { "table": "event_transactions", "column": "event_date" },
"unit": "DAYS",
"value": 90
},
"where": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{
"kind": "predicate",
"column": { "table": "event_transactions", "column": "status" },
"comparisonOperator": "EQUALS",
"value": "completed"
},
{
"kind": "predicate",
"column": { "table": "user_properties", "column": "country" },
"comparisonOperator": "IN",
"value": ["CL", "MX"]
}
]
},
"groupBy": [
{ "table": "event_transactions", "column": "user_id" }
],
"having": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{
"kind": "predicate",
"aggregate": {
"aggregationOperator": "SUM",
"operand": { "kind": "column", "column": { "table": "event_transactions", "column": "pricing.final_amount" } }
},
"comparisonOperator": "GREATER_THAN",
"value": 100000
}
]
}
}
],
"composedWith": [
{
"operandCriteriaId": "b2c3d4e5-6789-4abc-9def-0123456789ab",
"setOperator": "INTERSECT_DISTINCT"
}
]
}'import axios from 'axios';
const body = {
name: 'Compradores VIP (últimos 90 días)',
queryBlocks: [
{
order: 0,
from: {
table: 'event_transactions',
joins: [
{
type: 'INNER',
table: 'user_properties',
on: [
{
left: { table: 'event_transactions', column: 'user_id' },
right: { table: 'user_properties', column: 'user_id' },
},
],
},
],
},
select: {
distinct: false,
items: [
{ kind: 'column', column: { table: 'event_transactions', column: 'user_id' } },
],
},
timeWindow: {
timeOperator: 'IN_THE_LAST',
reference: { table: 'event_transactions', column: 'event_date' },
unit: 'DAYS',
value: 90,
},
where: {
kind: 'group',
logicalOperator: 'AND',
children: [
{
kind: 'predicate',
column: { table: 'event_transactions', column: 'status' },
comparisonOperator: 'EQUALS',
value: 'completed',
},
{
kind: 'predicate',
column: { table: 'user_properties', column: 'country' },
comparisonOperator: 'IN',
value: ['CL', 'MX'],
},
],
},
groupBy: [{ table: 'event_transactions', column: 'user_id' }],
having: {
kind: 'group',
logicalOperator: 'AND',
children: [
{
kind: 'predicate',
aggregate: {
aggregationOperator: 'SUM',
operand: { kind: 'column', column: { table: 'event_transactions', column: 'pricing.final_amount' } },
},
comparisonOperator: 'GREATER_THAN',
value: 100000,
},
],
},
},
],
composedWith: [
{
operandCriteriaId: 'b2c3d4e5-6789-4abc-9def-0123456789ab',
setOperator: 'INTERSECT_DISTINCT',
},
],
};
const response = await axios.post(
'https://api.reten.ai/api/audience-criterias',
body,
{ headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const created = response.data;Response 201 Created
Returns the created criteria as IAudienceCriteriaDetail — the same shape as
GET /audience-criterias/:id. It is the base
resource plus the full tree: queryBlocks[] (with their id and order already resolved,
ordered by order ascending) and compositions[] (with id and operandCriteriaName).
Base fields (IAudienceCriteria) — all camelCase; instants are serialized to ISO 8601:
| Field | Type | Description |
|---|---|---|
id | string (UUID) | Id of the created criteria. |
projectId | string (UUID) | Project the criteria belongs to. |
name | string | |
description | string | null | null if not sent. |
isActive | boolean | Criteria are created active (true). |
isRoot | boolean | true only for the project's root audience (see Projects). |
ownerId | string (UUID) | Creating user. |
updatedBy | string (UUID) | Last editor (on creation, the creator). |
createdAt | string (ISO 8601) | |
updatedAt | string (ISO 8601) |
Nested relations:
queryBlocks[]— each block adds itsid(UUID) and returns the tree exactly as sent (from,select,timeWindow,where,groupBy,having). Absent optional fields are returned asnull(setOperator,timeWindow,groupBy,having) andfrom.joinsas[]if there were no joins;from.unnestsalways comes as[].compositions[]— each composition:id(UUID),operandCriteriaId,operandCriteriaName(resolved operand name;""if it does not resolve) andsetOperator.
{
"id": "a1b2c3d4-5678-4abc-9def-1234567890ab",
"projectId": "3f6c2b1a-8d4e-4f7a-9b2c-5e1d0a7f6c34",
"name": "Compradores VIP (últimos 90 días)",
"description": null,
"isActive": true,
"isRoot": false,
"ownerId": "0f0e0d0c-0b0a-4090-8070-605040302010",
"updatedBy": "0f0e0d0c-0b0a-4090-8070-605040302010",
"createdAt": "2026-07-15T14:32:07.481Z",
"updatedAt": "2026-07-15T14:32:07.481Z",
"queryBlocks": [
{
"id": "c3d4e5f6-7890-4abc-9def-34567890abcd",
"order": 0,
"setOperator": null,
"from": {
"table": "event_transactions",
"joins": [
{
"type": "INNER",
"table": "user_properties",
"on": [
{
"left": { "table": "event_transactions", "column": "user_id" },
"right": { "table": "user_properties", "column": "user_id" }
}
]
}
],
"unnests": []
},
"select": {
"distinct": false,
"items": [
{ "kind": "column", "column": { "table": "event_transactions", "column": "user_id" } }
]
},
"timeWindow": {
"timeOperator": "IN_THE_LAST",
"reference": { "table": "event_transactions", "column": "event_date" },
"unit": "DAYS",
"value": 90
},
"where": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{
"kind": "predicate",
"column": { "table": "event_transactions", "column": "status" },
"comparisonOperator": "EQUALS",
"value": "completed"
},
{
"kind": "predicate",
"column": { "table": "user_properties", "column": "country" },
"comparisonOperator": "IN",
"value": ["CL", "MX"]
}
]
},
"groupBy": [
{ "table": "event_transactions", "column": "user_id" }
],
"having": {
"kind": "group",
"logicalOperator": "AND",
"children": [
{
"kind": "predicate",
"aggregate": {
"aggregationOperator": "SUM",
"operand": { "kind": "column", "column": { "table": "event_transactions", "column": "pricing.final_amount" } }
},
"comparisonOperator": "GREATER_THAN",
"value": 100000
}
]
}
}
],
"compositions": [
{
"id": "d4e5f6a7-8901-4abc-9def-4567890abcde",
"operandCriteriaId": "b2c3d4e5-6789-4abc-9def-0123456789ab",
"operandCriteriaName": "Usuarios activos",
"setOperator": "INTERSECT_DISTINCT"
}
]
}Error Responses
| Status | Description |
|---|---|
400 | Invalid DTO (missing field, incorrect type, enum out of range, unknown property) or a violated business rule (see validation rules). |
401 | Missing or invalid Authorization: Bearer. |
403 | The user lacks the audience-criteria:create permission, or the x-tenant-id is not authorized. |