Reten Docs
Audiências

Atualizar Audience Criteria

Atualize parcialmente um audience criteria por id. Somente os campos enviados são modificados; incluir queryBlocks substitui completamente a definição (blocos e composições).

PATCH /api/audience-criterias/:id

Atualiza um Audience Criteria existente. É uma atualização parcial: somente os campos presentes no corpo são modificados; os omitidos permanecem intactos. A operação é transacional — se a definição for substituída, as alterações são aplicadas juntas ou nenhuma é aplicada.

Auth: Obrigatória — permissão audience-criteria:update

Editar a audiência raiz do projeto exige também a permissão project:update, para qualquer alteração (mesmo que seja só name ou description). Sem ela, a API responde 403 — No tiene permiso para editar la audiencia raíz del proyecto.

Parâmetros de rota

ParâmetroTipoDescrição
idUUIDID do audience criteria. Deve ser um UUID válido (caso contrário, 400).

Semântica de atualização (leia primeiro)

O comportamento do PATCH não é um merge campo a campo da árvore. Resume-se a isto:

  • Só é atualizado o que é enviado. Um campo omitido não é tocado.
    • name — se enviado, não pode ser vazio nem null; atualiza o nome.
    • description — enviar uma string a altera; enviar null a limpa (fica null); omiti-la a deixa inalterada.
  • updatedBy é definido para o usuário que edita; updatedAt é atualizado automaticamente.

queryBlocks é uma substituição total, não um merge. Se o corpo incluir queryBlocks, o serviço apaga todos os query blocks (e todas as composições) existentes do critério e os recria do zero com o que foi enviado. Não é feito diff nem são mantidos os blocos anteriores. Se você omitir queryBlocks, a definição (blocos + composições) permanece intacta e somente name / description são atualizados.

Enviar queryBlocks sem composedWith apaga as composições. A substituição remove blocos e composições juntos, e as composições só são recriadas se composedWith trouxer ao menos um elemento. Para manter as composições ao reenviar queryBlocks, você deve também reenviar composedWith.

Corolário: composedWith sozinho é um no-op. As composições só são tocadas quando queryBlocks também é enviado; enviar composedWith sem queryBlocks não modifica as composições existentes.

A validação de negócio é condicional. A árvore da definição é revalidada (com o fuso horário do tenant) somente se queryBlocks for enviado. Se não for enviado, name / description são atualizados sem revalidar a definição.

Corpo da requisição

Nível raiz (UpdateAudienceCriteriaDto). Todos os campos são opcionais:

CampoTipoObrigatórioDescrição
namestringNãoSe enviado, não pode ser vazio nem null (400).
descriptionstring | nullNãoString para alterá-la, null para limpá-la. Omiti-la a deixa inalterada.
queryBlocksQueryBlockDto[]NãoSe incluído, substitui toda a definição. Mesmo formato que em Criar.
composedWithCreateCompositionDto[]NãoSó tem efeito se queryBlocks também for enviado (ver avisos).

Assim como em Criar, se queryBlocks for enviado, ele deve trazer pelo menos um elemento: um array vazio ([]) produz um 400. Para manter a definição atual, omita o campo.

Quando queryBlocks e/ou composedWith são enviados, eles usam exatamente os mesmos DTOs que Criar (QueryBlockDto / CreateCompositionDto). A subárvore está totalmente documentada abaixo.

queryBlocks[] — bloco de consulta

Cada elemento de queryBlocks (QueryBlockDto) é um statement SQL completo:

CampoTipoObrigatórioDescrição
ordernumber (inteiro)SimPosição do bloco; inteiro ≥ 0.
setOperatorSetOperatorEnumNãoCombina este bloco com o anterior. Ausente no primeiro bloco; obrigatório nos demais (ver regras).
fromFromDtoSimFonte de dados: tabela principal + joins.
selectSelectDtoSimProjeção (colunas e/ou agregações).
timeWindowTimeWindowDtoNãoJanela temporal do bloco (união discriminada por timeOperator).
whereWhereDtoSimÁrvore de predicados. Obrigatório mesmo que seja um grupo vazio.
groupByColumnRefDto[]NãoColunas de agrupamento.
havingHavingDtoNãoFiltro sobre agregados.

O setOperator de um query block combina blocos dentro do mesmo critério; não deve ser confundido com o setOperator de uma composição, que combina critérios inteiros. São dois níveis diferentes de operação de conjunto.

ColumnRefDto — referência a coluna

Usado em toda a árvore (select, filtros, group by, joins, agregados, janela temporal):

CampoTipoObrigatórioDescrição
tablestringSimTabela do catálogo. Não vazio.
columnstringSimColuna. Não vazio. Pode ser um caminho STRUCT (ex.: "pricing.final_amount").

Toda coluna é sempre qualificada com sua table para evitar ambiguidade.

from — fonte de dados (FromDto)

CampoTipoObrigatórioDescrição
tablestringSimTabela principal do catálogo. Não vazio.
joinsJoinDto[]NãoCruzamentos com outras tabelas. Padrão [].
unnestsColumnRefDto[]NãoColunas repetidas declaradas como fonte de linhas (uma linha por elemento). Uma audiência não as aceita: omita o campo ou envie [] (o padrão); uma lista não vazia retorna 400.

Cada JoinDto:

CampoTipoObrigatórioDescrição
typeJoinTypeEnumSimINNER | LEFT | RIGHT | FULL.
tablestringSimTabela a unir (do catálogo). Não vazio.
onJoinPredicateDto[]SimCondições de junção.

Cada JoinPredicateDto é um par de colunas: { left: ColumnRefDto, right: ColumnRefDto }.

select — projeção (SelectDto)

CampoTipoObrigatórioDescrição
distinctbooleanSimAplica SELECT DISTINCT.
items(SelectColumnItem | SelectAggregateItem)[]SimNão vazio. União discriminada pelo campo kind.

Os items são discriminados por kind:

  • Coluna — kind: "column":

    CampoTipoObrigatórioDescrição
    kind"column"SimDiscriminador.
    columnColumnRefDtoSimColuna projetada.
    aliasstringNãoDeve corresponder a ^[A-Za-z_][A-Za-z0-9_]*$.
  • Agregado — kind: "aggregate":

    CampoTipoObrigatórioDescrição
    kind"aggregate"SimDiscriminador.
    aggregateAggregateDtoSimExpressão de agregação.
    aliasstringSimAqui o alias é obrigatório. Mesmo regex de identificador.

AggregateDto:

CampoTipoObrigatórioDescrição
aggregationOperatorAggregationOperatorEnumSimCOUNT | SUM | AVG | MIN | MAX.
operandobjectSimO que é agregado. União discriminada por kind: column, star ou arithmetic (veja abaixo).
distinctbooleanNãoAplica DISTINCT dentro do agregado.

operand —o que é agregado— é discriminado por kind:

kindCampos adicionaisSignificado
columncolumn: ColumnRefDtoUma coluna do catálogo.
star(nenhum)*: conta linhas. Só é válido com COUNT e sem distinct (COUNT(DISTINCT *) não é permitido).
arithmeticarithmeticOperator: ArithmeticOperatorEnum, left, rightExpressão binária. Cada lado é outro operando: uma column ou um arithmetic aninhado (nunca star).

ArithmeticOperatorEnum: ADD | SUBTRACT | MULTIPLY | SAFE_DIVIDE (uma divisão que retorna NULL quando o denominador é zero, em vez de fazer a consulta falhar). As colunas de uma expressão aritmética precisam ser numéricas (INT64, NUMERIC, BIGNUMERIC, FLOAT64), e as expressões se aninham até 3 níveis de arithmetic, contando o externo.

where — árvore de predicados (WhereDto)

O where é uma árvore recursiva. O nó raiz é sempre um grupo, e suas folhas são de dois tipos:

kindO que filtra
groupNão é uma folha: agrupa filhos com AND / OR. Os grupos podem ser aninhados.
predicateUma coluna escalar de uma tabela do from.
recordPredicateUma coluna RECORD REPEATED: pelo menos 1 elemento satisfaz o sub-filtro.
Grupo — kind: "group"
CampoTipoObrigatórioDescrição
kind"group"SimDiscriminador.
logicalOperatorLogicalOperatorEnum (AND | OR)SimConectivo entre os filhos.
children(Group | Leaf)[]SimFilhos do grupo; podem ser grupos aninhados.

logicalOperator é sempre obrigatório em um grupo, mas só tem efeito com 2 ou mais filhos. Com menos de 2 filhos seu valor é irrelevante (gera o mesmo SQL), mas ainda assim deve ser enviado. Um where sem filtros é expresso como um grupo vazio: { "kind": "group", "logicalOperator": "AND", "children": [] }.

Folha de coluna — kind: "predicate"

Compara uma coluna escalar qualificada por sua tabela.

CampoTipoObrigatórioDescrição
kind"predicate"SimDiscriminador.
columnColumnRefDtoSimColuna do predicado.
comparisonOperatorComparisonOperatorEnumSimOperador de comparação.
valuePredicateValueDependeSeu formato depende do operador.
{
  "kind": "predicate",
  "column": { "table": "user_properties", "column": "country" },
  "comparisonOperator": "IN",
  "value": ["CL", "MX"]
}
Folha de registro — kind: "recordPredicate"

Filtra por uma coluna RECORD REPEATED (um array de structs, como attributes). Significa que pelo menos um elemento do array satisfaz o sub-filtro.

CampoTipoObrigatórioDescrição
kind"recordPredicate"SimDiscriminador.
columnColumnRefDtoSimColuna RECORD REPEATED de uma tabela do from.
subWhereSub-árvore relativaSimCondições sobre os sub-campos do elemento. Não pode ficar vazio.
{
  "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" }
    ]
  }
}

As condições do subWhere são avaliadas sobre um mesmo elemento do array, e não sobre o array inteiro. O exemplo acima seleciona linhas que têm um atributo cujo key é canal e cujo value é web — não linhas que têm um atributo com esse key e outro, diferente, com esse value.

Quais colunas aceitam um filtro de registro é informado pelo catálogo: as que vêm com kind: "record".

Sub-árvore relativa

O subWhere é um grupo AND/OR cujas folhas são resolvidas contra os sub-campos que o registro declara.

Há uma única diferença em relação à árvore de nível superior, e ela é a origem mais comum de erros:

Dentro de uma sub-árvore relativa os operandos não levam tabela: você escreve "field": "key", e não "column": { "table": "attributes", "column": "key" }. A folha usa a chave field (uma string), não column.

Folhas permitidas dentro de uma sub-árvore relativa:

  • Folha escalar — { "kind": "predicate", "field": string, "comparisonOperator": …, "value"?: … }. O field deve ser um sub-campo escalar do registro.
  • Registro aninhado — { "kind": "recordPredicate", "field": string, "subWhere": … }. Um registro dentro de outro registro; aninha sem limite declarado, desde que o catálogo declare cada nível.
  • Grupo — o mesmo formato de grupo, para combinar as folhas anteriores com AND/OR.
Formato de value conforme comparisonOperator
OperadoresFormato de valueExemplo
IS_NULL, IS_NOT_NULLSem value (omitido)(omitido)
IN, NOT_INLista não vazia ScalarValue[]["CL", "MX"]
BETWEENPar [min, max][10, 20]
O 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. Além disso, cada operador deve ser compatível com o tipo da coluna (ver regras de validação).

groupBy — colunas de agrupamento

Lista opcional de ColumnRefDto[]. Quando o bloco agrega (declara groupBy ou tem um agregado no select), o groupBy deve corresponder exatamente às colunas não agregadas do select (ver regras de validação).

having — filtro sobre agregados (HavingDto)

Mesma estrutura recursiva que where (grupos AND/OR aninhados e mesma regra de value por operador), mas a folha opera sobre um agregado, não sobre uma coluna:

  • Predicado (folha) — { kind: "predicate", aggregate: AggregateDto, comparisonOperator: ComparisonOperatorEnum, value?: PredicateValue }.
  • Grupo — idêntico ao de where.

timeWindow — janela temporal (TimeWindowDto)

União discriminada por timeOperator. Restringe o bloco a um período de tempo sobre uma coluna TIMESTAMP do catálogo (reference). É um campo irmão do where, não um nó da árvore de filtros.

timeOperatorCampos adicionais
ALL_TIME(nenhum) — sem restrição de tempo.
IN_THE_LASTreference: ColumnRefDto, unit: TimeUnitEnum, value: inteiro > 0
IN_THE_FIRSTreference: ColumnRefDto, unit: TimeUnitEnum, value: inteiro > 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.

Semântica de negócio de cada operador:

  • ALL_TIME — sem janela; considera todo o histórico.
  • IN_THE_LAST — janela rolante relativa a "agora": os últimos value unit (ex.: últimos 30 DAYS). Recalculada a cada execução.
  • IN_THE_FIRST — janela relativa à origem do ciclo de vida do usuário (hoje, sua data de cadastro): os primeiros value unit de vida de cada usuário. Ancorada por usuário, não a uma data fixa.
  • SINCE — a partir da data civil from (inclusive).
  • BETWEEN — entre from e to, ambas as datas civis inclusivas de seu dia completo.

As datas civis YYYY-MM-DD (SINCE, BETWEEN) se resolvem para o início do dia no fuso horário do tenant. O detalhe de fuso horário é interno; basta enviar a data civil.

composedWith[] — composição com outras audiências

Cada elemento (CreateCompositionDto) combina este critério com outra audiência via uma operação de conjunto:

CampoTipoObrigatórioDescrição
operandCriteriaIdstring (UUID)SimId da outra audiência (operando). Deve existir.
setOperatorSetOperatorEnumSimUNION_DISTINCT | UNION_ALL | INTERSECT_DISTINCT.

Lembre-se: composedWith só se aplica quando queryBlocks também é enviado (ver Semântica de atualização).

Regras de validação

A requisição passa por duas camadas de validação, ambas retornando 400 Bad Request:

  1. Validação do DTO (estrutura, tipos, enums, campos obrigatórios da subárvore). Nesse caso message é um array de strings e propriedades desconhecidas são rejeitadas. Sempre é executada.
  2. Validação de negócio (o critério deve gerar SQL válido e coerente com o catálogo). Nesse caso message é uma string única com a mensagem da regra violada e para no primeiro invariante violado. Só é executada se o corpo incluir queryBlocks.

As regras de negócio da definição são idênticas às de Criar (catálogo de tabelas/colunas, star (*) só com COUNT, colunas numéricas nas expressões aritméticas, cobertura de GROUP BY, contexto de HAVING, coluna de identidade, setOperator por posição do bloco, compatibilidade de operador por tipo, formato de value, regras do timeWindow, etc.).

Regras específicas de composedWith (também 400, verificadas ao recriar as composições quando queryBlocks é enviado):

RegraMensagem
Um critério não pode referenciar a si mesmo.An audience criteria cannot reference itself
O operando deve existir.Operand audience criteria <id> not found or deleted
Não são permitidas referências circulares entre critérios.Circular reference detected

Exemplos

Atualização parcial (somente metadados)

Renomeia o critério e limpa sua descrição, sem tocar na definição (queryBlocks / composedWith são omitidos, então blocos e composições permanecem intactos):

curl -X PATCH https://api.reten.ai/api/audience-criterias/a1b2c3d4-5678-4abc-9def-1234567890ab \
  -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 (renombrado)",
    "description": null
  }'
import axios from 'axios';

const id = 'a1b2c3d4-5678-4abc-9def-1234567890ab';
const response = await axios.patch(
  `https://api.reten.ai/api/audience-criterias/${id}`,
  { name: 'Compradores VIP (renombrado)', description: null },
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const updated = response.data;

Substituição da definição

Incluir queryBlocks substitui toda a definição. Aqui composedWith também é reenviado para manter a composição (se fosse omitido, a composição seria apagada):

curl -X PATCH https://api.reten.ai/api/audience-criterias/a1b2c3d4-5678-4abc-9def-1234567890ab \
  -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 60 días)",
    "queryBlocks": [
      {
        "order": 0,
        "from": { "table": "event_transactions", "joins": [] },
        "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": 60
        },
        "where": {
          "kind": "group",
          "logicalOperator": "AND",
          "children": [
            {
              "kind": "predicate",
              "column": { "table": "event_transactions", "column": "status" },
              "comparisonOperator": "EQUALS",
              "value": "completed"
            }
          ]
        },
        "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 id = 'a1b2c3d4-5678-4abc-9def-1234567890ab';
const body = {
  name: 'Compradores VIP (últimos 60 días)',
  queryBlocks: [
    {
      order: 0,
      from: { table: 'event_transactions', joins: [] },
      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: 60,
      },
      where: {
        kind: 'group',
        logicalOperator: 'AND',
        children: [
          {
            kind: 'predicate',
            column: { table: 'event_transactions', column: 'status' },
            comparisonOperator: 'EQUALS',
            value: 'completed',
          },
        ],
      },
      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.patch(
  `https://api.reten.ai/api/audience-criterias/${id}`,
  body,
  { headers: { Authorization: 'Bearer <token>', 'x-tenant-id': '<tenant-id>', 'x-project-id': '<project-id>' } },
);
const updated = response.data;

Resposta 200 OK

Retorna o critério já atualizado como IAudienceCriteriaDetail — a mesma forma que GET /audience-criterias/:id e que Criar. É o recurso base mais a árvore completa: queryBlocks[] (com seu id e order resolvidos, ordenados por order ascendente) e compositions[] (com id e operandCriteriaName).

Campos base (IAudienceCriteria) — todos camelCase; os instantes são serializados em ISO 8601:

CampoTipoDescrição
idstring (UUID)Id do critério.
projectIdstring (UUID)Projeto ao qual o critério pertence.
namestringNome (já atualizado se enviado).
descriptionstring | nullnull se vazio.
isActivebooleanSe o critério está ativo.
isRootbooleantrue apenas para a audiência raiz do projeto (veja Projetos).
ownerIdstring (UUID)Usuário criador (não muda na edição).
updatedBystring (UUID)Usuário que fez esta edição.
createdAtstring (ISO 8601)Data de criação.
updatedAtstring (ISO 8601)Atualizado na edição.

Relações aninhadas:

  • queryBlocks[] — cada bloco adiciona seu id (UUID) e retorna a árvore como ela ficou (from, select, timeWindow, where, groupBy, having). Campos opcionais ausentes são retornados como null (setOperator, timeWindow, groupBy, having) e from.joins como [] se não havia joins; from.unnests vem sempre como [].
  • compositions[] — cada composição: id (UUID), operandCriteriaId, operandCriteriaName (nome do operando resolvido; "" se não resolver) e setOperator.
{
  "id": "a1b2c3d4-5678-4abc-9def-1234567890ab",
  "projectId": "3f6c2b1a-8d4e-4f7a-9b2c-5e1d0a7f6c34",
  "name": "Compradores VIP (últimos 60 días)",
  "description": null,
  "isActive": true,
  "isRoot": false,
  "ownerId": "0f0e0d0c-0b0a-4090-8070-605040302010",
  "updatedBy": "1a2b3c4d-5e6f-4071-8092-a0b0c0d0e0f0",
  "createdAt": "2026-07-15T14:32:07.481Z",
  "updatedAt": "2026-07-15T16:05:44.902Z",
  "queryBlocks": [
    {
      "id": "e5f6a7b8-9012-4abc-9def-567890abcdef",
      "order": 0,
      "setOperator": null,
      "from": { "table": "event_transactions", "joins": [], "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": 60
      },
      "where": {
        "kind": "group",
        "logicalOperator": "AND",
        "children": [
          {
            "kind": "predicate",
            "column": { "table": "event_transactions", "column": "status" },
            "comparisonOperator": "EQUALS",
            "value": "completed"
          }
        ]
      },
      "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": "f6a7b8c9-0123-4abc-9def-67890abcdef0",
      "operandCriteriaId": "b2c3d4e5-6789-4abc-9def-0123456789ab",
      "operandCriteriaName": "Usuarios activos",
      "setOperator": "INTERSECT_DISTINCT"
    }
  ]
}

Respostas de erro

StatusDescrição
400DTO inválido (tipo incorreto, enum fora do intervalo, propriedade desconhecida, name vazio ou null, queryBlocks vazio) ou, quando queryBlocks é enviado, uma regra de negócio violada (ver regras de validação). Também se o id não for um UUID válido.
401Authorization: Bearer ausente ou inválido.
403O usuário não tem a permissão audience-criteria:update, ou o x-tenant-id não está autorizado. Também quando o critério é a audiência raiz do projeto e o usuário não tem project:update (No tiene permiso para editar la audiencia raíz del proyecto).
404Não existe um audience criteria com esse id no projeto ativo.