Reten Docs

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):

FieldTypeRequiredDescription
namestringYesCriteria name. Cannot be empty.
descriptionstringNoFree-text description. If omitted, the response returns it as null.
queryBlocksQueryBlockDto[]YesMinimum 1 element. Each block ≈ a complete SQL statement.
composedWithCreateCompositionDto[]NoComposes this criteria with other audiences via set operations.

queryBlocks[] — query block

Each element of queryBlocks (QueryBlockDto) is a complete SQL statement:

FieldTypeRequiredDescription
ordernumber (integer)YesBlock position; integer ≥ 0.
setOperatorSetOperatorEnumNoCombines this block with the previous one. Absent in the first block; required in the rest (see rules).
fromFromDtoYesData source: main table + joins.
selectSelectDtoYesProjection (columns and/or aggregations).
timeWindowTimeWindowDtoNoBlock time window (discriminated union by timeOperator).
whereWhereDtoYesPredicate tree. Required even if it is an empty group.
groupByColumnRefDto[]NoGrouping columns.
havingHavingDtoNoFilter 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):

FieldTypeRequiredDescription
tablestringYesCatalog table. Non-empty.
columnstringYesColumn. 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)

FieldTypeRequiredDescription
tablestringYesMain catalog table. Non-empty.
joinsJoinDto[]NoJoins with other tables. Default [].
unnestsColumnRefDto[]NoRepeated 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:

FieldTypeRequiredDescription
typeJoinTypeEnumYesINNER | LEFT | RIGHT | FULL.
tablestringYesTable to join (from the catalog). Non-empty.
onJoinPredicateDto[]YesJoin conditions.

Each JoinPredicateDto is a pair of columns: { left: ColumnRefDto, right: ColumnRefDto }.

select — projection (SelectDto)

FieldTypeRequiredDescription
distinctbooleanYesApplies SELECT DISTINCT.
items(SelectColumnItem | SelectAggregateItem)[]YesNon-empty. Discriminated union by the kind field.

The items are discriminated by kind:

  • Column — kind: "column":

    FieldTypeRequiredDescription
    kind"column"YesDiscriminator.
    columnColumnRefDtoYesProjected column.
    aliasstringNoMust match ^[A-Za-z_][A-Za-z0-9_]*$.
  • Aggregate — kind: "aggregate":

    FieldTypeRequiredDescription
    kind"aggregate"YesDiscriminator.
    aggregateAggregateDtoYesAggregation expression.
    aliasstringYesHere the alias is required. Same identifier regex.

AggregateDto:

FieldTypeRequiredDescription
aggregationOperatorAggregationOperatorEnumYesCOUNT | SUM | AVG | MIN | MAX.
operandobjectYesWhat is aggregated. Discriminated union on kind: column, star or arithmetic (see below).
distinctbooleanNoApplies DISTINCT within the aggregate.

operand — what is aggregated — is discriminated by kind:

kindAdditional fieldsMeaning
columncolumn: ColumnRefDtoA catalog column.
star(none)*: counts rows. Only valid with COUNT and without distinct (COUNT(DISTINCT *) is not allowed).
arithmeticarithmeticOperator: ArithmeticOperatorEnum, left, rightBinary 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:

kindWhat it filters
groupNot a leaf: it groups children with AND / OR. Groups can be nested.
predicateA scalar column of a table in the from.
recordPredicateA RECORD REPEATED column: at least 1 element satisfies the sub-filter.
Group — kind: "group"
FieldTypeRequiredDescription
kind"group"YesDiscriminator.
logicalOperatorLogicalOperatorEnum (AND | OR)YesConnective between the children.
children(Group | Leaf)[]YesGroup 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.

FieldTypeRequiredDescription
kind"predicate"YesDiscriminator.
columnColumnRefDtoYesPredicate column.
comparisonOperatorComparisonOperatorEnumYesComparison operator.
valuePredicateValueDependsIts 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.

FieldTypeRequiredDescription
kind"recordPredicate"YesDiscriminator.
columnColumnRefDtoYesRECORD REPEATED column of a table in the from.
subWhereRelative sub-treeYesConditions 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"?: … }. The field must 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
OperatorsShape of valueExample
IS_NULL, IS_NOT_NULLNo value (omitted)(omitted)
IN, NOT_INNon-empty list ScalarValue[]["CL", "MX"]
BETWEENPair [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.

timeOperatorAdditional fields
ALL_TIME(none) — no time constraint.
IN_THE_LASTreference: ColumnRefDto, unit: TimeUnitEnum, value: integer > 0
IN_THE_FIRSTreference: ColumnRefDto, unit: TimeUnitEnum, value: integer > 0
SINCEreference: ColumnRefDto, from: string (YYYY-MM-DD)
BETWEENreference: 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 last value unit (e.g. last 30 DAYS). Recomputed on each run.
  • IN_THE_FIRST — window relative to the origin of the user's lifecycle (currently, their signup date): the first value unit of each user's life. It is anchored per user, not to a fixed date.
  • SINCE — from the civil date from (inclusive) onward.
  • BETWEEN — between from and to, 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:

FieldTypeRequiredDescription
operandCriteriaIdstring (UUID)YesId of the other audience (operand). Must exist.
setOperatorSetOperatorEnumYesUNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT.

Validation Rules

The request goes through two layers of validation, both returning 400 Bad Request:

  1. DTO validation (structure, types, enums, required fields). In this case message is an array of strings and unknown properties are rejected.
  2. Business validation (the criteria must generate valid SQL that is coherent against the catalog). In this case message is 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):

RuleMessage
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):

RuleMessage
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:

FieldTypeDescription
idstring (UUID)Id of the created criteria.
projectIdstring (UUID)Project the criteria belongs to.
namestring
descriptionstring | nullnull if not sent.
isActivebooleanCriteria are created active (true).
isRootbooleantrue only for the project's root audience (see Projects).
ownerIdstring (UUID)Creating user.
updatedBystring (UUID)Last editor (on creation, the creator).
createdAtstring (ISO 8601)
updatedAtstring (ISO 8601)

Nested relations:

  • queryBlocks[] — each block adds its id (UUID) and returns the tree exactly as sent (from, select, timeWindow, where, groupBy, having). Absent optional fields are returned as null (setOperator, timeWindow, groupBy, having) and from.joins as [] if there were no joins; from.unnests always comes as [].
  • compositions[] — each composition: id (UUID), operandCriteriaId, operandCriteriaName (resolved operand name; "" if it does not resolve) and setOperator.
{
  "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

StatusDescription
400Invalid DTO (missing field, incorrect type, enum out of range, unknown property) or a violated business rule (see validation rules).
401Missing or invalid Authorization: Bearer.
403The user lacks the audience-criteria:create permission, or the x-tenant-id is not authorized.