Skip to content

Catálogo de variáveis (Router e Flow)

Variáveis estáticas vinculadas ao Router ou ao Flow. Diferentemente das variáveis de sessão (session.variables), que são efêmeras e criadas em runtime pelo flow, as variáveis de catálogo são configuradas antecipadamente, sobrevivem entre sessões e ficam disponíveis para interpolação sem ocupar espaço na sessão de nenhum usuário.

O caso de uso típico é armazenar credenciais de API, URLs de ambientes ou flags de configuração que todos os flows de um Router (ou um Flow específico) precisam consultar, especialmente como secrets que não devem aparecer em texto plano no código do flow.


Dois escopos distintos

EscopoModeloAcesso na interpolaçãoVisibilidade
RouterRouterVariable{{router.key}}Todos os flows do Router
FlowFlowVariable{{flow.key}}Apenas o Flow dono

A distinção é proposital: credenciais compartilhadas entre vários flows (token de integração, URL de webhook) vão no Router; configurações específicas de um flow (prompt de sistema de um LLM, endpoint de uma API específica) vão no Flow.


Modelo de dados

prisma
model RouterVariable {
  id             String   @id @default(cuid())
  routerId       String
  router         Router   @relation(fields: [routerId], references: [id], onDelete: Cascade)
  key            String
  value          String?           // plaintext — só preenchido se isSecret=false
  valueEncrypted String?           // AES-256-CBC — só preenchido se isSecret=true
  isSecret       Boolean  @default(false)
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt

  @@unique([routerId, key])
  @@map("router_variables")
}

model FlowVariable {
  id             String   @id @default(cuid())
  flowId         String
  flow           Flow     @relation(fields: [flowId], references: [id], onDelete: Cascade)
  key            String
  value          String?
  valueEncrypted String?
  isSecret       Boolean  @default(false)
  createdAt      DateTime @default(now())
  updatedAt      DateTime @updatedAt

  @@unique([flowId, key])
  @@map("flow_variables")
}

Notas do schema:

  • @@unique([routerId, key]) — a mesma chave não pode existir duas vezes no mesmo Router (ou Flow). Renomear é um DELETE + POST.
  • onDelete: Cascade — ao deletar um Router ou Flow, todas as suas variáveis são deletadas automaticamente pelo banco.
  • Nunca ambos value e valueEncrypted são preenchidos ao mesmo tempo — a coluna usada depende de isSecret.

Restrição de nomenclatura de chaves: o regex ^[a-zA-Z_][a-zA-Z0-9_]*$ é validado no backend. Chaves só podem conter letras, dígitos e underscore, e não podem começar com dígito. Isso garante que {{router.MY_KEY}} seja sempre um token válido na interpolação.


Fronteira de criptografia

O flow-ai-core é a única peça do sistema que conhece a chave de criptografia (CRYPTO_SECRET, 32 bytes). O fluxo é exatamente o mesmo dos outros secrets da plataforma:

Postgres (criptografado)  ←→  flow-ai-core (decripta)  →  Redis (plaintext)

                                                         flow-ai-engine lê aqui

A criptografia usa AES-256-CBC com IV aleatório de 16 bytes. O resultado armazenado no banco tem o formato iv_hex:ciphertext_hex:

typescript
// Crypto.encrypt() — gera IV aleatório a cada chamada
const iv = randomBytes(16)
const ivHex = iv.toString("hex")
const cipher = createCipheriv("aes-256-cbc", Buffer.from(CRYPTO_SECRET), iv)
let encrypted = cipher.update(data, "utf8", "hex")
encrypted += cipher.final("hex")
return `${ivHex}:${encrypted}`  // ex: "a1b2c3d4...:e5f6a7b8..."

// Crypto.decrypt() — extrai IV do prefixo
const [ivHex, encrypted] = encryptedData.split(":")
const decipher = createDecipheriv("aes-256-cbc", Buffer.from(CRYPTO_SECRET), Buffer.from(ivHex, "hex"))

O Redis recebe o valor já decifrado. A API nunca devolve o valor de um secret — retorna null no campo value. Isso evita que credenciais apareçam em logs de requisição ou ferramentas de monitoramento de HTTP.


Caches Redis

Dois caches armazenam todas as variáveis de um escopo já decifradas, como um Record<string, string> serializado em JSON:

ChaveConteúdo
vars:router:{routerId}{"MY_API_KEY": "sk-...", "WEBHOOK_URL": "https://..."}
vars:flow:{flowId}{"SYSTEM_PROMPT": "Você é um assistente...", "MAX_TOKENS": "500"}

Os caches não têm TTL — são atualizados sincronamente pelo flow-ai-core em qualquer operação de escrita (create, update, delete) de variável, sempre via syncRouterVarsCache / syncFlowVarsCache (que regeram o Record inteiro a partir do Postgres). Ao deletar uma variável, o cache é regerado sem ela.

Nota: ao deletar o Router ou Flow inteiro, o cache não é limpo — os helpers deleteRouterVars / deleteFlowVars existem mas não são chamados no caminho de delete da entidade (ver "Casos de borda"). A chave vars:* fica órfã no Redis.

POST /routers/:id/variables  (cria variável)
└─ Valida formato da key
├─ Se isSecret: Crypto.encrypt(value) → valueEncrypted
├─ Se não: salva em value direto
├─ Persiste RouterVariable no Postgres
└─ syncRouterVarsCache(routerId)
   ├─ Busca todas RouterVariable do router
   ├─ Para cada: decripta se isSecret, usa value se não
   └─ setRouterVars(routerId, JSON.stringify(resolved))
      → grava vars:router:{routerId}

O mesmo padrão vale para FlowVariable.


Carregamento no engine

O flow-ai-engine carrega as variáveis antes de construir o ExecutionContext, imediatamente após carregar a sessão e o flow ativo:

typescript
// services/flow-ai-engine/src/handlers/flow.ts
const rawRouterVars = await getRouterVars(session.routerId)
const routerVars: Record<string, string> = rawRouterVars ? JSON.parse(rawRouterVars) : {}

const rawFlowVars = session.flowId ? await getFlowVars(session.flowId) : null
const flowVars: Record<string, string> = rawFlowVars ? JSON.parse(rawFlowVars) : {}

const ctx: ExecutionContext = {
  session, event, activeFlow, flowsCache, currentInput: null,
  redirectHopCount: 0,
  routerVars,   // ← disponível em toda execução via ctx.routerVars
  flowVars,     // ← disponível em toda execução via ctx.flowVars
}

O que acontece se o cache não existir? O parser retorna um objeto vazio {}. A execução não falha — tokens como {{router.MINHA_VAR}} simplesmente ficam sem valor (comportamento de token não resolvido na interpolação: emite console.info e substitui o token por string vazia no output, não pelo token literal). Isso ocorre quando o Router não tem variáveis cadastradas ou quando o cache ainda não foi populado (ex: variável criada mas flow-ai-core ainda não terminou de sincronizar).

As variáveis nunca são gravadas na sessão. São somente-leitura durante a execução e isoladas do session.variables do assinante — um flow não pode sobrescrever uma variável de Router ou de Flow.


Interpolação

O interpolate.ts — que vive no pacote compartilhado flow-ai-runtime (usado tanto pelo engine quanto pelo agent) — resolve tokens em qualquer string de configuração do flow antes de executar ações:

typescript
// packages/flow-ai-runtime/src/interpolate.ts

if (expr.startsWith("router.")) {
  const key = expr.slice(7)        // remove "router."
  const value = ctx.routerVars[key]
  return value !== undefined ? value : undefined  // undefined se não existir
}
if (expr.startsWith("flow.")) {
  const key = expr.slice(5)        // remove "flow."
  const value = ctx.flowVars[key]
  return value !== undefined ? value : undefined  // undefined se não existir
}

Tokens disponíveis na interpolação — visão completa:

TokenFonteEscopo
{{router.key}}ctx.routerVars[key]Todos os flows do Router
{{flow.key}}ctx.flowVars[key]Flow atual
{{contact.name}}session.whatsApp.contact.name (perfil do canal — alias legado; reflete a action setContactName quando o nome foi fixado)Sessão atual
{{contact.phoneNumber}}session.whatsApp.contact.phoneNumber (perfil do canal — alias legado)Sessão atual
{{contact.whatsapp.name}}session.whatsApp.contact.name (perfil do canal — forma explícita)Sessão atual
{{contact.whatsapp.phoneNumber}}session.whatsApp.contact.phoneNumber (perfil do canal — forma explícita)Sessão atual
{{contact.<chave>}}ctx.contactVars[chave] — variável de contato (Contact.contactVariables); aceita path aninhado ({{contact.customer.nome}})Contato (permanente)
{{contact.variables.<chave>}}idem acima, forma explícita (paridade com o desk)Contato (permanente)
{{input}}ctx.currentInput (JSON stringify do InputObject completo)Input do usuário neste turno
{{input.type}}ctx.currentInput.type"text" | "image" | "audio" | "video" | "document" | "sticker" | "interactive" | "location" | "reaction" | "contacts" | "button"Input do usuário neste turno
{{input.content}}ctx.currentInput.content — texto body ou ID do botão/listaInput do usuário neste turno
{{inputMessage}}Texto para type=text, ID para type=interactive, "" para mídiaInput do usuário neste turno
{{lastInput}}session.lastInput — conteúdo do último input recebido do contato; persiste entre blocos até ser substituído pelo próximo inputSessão atual
{{input.media.id}}ctx.currentInput.media.idInput de mídia
{{input.media.mimeType}}ctx.currentInput.media.mimeTypeInput de mídia
{{input.interactive.type}}ctx.currentInput.interactive.typeInput interativo
{{input.interactive.id}}ctx.currentInput.interactive.idInput interativo
{{input.interactive.title}}ctx.currentInput.interactive.titleInput interativo
{{system.now}}new Date().toISOString()Momento da execução
{{session.flowId}}session.flowIdSessão atual
{{session.routerId}}session.routerIdSessão atual
{{session.blockId}}session.currentBlockIdSessão atual
{{session.chatId}}ctx.event.chatIdSessão atual
{{session.mode}}session.mode ("flow" | "human" | "agent")Sessão atual
{{session.source}}session.source ("whatsapp" | "webchat" | "instagram")Sessão atual
{{session.contactIdentity}}Identificador composto do contato (= sessionId)Sessão atual
{{session.currentFlow.id}}session.currentFlow.idSessão atual
{{session.currentFlow.name}}session.currentFlow.nameSessão atual
{{session.currentBlock.id}}session.currentBlock.idSessão atual
{{session.currentBlock.name}}session.currentBlock.nameSessão atual
{{session.previousBlock.id}}session.previousBlock.id (ausente na primeira parada)Sessão atual
{{session.previousBlock.name}}session.previousBlock.name (ausente na primeira parada)Sessão atual
{{session.sessionHistory}}JSON do histórico de paradas em InputBlocksSessão atual
{{session.*}}Acesso genérico a qualquer campo do SessionState via JSON path (ex: session.variables.myVar, session.whatsApp.contact.name)Sessão atual
{{varName}}session.variables[varName]Sessão atual
{{varName.campo.sub}}Navega JSON path dentro de session.variables[varName]Sessão atual
{{ticket}} / {{ticket.campo}}Variável de contexto gravada pelo engine ao sair do atendimento humano: o ticket encerrado em JSON (closeKind, closedBy, queue, tags, datas, durações). Ver Atendimento humanoSessão atual
{{handoffDeflect.reason}} / {{handoffDeflect.detail}}Por que o atendimento humano não começou (fila fora do horário, sem atendente, fila inviável) — ver Deflexão na entradaSessão atual
{{ticket.exit.key}} / {{ticket.exit.survey}}Saída do bloco de atendimento que casou com o motivo do encerramento, e a marcação de pesquisa dela. Presente só quando o bloco configurou saída para aquele motivo — ver Saídas por motivoSessão atual

Prioridade de resolução: tokens com prefixo explícito (router., flow., contact., input, system., session.) são resolvidos primeiro. Tokens sem prefixo caem no dicionário session.variables. O padrão {{session.*}} resolve qualquer campo do SessionState via JSON path — útil para acessar campos que não têm atalho direto.


API REST

Router Variables

GET    /routers/:id/variables         → lista todas (value=null para secrets)
POST   /routers/:id/variables         → cria variável (body: key, value, isSecret)
PUT    /routers/:id/variables/:varId  → atualiza value e/ou isSecret (a key não muda)
DELETE /routers/:id/variables/:varId  → remove variável

Flow Variables

GET    /flows/:id/variables           → lista todas (value=null para secrets)
POST   /flows/:id/variables           → cria variável (body: key, value, isSecret)
PUT    /flows/:id/variables/:varId    → atualiza value e/ou isSecret (a key não muda)
DELETE /flows/:id/variables/:varId    → remove variável

Resposta GET: campo value é null para variáveis com isSecret=true, independentemente do que estiver no banco. Isso é garantido no nível do service, não do serializer — o campo nunca entra na resposta.

Atualizar isSecret: o value é sempre obrigatório no body do PUT (o cliente reenvia o valor a cada atualização). Ao mudar de false → true, o service cifra o value recebido e grava em valueEncrypted, zerando value. Ao mudar de true → false, grava o value recebido como texto plano, zerando valueEncrypted. O service não decifra nem migra o valor antigo do banco — em ambas as direções ele usa o value do request. O cache é regerado em ambos os casos.


Interface do usuário

Router settings

A página de configurações do Router tem um card "Variáveis do Router" que usa o componente VariablesCard — uma tabela com CRUD inline. Mostra: chave, tipo (texto/senha), valor (mascarado com botão de reveal para secrets), e ações de editar/excluir. O exemplo de uso do token ({{router.MINHA_VAR}}) é exibido abaixo da tabela.

Flow builder

O builder tem um painel lateral "Variáveis" implementado pelo componente FlowVariablesPanel. O painel mostra três seções:

  1. Variáveis do Flow — CRUD completo das variáveis do flow atual ({{flow.key}})
  2. Variáveis do Router — exibição somente-leitura das variáveis herdadas do Router pai ({{router.key}})
  3. Variáveis de sistema — referência das variáveis built-in ({{contact.name}}, {{input}}, etc.) disponíveis para consulta

A seção de variáveis do Router no painel do builder é somente-leitura intencionalmente: modificar variáveis de Router a partir do contexto de um Flow seria confuso, pois afetaria todos os outros flows do Router.


Casos de borda

Cache miss por race condition: se uma variável for criada e imediatamente uma execução de flow for disparada, o engine pode ler o cache antes de syncRouterVarsCache terminar. O engine usa {} como fallback e a interpolação falha graciosamente (token substituído por string vazia no output). Na próxima execução, o cache já estará populado.

Variável renomeada: não existe endpoint de rename — é um DELETE + POST. O cache é atualizado nos dois passos. Durante a janela entre os dois, qualquer execução usando o nome antigo obterá string vazia no lugar do token.

Router ou Flow deletado: onDelete: Cascade no Prisma apaga todas as RouterVariable/FlowVariable no Postgres. Flows executando em memória no momento do delete usarão o routerVars/flowVars que já carregaram no início da execução — esse snapshot não é recarregado mid-execution.

Nota: o cache Redis vars:router:{routerId} / vars:flow:{flowId} não é limpo quando o Router ou Flow é deletado. Os repositórios router.repository.delete() / flow.repository.delete() chamam apenas prisma.*.delete(); os helpers deleteRouterVars / deleteFlowVars existem em flow-ai-redis mas não são invocados nesse caminho (deleteRouterVars é importado em router-variables.routes.ts porém não é usado; deleteFlowVars não é importado em lugar nenhum). Como esses caches não têm TTL, a chave permanece órfã no Redis até ser sobrescrita ou removida manualmente. Isso é inofensivo (nenhuma sessão nova referencia o escopo deletado), mas convém saber ao inspecionar o Redis.


Arquivos relevantes

ArquivoPapel
packages/flow-ai-database/prisma/schema.prismaModelos RouterVariable, FlowVariable
packages/flow-ai-redis/src/cache.tsgetRouterVars, setRouterVars, deleteRouterVars, getFlowVars, setFlowVars, deleteFlowVars
services/flow-ai-core/src/common/crypto.tsCrypto.encrypt() / Crypto.decrypt() — AES-256-CBC
services/flow-ai-core/src/http/routes/router-variables.routes.tsCRUD + syncRouterVarsCache()
services/flow-ai-core/src/http/routes/flow-variables.routes.tsCRUD + syncFlowVarsCache()
services/flow-ai-engine/src/handlers/flow.tsCarregamento de routerVars/flowVars no ExecutionContext
services/flow-ai-engine/src/runtime/execute.tsDefinição de ExecutionContext com routerVars/flowVars
packages/flow-ai-runtime/src/interpolate.tsResolução de {{router.key}} e {{flow.key}} (primitiva compartilhada entre engine e agent)
apps/flow-ai-core-ui/components/studio/VariablesCard.tsxComponente de UI reutilizável para CRUD de variáveis
apps/flow-ai-core-ui/components/studio/FlowVariablesPanel.tsxPainel de variáveis no builder (flow + router + built-in)
apps/flow-ai-core-ui/app/(studio)/router/[id]/(with-sidebar)/settings/page.tsxCard de variáveis na página de settings do Router

flow-ai — plataforma proprietária de atendimento via WhatsApp