Crear Audience Criteria
Crea un criterio de audiencia a partir de su definición completa (bloques de consulta, filtros, ventana temporal, agregaciones y composiciones).
POST /api/audience-criterias
Crea un Audience Criteria a partir de su definición completa. Es el endpoint central del recurso: recibe todo el árbol de la definición —bloques de consulta, proyección, filtros, ventana temporal, cruces, agregaciones y composiciones con otras audiencias— y lo persiste. La operación es transaccional: el criterio, sus query blocks y sus composiciones se crean juntos, o no se crea nada.
Auth: Requerida — permiso audience-criteria:create
Cuerpo de la solicitud
Nivel raíz (CreateAudienceCriteriaDto):
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
name | string | Sí | Nombre del criterio. No puede ser vacío. |
description | string | No | Descripción de texto libre. Si se omite, la respuesta la devuelve como null. |
type | AudienceCriteriaTypeEnum | Sí | AUDIENCE | FILTER. |
queryBlocks | CreateQueryBlockDto[] | Sí | Mínimo 1 elemento. Cada bloque ≈ un statement SQL completo. |
composedWith | CreateCompositionDto[] | No | Compone este criterio con otras audiencias vía operaciones de conjunto. |
queryBlocks[] — bloque de consulta
Cada elemento de queryBlocks (CreateQueryBlockDto) es un statement SQL completo:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
order | number (entero) | Sí | Posición del bloque; entero ≥ 0. |
setOperator | SetOperatorEnum | No | Combina este bloque con el anterior. Ausente en el primer bloque; obligatorio en el resto (ver reglas). |
from | FromDto | Sí | Fuente de datos: tabla principal + joins. |
select | SelectDto | Sí | Proyección (columnas y/o agregaciones). |
timeWindow | TimeWindowDto | No | Ventana temporal del bloque (unión discriminada por timeOperator). |
where | WhereDto | Sí | Árbol de predicados. Obligatorio aunque sea un grupo vacío. |
groupBy | ColumnRefDto[] | No | Columnas de agrupación. |
having | HavingDto | No | Filtro sobre agregados. |
El setOperator de un query block combina bloques dentro del mismo criterio;
no debe confundirse con el setOperator de una composición,
que combina criterios enteros. Son dos niveles distintos de operación de conjunto.
ColumnRefDto — referencia a columna
Se usa en todo el árbol (select, filtros, group by, joins, agregados, ventana temporal):
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
table | string | Sí | Tabla del catálogo. No vacío. |
column | string | Sí | Columna. No vacío. Puede ser un path de STRUCT (p. ej. "pricing.final_amount"). |
Toda columna se cualifica siempre con su table para evitar ambigüedad.
from — fuente de datos (FromDto)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
table | string | Sí | Tabla principal del catálogo. No vacío. |
joins | JoinDto[] | No | Cruces con otras tablas. Default []. |
Cada JoinDto:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
type | JoinTypeEnum | Sí | INNER | LEFT | RIGHT | FULL. |
table | string | Sí | Tabla a unir (del catálogo). No vacío. |
on | JoinPredicateDto[] | Sí | Condiciones de unión. |
Cada JoinPredicateDto es un par de columnas: { left: ColumnRefDto, right: ColumnRefDto }.
select — proyección (SelectDto)
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
distinct | boolean | Sí | Aplica SELECT DISTINCT. |
items | (SelectColumnItem | SelectAggregateItem)[] | Sí | No vacío. Unión discriminada por el campo kind. |
Los items se discriminan por kind:
-
Columna —
kind: "column":Campo Tipo Requerido Descripción kind"column"Sí Discriminador. columnColumnRefDtoSí Columna proyectada. aliasstring No Debe matchear ^[A-Za-z_][A-Za-z0-9_]*$. -
Agregado —
kind: "aggregate":Campo Tipo Requerido Descripción kind"aggregate"Sí Discriminador. aggregateAggregateDtoSí Expresión de agregación. aliasstring Sí Aquí el alias es obligatorio. Mismo regex de identificador.
AggregateDto:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
aggregationOperator | AggregationOperatorEnum | Sí | COUNT | SUM | AVG | MIN | MAX. |
column | ColumnRefDto | "*" | Sí | Columna a agregar, o el literal "*". El "*" solo tiene sentido para COUNT(*); con column distinto de "*" se valida como ColumnRefDto. |
distinct | boolean | No | Aplica DISTINCT dentro del agregado. |
where — árbol de predicados (WhereDto)
El where es un árbol recursivo. El nodo raíz es siempre un grupo:
- Grupo —
{ kind: "group", logicalOperator: LogicalOperatorEnum, children: (Predicado | Grupo)[] }. Se permiten grupos anidados (AND/OR a varios niveles). - Predicado (hoja) —
{ kind: "predicate", column: ColumnRefDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }.
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
kind | "group" | "predicate" | Sí | Discriminador de nodo. |
logicalOperator | LogicalOperatorEnum (AND | OR) | Grupo | En un grupo: siempre obligatorio. |
children | (Grupo | Predicado)[] | Grupo | Hijos del grupo. |
column | ColumnRefDto | Hoja | Columna del predicado. |
comparisonOperator | ComparisonOperatorEnum | Hoja | Operador de comparación. |
value | PredicateValue | Hoja | Depende del operador (ver abajo). |
logicalOperator es obligatorio siempre en un grupo, pero solo tiene efecto con
2 o más hijos. Con menos de 2 hijos su valor es indiferente (genera el mismo SQL), pero
igual debe enviarse. Un where sin filtros se expresa como un grupo vacío:
{ "kind": "group", "logicalOperator": "AND", "children": [] }.
Forma de value según comparisonOperator
| Operadores | Forma de value | Ejemplo |
|---|---|---|
IS_NULL, IS_NOT_NULL | Sin value (se omite) | (se omite) |
IN, NOT_IN | Lista no vacía ScalarValue[] | ["CL", "MX"] |
BETWEEN | Par [min, max] | [10, 20] |
El resto (EQUALS, NOT_EQUALS, GREATER_THAN, LESS_THAN, GREATER_THAN_OR_EQUAL, LESS_THAN_OR_EQUAL, CONTAINS, NOT_CONTAINS, STARTS_WITH, ENDS_WITH) | Escalar único | "activo" · 42 |
ScalarValue = string | number | boolean. Además, cada operador debe ser
compatible con el tipo de la columna (ver reglas de validación).
groupBy — columnas de agrupación
Lista opcional de ColumnRefDto[]. Cuando el bloque agrega (declara groupBy o tiene un
agregado en el select), el groupBy debe coincidir exactamente con las columnas
no agregadas del select (ver reglas de validación).
having — filtro sobre agregados (HavingDto)
Misma estructura recursiva que where (grupos AND/OR anidados y misma regla de value
por operador), pero la hoja opera sobre un agregado, no sobre una columna:
- Predicado (hoja) —
{ kind: "predicate", aggregate: AggregateDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }. - Grupo — idéntico al de
where.
timeWindow — ventana temporal (TimeWindowDto)
Unión discriminada por timeOperator. Acota el bloque a un período de tiempo sobre una
columna TIMESTAMP del catálogo (reference). Es un campo hermano del where, no un
nodo del árbol de filtros.
timeOperator | Campos adicionales |
|---|---|
ALL_TIME | (ninguno) — sin acotar por tiempo. |
IN_THE_LAST | reference: ColumnRefDto, unit: TimeUnitEnum, value: entero > 0 |
IN_THE_FIRST | reference: ColumnRefDto, unit: TimeUnitEnum, value: entero > 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.
Semántica de negocio de cada operador:
ALL_TIME— sin ventana; considera todo el histórico.IN_THE_LAST— ventana rodante relativa a "ahora": los últimosvalueunit(p. ej. últimos30 DAYS). Se recomputa en cada corrida.IN_THE_FIRST— ventana relativa al origen del ciclo de vida del usuario (hoy, su fecha de alta): los primerosvalueunitde vida de cada usuario. Se ancla por usuario, no a una fecha fija.SINCE— desde la fecha civilfrom(inclusiva) en adelante.BETWEEN— entrefromyto, ambas fechas civiles inclusivas de su día completo.
Las fechas civiles YYYY-MM-DD (SINCE, BETWEEN) se resuelven al inicio del día en
la zona horaria del tenant. El detalle de zona horaria es interno; basta con enviar la
fecha civil.
composedWith[] — composición con otras audiencias
Cada elemento (CreateCompositionDto) combina este criterio con otra audiencia
vía una operación de conjunto:
| Campo | Tipo | Requerido | Descripción |
|---|---|---|---|
operandCriteriaId | string (UUID) | Sí | Id de la otra audiencia (operando). Debe existir. |
setOperator | SetOperatorEnum | Sí | UNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT. |
Reglas de validación
La solicitud pasa por dos capas de validación, ambas devuelven 400 Bad Request:
- Validación del DTO (estructura, tipos, enums, campos requeridos). En este caso
messagees un arreglo de strings y se rechazan propiedades desconocidas. - Validación de negocio (el criterio debe generar SQL válido y coherente contra el
catálogo). En este caso
messagees un string único con el mensaje de la regla incumplida. La validación de negocio se detiene en el primer invariante violado.
Reglas de negocio verificadas (todas devuelven 400, con el mensaje exacto del API):
| Regla | Mensaje |
|---|---|
| La tabla debe pertenecer al catálogo. | Table "<t>" is not in the audience catalog |
Cada (table, column) debe pertenecer al catálogo. | Column "<t>.<c>" is not in the audience catalog |
"*" solo es válido con COUNT. | '*' is only valid with COUNT, not <op> |
COUNT(DISTINCT *) no está permitido. | COUNT(DISTINCT *) is not allowed |
Toda columna referenciada debe pertenecer a una tabla del from (principal ∪ joins). | Column "<t>.<c>" references a table that is not in the query block FROM |
Al agregar, toda columna no agregada del select debe estar en el groupBy. | Column "<t>.<c>" must appear in GROUP BY when the operation aggregates |
Toda columna del groupBy debe ser una columna no agregada del select. | Column "<t>.<c>" in GROUP BY must be a non-aggregated column of the SELECT |
having exige contexto de agregación (groupBy o un agregado en el select). | HAVING requires an aggregation context (GROUP BY or an aggregate in the SELECT) |
Un AUDIENCE debe proyectar su columna de identidad (p. ej. user_id) como columna. | AUDIENCE type requires at least one operation that selects its identity column (e.g. user_id) |
El primer bloque (order=0) no lleva setOperator. | First operation (order=0) must not have a setOperator |
Todo bloque posterior (order>0) debe llevar setOperator. | Operation with order=<n> must have a setOperator |
| El operador debe ser compatible con el tipo de la columna/agregado. | Operator <op> is not allowed for <type> operands |
Operador escalar con value faltante o lista. | Operator <op> requires a single value |
IN/NOT_IN con value no-lista o vacía. | Operator <op> requires a non-empty list of values |
BETWEEN sin exactamente dos valores. | Operator <op> requires exactly two values |
La reference del timeWindow debe estar en el catálogo. | timeWindow reference "<t>.<c>" is not in the audience catalog |
La reference del timeWindow debe ser una columna TIMESTAMP. | timeWindow reference "<t>.<c>" must be a TIMESTAMP column |
value del timeWindow debe ser un entero positivo. | timeWindow value must be a positive integer |
En timeWindow BETWEEN, from debe ser anterior o igual a to. | timeWindow BETWEEN requires "from" to be on or before "to" |
Las fechas del timeWindow deben ser fechas de calendario válidas. | timeWindow date "<d>" is not a valid calendar date |
Reglas específicas de composedWith (también 400):
| Regla | Mensaje |
|---|---|
| Un criterio no puede referenciarse a sí mismo. | An audience criteria cannot reference itself |
| El operando debe existir. | Operand audience criteria <id> not found or deleted |
| No se permiten referencias circulares entre criterios. | Circular reference detected |
Ejemplo
Audiencia de usuarios que en los últimos 90 días completaron transacciones por más de
100000, cruzando event_transactions con user_properties para filtrar por país, e
intersectada con otra audiencia existente.
curl -X POST https://api.reten.ai/api/audience-criterias \
-H "Authorization: Bearer <token>" \
-H "x-tenant-id: <tenant-id>" \
-H "Content-Type: application/json" \
-d '{
"name": "Compradores VIP (últimos 90 días)",
"type": "AUDIENCE",
"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" } },
{
"kind": "aggregate",
"aggregate": {
"aggregationOperator": "SUM",
"column": { "table": "event_transactions", "column": "pricing.final_amount" }
},
"alias": "total_gastado"
}
]
},
"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",
"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)',
type: 'AUDIENCE',
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' } },
{
kind: 'aggregate',
aggregate: {
aggregationOperator: 'SUM',
column: { table: 'event_transactions', column: 'pricing.final_amount' },
},
alias: 'total_gastado',
},
],
},
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',
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>' } },
);
const created = response.data;Respuesta 201 Created
Devuelve el criterio creado como IAudienceCriteriaDetail — la misma forma que
GET /audience-criterias/:id. Es el recurso
base más el árbol completo: queryBlocks[] (con su id y order ya resueltos, ordenados
por order ascendente) y compositions[] (con id y operandCriteriaName).
Campos base (IAudienceCriteria) — todos camelCase; los instantes se serializan a ISO 8601:
| Campo | Tipo | Descripción |
|---|---|---|
id | string (UUID) | Id del criterio creado. |
name | string | |
description | string | null | null si no se envió. |
type | AudienceCriteriaTypeEnum | AUDIENCE | FILTER. |
isActive | boolean | Los criterios se crean activos (true). |
ownerId | string (UUID) | Usuario creador. |
updatedBy | string (UUID) | Último editor (en la creación, el creador). |
createdAt | string (ISO 8601) | |
updatedAt | string (ISO 8601) |
Relaciones anidadas:
queryBlocks[]— cada bloque añade suid(UUID) y devuelve el árbol tal como se envió (from,select,timeWindow,where,groupBy,having). Los campos opcionales ausentes se devuelven comonull(setOperator,timeWindow,groupBy,having) yfrom.joinscomo[]si no había joins.compositions[]— cada composición:id(UUID),operandCriteriaId,operandCriteriaName(nombre del operando resuelto;""si no resuelve) ysetOperator.
{
"id": "a1b2c3d4-5678-4abc-9def-1234567890ab",
"name": "Compradores VIP (últimos 90 días)",
"description": null,
"type": "AUDIENCE",
"isActive": true,
"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" }
}
]
}
]
},
"select": {
"distinct": false,
"items": [
{ "kind": "column", "column": { "table": "event_transactions", "column": "user_id" } },
{
"kind": "aggregate",
"aggregate": {
"aggregationOperator": "SUM",
"column": { "table": "event_transactions", "column": "pricing.final_amount" }
},
"alias": "total_gastado"
}
]
},
"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",
"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"
}
]
}Respuestas de error
| Estado | Descripción |
|---|---|
400 | DTO inválido (campo faltante, tipo incorrecto, enum fuera de rango, propiedad desconocida) o una regla de negocio incumplida (ver reglas de validación). |
401 | Falta o es inválido el Authorization: Bearer. |
403 | El usuario no tiene el permiso audience-criteria:create, o el x-tenant-id no está autorizado. |
Obtener Audiencia
Obtiene un audience criteria por id, con su definición completa — bloques de consulta y composiciones anidadas.
Actualizar Audience Criteria
Actualiza parcialmente un audience criteria por id. Solo se modifican los campos enviados; incluir queryBlocks reemplaza por completo la definición (bloques y composiciones).