Skip to content

Modelo de dados geral

Mapa das entidades do banco PostgreSQL — grupos lógicos, relações, constraints e a lógica de negócio por trás das decisões de design. Cobre todos os domínios: conversação, helpdesk, roteamento, agentes de IA, WhatsApp Flows, WebChat, Instagram, campanhas, DeskActions, eventos/relatórios e o framework de Chat-Test.

O schema vive em um único arquivo (packages/flow-ai-database/prisma/schema.prisma) e hoje declara 77 models e 10 enums de banco. Este guia agrupa as entidades por domínio; nem todo model tem uma seção detalhada, mas todos aparecem no diagrama e na tabela de domínios abaixo.


Diagrama de entidades (grupos lógicos)

IDENTIDADE, RBAC E AUDITORIA
  User ──── Group (permissions Json + routerIds Json)

   ├── PasswordRecovery
   ├── UserDevice        (push tokens)
   └── AuditLog          (actorId, sem FK de router)
  ApiClient              (M2M — tokenHash SHA-256; mesmo formato de Group)

CONVERSAÇÃO
  Contact ────────────────────── Chat ──── Message ──── Ticket (ticketId)
     │                            │           └── User / AiAgent (sentBy*)
     ├── WebChatIdentity          └── Ticket ──── TicketTag ──── Tag
     ├── ContactOptIn             └── Ticket (previousTicketId → TicketLineage)
     └── CampaignRecipient        └── AgentTurn

ROTEAMENTO E CONFIGURAÇÃO (agregado raiz: Router)
  Router ──────────────────────────────────────────────────────────┐
     ├── Flow ──── PublishedFlow ──── User (publishedBy)            │
     │    └── FlowVariable                                          │
     ├── WhatsAppNumber ──── WhatsAppFlow ── WhatsAppFlowPublishLog │
     │                           │           WhatsAppFlowWebhookLog │
     │                           └── WhatsAppFlowDeferredJob        │
     ├── InstagramAccount ── InstagramCommentTrigger               │
     │                       InstagramCommentActivation            │
     ├── WebChatChannel ──── WebChatIdentity ──── Contact          │
     ├── ChannelTypeMapping                                        │
     ├── Queue ──── QueueAgent ──── User                           │
     │    └── QueueDistributionRule                                │
     ├── EventDefinition ──── EventOccurrence                      │
     ├── MessageTemplate ──── Campaign ──── CampaignRecipient      │
     ├── RouterVariable / RouterIntegration (→ Integration)        │
     ├── AgentGroup ──── Agent ──── User                           │
     ├── DeskActionCategory ── DeskAction ── DeskActionAgentGroup  │
     │                                       DeskActionDepartment  │
     ├── LlmCredential (llmCredentialId)                           │
     └── ContactOptIn ─────────────────────────────── Chat         │

     Router.initialFlowId ──────────────────────────── Flow ───────┘

AGENTES DE IA (scoped a Router)
  AiAgent ──── AiAgentRoute (from/to, self-graph)
     ├── AiAgentTool ──── Tool
     ├── AgentSystemPrompt ──── SystemPrompt
     ├── Guardrail (Router-level ou Agent-level)
     ├── LlmUsage / AgentTurn (AI Debugger)
     └── ragTags → AgentRagDocument ──── AgentRagChunk (pgvector)

TOOLS E INTEGRAÇÕES
  Tool ──── PublishedTool          ToolExecutionLog (sem FK)
  IntegrationServer ──── IntegrationEndpoint
  ImportDraft (wizard de importação de bots externos)

HELPDESK
  Queue ──── Ticket ──── Chat
  User (openedBy / assignedUser / closedBy) ──── Ticket
  Ticket.previousTicketId ──── Ticket (cadeia de transferências)

RESPOSTAS PRONTAS E ATENDIMENTO
  Department ──── DepartmentAgent ──── User
      └── DepartmentMessageGroup ──── MessageGroup ──── QuickReply

RELATÓRIOS, EVENTOS E ERROS
  SessionRuntimeError  (sem relação FK — sessionId é string)
  EventDefinition ──── EventOccurrence
  Report               (routerId/ownerId como strings, sem FK)
  ScriptTemplate       (sem relações — biblioteca standalone)
  FlowTag              (sem relações — tag visual do builder)
  ObservabilitySettings (SINGLETON — id fixo "singleton")
  MetaDataDeletionRequest (callback de exclusão de dados da Meta)

CHAT-TEST FRAMEWORK
  FlowGuide ──── FlowGuideStep
      └── TestRun ──── TestRunStep

Domínios e models

DomínioModels
Identidade / RBAC / AuditoriaUser, Group, PasswordRecovery, ApiClient, UserDevice, AuditLog
Router e configuraçãoRouter, RouterVariable, WhatsAppNumber, LlmCredential, ObservabilitySettings, Integration, RouterIntegration, MetaDataDeletionRequest, ChannelTypeMapping
ConversaçãoContact, Chat, Message, ContactOptIn
HelpdeskTicket, Queue, QueueAgent, QueueDistributionRule, Tag, TicketTag
Atendimento / respostas prontasDepartment, DepartmentAgent, MessageGroup, DepartmentMessageGroup, QuickReply, AgentGroup, Agent
Flows e toolsFlow, PublishedFlow, FlowVariable, FlowTag, ScriptTemplate, Tool, PublishedTool, ToolExecutionLog, IntegrationServer, IntegrationEndpoint, ImportDraft, SessionRuntimeError
WhatsApp Flows / templatesWhatsAppFlow, WhatsAppFlowPublishLog, WhatsAppFlowWebhookLog, WhatsAppFlowDeferredJob, MessageTemplate
WebChatWebChatChannel, WebChatIdentity
InstagramInstagramAccount, InstagramCommentTrigger, InstagramCommentActivation
Agentes de IAAiAgent, AiAgentRoute, AiAgentTool, SystemPrompt, AgentSystemPrompt, Guardrail, AgentRagDocument, AgentRagChunk, LlmUsage, AgentTurn
DeskActionsDeskActionCategory, DeskAction, DeskActionAgentGroup, DeskActionDepartment
Eventos e relatóriosEventDefinition, EventOccurrence, Report
CampanhasCampaign, CampaignRecipient
Chat-TestFlowGuide, FlowGuideStep, TestRun, TestRunStep

Grupo: Identidade, RBAC e auditoria

User

O User é o operador do sistema — agente de helpdesk, administrador ou desenvolvedor.

prisma
User {
  id, name, email (unique), phone, password (bcrypt)
  userRole: String @default("member")  // "admin" bypassa todas as verificações de grupo
  groupId? → Group (onDelete: SetNull)
}

O userRole é um papel de sistema em texto livre — "admin" ignora todas as verificações de grupo; qualquer outro valor (default "member") fica sujeito às permissões do Group. Um User não tem relação direta com Router ou Flow; suas permissões vêm do Group. O User aparece no domínio como: publicador de flows, agente assignado a tickets, membro de queues/departments, remetente de mensagens (Message.sentByUserId), criador de campanhas e ator de AuditLog.

Group (RBAC)

O RBAC é baseado em grupos — não existe mais Role/RoleRoute.

prisma
Group {
  id, name
  permissions: Json @default("[]")  // GroupPermission[]: [{ resource, actions: string[] }]
  routerIds:   Json @default("[]")  // string[] — vazio = sem restrição de router
  users: User[]
}

Cada usuário pertence a no máximo um Group. permissions é um array de { resource, actions }; routerIds, quando não vazio, restringe o grupo aos Routers listados (e bloqueia recursos globais). Ver o guia de Autenticação e RBAC para a semântica operacional.

ApiClient (M2M)

Acesso máquina-a-máquina com token estático.

prisma
ApiClient {
  id, name
  tokenHash (unique)          // SHA-256(token) em hex — o token nunca é persistido
  permissions: Json @default("[]")  // mesmo formato de Group
  routerIds:   Json @default("[]")  // mesmo formato de Group.routerIds
  active, expiresAt?, lastUsedAt?
  createdByUserId? → User (SetNull)
}

O token só existe no momento da criação; o banco guarda apenas o hash SHA-256, permitindo lookup O(1) na autenticação sem expor o segredo. permissions/routerIds reutilizam exatamente o formato do Group.

PasswordRecovery, UserDevice e AuditLog

prisma
PasswordRecovery { userId → User (Cascade), code, expiresAt, completed }
UserDevice       { userId → User (Cascade), token (unique), platform }  // push tokens
AuditLog {
  action: AuditAction, entityType?: AuditEntityType, entityId?
  actorId? → User (SetNull), routerId? (sem FK), status: AuditStatus
  changes: Json?, metadata: Json?  // ip/userAgent
}

AuditLog é append-only e nunca bloqueia a operação principal (fire-and-forget para eventos de auth). routerId é uma string sem FK — o log sobrevive à remoção do Router. Os enums AuditAction, AuditEntityType e AuditStatus estão listados em Enums de banco.


Grupo: Conversação

Contact

prisma
Contact {
  id, phoneNumber (unique), name, origin?, contactVariables: Json?
  context?: @db.Text             // resumo histórico acumulado (agente summarizador)
  lastSessionSummary?: @db.Text  // resumo da última sessão, injetado no snapshot do agente de IA
}

O phoneNumber é o identificador universal do canal:

  • WhatsApp: número real (+5511...)
  • WebChat: wc-{channelId}-{userId} (sintético)
  • Instagram: identidade derivada do igUserId (transporte reaproveita os campos WhatsApp-shaped)

contactVariables é um JSON livre — variáveis persistentes do contato, preenchidas por SetContactVariableAction no engine ou via API. Diferem das session.variables, que são efêmeras. context e lastSessionSummary são campos de texto alimentados pelos agentes de IA (resumo histórico acumulado e resumo da sessão mais recente).

Chat

prisma
Chat {
  id, contactId → Contact
  routerId? → Router     // router ao qual esta conversa pertence (SetNull)
  status: ChatStatus     // open | finished | expired
  endedAt?, endReason?
  summary?: @db.Text     // resumo completo da sessão gerado por LLM ao encerrar
}

Um Chat representa toda a linha do tempo de conversação de um Contact com um Router específico. O campo routerId é o que permite que o mesmo contato tenha conversas simultâneas e independentes em routers diferentes (números/canais distintos).

  • Um contato pode ter no máximo um Chat com status open por router — o find-or-create do orchestrator filtra { contactId, routerId, status: "open" }.
  • Quando o mesmo contato interage com dois routers, existem dois Chats distintos — as mensagens e tickets nunca se misturam.
  • routerId é nullable para compatibilidade com dados históricos criados antes do isolamento por router.

Message e Ticket são filhos do Chat.

Message

prisma
Message {
  id, chatId → Chat
  ticketId? → Ticket (SetNull)     // ticket ativo no momento (forward-only)
  sender: MessageSender            // customer | flow | agent | human
  type, content
  whatsappMessageId? (unique)      // wamid da Meta, null para WebChat/Instagram
  metadata: Json?
  status, statusUpdatedAt
  sentByUserId?    → User    (SetNull, @relation "MessagesSentByUser")
  sentByAiAgentId? → AiAgent (SetNull, @relation "MessagesSentByAiAgent")
}

O campo sender: "human" (sinônimo de "agent" no contexto do enum, distinto de "flow") indica mensagem enviada por operador humano via desk. ticketId correlaciona a mensagem ao ticket ativo no instante do envio/recebimento (nullable em mensagens anteriores à migration). sentByUserId/sentByAiAgentId identificam a autoria: operador humano ou agente de IA, respectivamente. metadata armazena dados extras como IDs de reply, mídia, etc. sem schema rígido.

Ticket

prisma
Ticket {
  id
  kind: String @default("human")  // "human" (atendimento humano) | "ai" (sessão de agente de IA)
  chatId → Chat
  previousTicketId? → Ticket   // self-referential (@relation "TicketLineage")
  nextTickets: Ticket[]        // inverse da lineage
  sessionId?                   // id da SessionState no Redis, populado no handoff
  queueId? → Queue
  status: TicketStatus         // waiting | assigned | closed
  openedByKind: TicketOpenedByKind  // flow | agent | system | ai_agent
  openReason: TicketOpenReason      // human_handoff | agent_transfer | queue_transfer | ai_session
  openedByUserId? → User
  assignedUserId? → User
  assignedAt?
  closedAt?, closedByUserId? → User
  closeKind?: TicketCloseKind       // resolved | agent_transfer | queue_transfer | handoff_replaced |
                                    // system | meta_window_expired | abandoned
  closeReason?
  summary?: @db.Text                // resumo do atendimento humano gerado por LLM ao fechar
  sentimentResult?: Json            // último resultado de análise de sentimento
  sentimentHistory?: Json           // série histórica por turno (tickets de IA)
  tags: TicketTag[]
  messages: Message[]
}

kind separa os dois usos do ticket: "human" para atendimento por operador e "ai" para uma sessão de agente de IA (aberta quando o flow entra em mode:"agent").

previousTicketId é a chave da cadeia de transferências (relação TicketLineage, com o inverso nextTickets) — cada transferência fecha o ticket atual e abre um novo com previousTicketId apontando para o anterior. Isso preserva o histórico completo sem alterar registros passados. O sessionId guarda o id da SessionState no Redis (populado a partir do HelpdeskHandoffEvent), permitindo que o close path retome o flow via humanSessionEnded sem precisar de SCAN.

A tripla openedByUserId → User, assignedUserId → User, closedByUserId → User usa três @relation nomeadas distintas no Prisma para três papéis diferentes do User no mesmo ticket. Note que queueId é opcional — tickets de IA podem existir sem fila.


Grupo: Roteamento e configuração

Router

O agregado raiz. Tudo pertence a um Router.

prisma
Router {
  id, name, description?, active, logoUrl?
  metaVerifyToken?               // NÃO é @unique — ver nota abaixo
  metaAppSecretEncrypted?
  initialFlowId? (unique) → Flow (@relation "RouterInitialFlow", SetNull)
  llmCredentialId? → LlmCredential (SetNull)  // credencial LLM de todos os agentes deste router
  tz: String @default("America/Sao_Paulo")   // fuso IANA (validação de loginSchedule)
  helpdeskAgentPrefixEnabled, helpdeskAgentPrefixTemplate
  sentimentConfig?: Json         // config da análise de sentimento do copilot
  copilotConfig?: Json           // visibilidade/ordem dos blocos da aba Copilot
}

Nota: metaVerifyToken não é @unique. Vários routers podem compartilhar o mesmo verify token quando usam o mesmo Meta App (cenário típico de Embedded Signup). O cache meta:verify-token:{token} é keyed pelo próprio token, então writes idempotentes são seguros.

Relações do Router (o agregado raiz — praticamente todo domínio pendura aqui):

  • flows[] — flows conversacionais
  • whatsAppNumbers[] — números conectados à Meta
  • instagramAccounts[] — contas Instagram Business
  • channelTypeMappings[] — mapeamento de tipos de mensagem entre canais
  • whatsAppFlows[] — formulários interativos da Meta
  • webChatChannels[] — canais de webchat
  • queues[], queueDistributionRules[] — helpdesk e roteamento de tickets
  • messageTemplates[], campaigns[] — templates HSM e disparos ativos
  • eventDefinitions[] — eventos rastreados
  • contactOptIns[], variables[], chats[] — opt-ins, variáveis estáticas, chats
  • agentGroups[], agents[] — grupos operacionais de agentes humanos
  • aiAgents[], guardrails[], systemPrompts[], ragDocuments[] — subsistema de agentes de IA
  • routerIntegrations[] — integrações externas ativadas
  • deskActionCategories[], deskActions[] — ações rápidas do desk
  • importDrafts[] — rascunhos de importação de bots externos

initialFlowId tem @unique porque um mesmo Flow não pode ser o flow inicial de dois Routers diferentes.

Flow e PublishedFlow

prisma
Flow {
  id, routerId → Router
  name, description?
  draftDefinition: Json        // definição editável (builder)
  activePublicationId? (unique) → PublishedFlow
  publications: PublishedFlow[]
  variables: FlowVariable[]
}

PublishedFlow {
  id, flowId → Flow
  definition: Json             // snapshot imutável da publicação
  version: Int
  publishedById → User
  publishedAt, notes?
  @@unique([flowId, version])
}

O padrão draft/publish separa a edição da execução:

  • Flow.draftDefinition → o que o builder salva (mutável)
  • PublishedFlow.definition → snapshot imutável carregado pelo engine
  • Flow.activePublicationId → qual publicação o engine usa agora

O engine carrega a PublishedFlow referenciada, não o Flow diretamente — garantindo que edições em progresso não afetem sessões ativas.

Queue e QueueDistributionRule

prisma
Queue {
  id, routerId → Router
  name, isActive
  distributionStrategy: DistributionStrategy  // least_loaded | round_robin | manual_pickup
  maxConcurrentTicketsPerAgent: Int
  schedule: Json?, channels: Json?
  agents: QueueAgent[]
  tickets: Ticket[]
  distributionRules: QueueDistributionRule[]
  @@unique([routerId, name])
}

QueueDistributionRule {
  id, routerId → Router
  priority: Int
  conditions: Json          // array de Condition (AND implícito)
  targetQueueId → Queue
  flowId?: String           // escopo por flow (opcional)
  isActive: Boolean
}

QueueDistributionRule.conditions é avaliado contra contextVariables e contact.variables no momento do handoff. A primeira regra ativa (por prioridade) define a fila de destino.


Grupo: Respostas prontas

Department, MessageGroup, QuickReply

prisma
Department { id, name (unique), isActive, agents: DepartmentAgent[], messageGroups: DepartmentMessageGroup[] }
DepartmentAgent { departmentId → Department, userId → User, @@unique([departmentId, userId]) }

MessageGroup { id, name, isActive, sortOrder, quickReplies: QuickReply[], departments: DepartmentMessageGroup[] }
DepartmentMessageGroup { departmentId → Department, messageGroupId → MessageGroup, @@unique }

QuickReply { id, title, message, messageGroupId → MessageGroup, isActive, sortOrder }

Estrutura hierárquica: Departments agrupam usuários e determinam quais MessageGroups (grupos de respostas prontas) ficam visíveis para eles. O agente vê apenas as QuickReplys dos grupos de seu departamento.


Grupo: Canais externos

WhatsAppNumber

prisma
WhatsAppNumber {
  id, routerId → Router
  phoneNumberId (unique)      // ID da Meta (ex: "1234567890")
  displayName, wabaId?
  accessTokenEncrypted        // AES-256-CBC
  active: Boolean
  encryptionPrivateKeyEncrypted? @db.Text  // RSA-2048 PKCS#8
  encryptionPassphraseEncrypted?           // passphrase da chave RSA
  encryptionPublicKey? @db.Text            // SPKI PEM (não cifrado)
  encryptionUploadedAt?
}

WebChatChannel e WebChatIdentity

prisma
WebChatChannel { id, routerId → Router, name, isTestChannel, appearance: Json? }
WebChatIdentity { channelId → WebChatChannel, contactId → Contact, sessionKeyHash, @@unique([channelId, contactId]) }

sessionKeyHash é o hash (bcrypt) do sessionKey que o browser persiste — o flow-ai-webchat-gateway valida a identidade contra esse hash na conexão do socket.

InstagramAccount e gatilhos de comentário

prisma
InstagramAccount {
  id, routerId → Router
  igUserId (unique), pageId?, username?
  accessTokenEncrypted        // AES-256-CBC
  igAppSecretEncrypted?
  active: Boolean
  commentTriggers: InstagramCommentTrigger[]
  commentActivations: InstagramCommentActivation[]
}

InstagramCommentTrigger {
  id, instagramAccountId → InstagramAccount
  mediaId, keywords: String[], matchType   // "contains" | "exact"
  openingType, destination                  // régua de moderação de comentários
  flowId?, blockId?, targetQueueId?, targetUserId?
  dmText, publicReplyText?, active
}

InstagramCommentActivation {
  id, instagramAccountId → InstagramAccount
  triggerId?                // referência fraca — o log persiste se o gatilho for removido
  commentId, matchedKeyword, matchType
  dmDispatched, publicReplyDispatched
  @@unique([instagramAccountId, commentId])  // 1 ativação por comentário (idempotência)
}

O canal Instagram tem o mesmo formato do WhatsApp: accessTokenEncrypted é decifrado pelo flow-ai-core para os caches ig:*, de onde o flow-ai-ig-api lê. InstagramCommentTrigger é a "régua" de moderação (palavras-chave → DM/resposta pública), e InstagramCommentActivation é o log append-only de cada disparo, com commentId como chave de idempotência.

WhatsAppFlow e jobs deferidos

prisma
WhatsAppFlow {
  id, routerId → Router, whatsAppNumberId → WhatsAppNumber
  name, metaFlowId? (unique)
  publishState: String  // UNPUBLISHED | DRAFT | PUBLISHED | DEPRECATED | BLOCKED | ERROR
  endpointMode: String @default("declarative")  // declarative | external
  flowDefinitionJson: Json?   // Meta Flow Definition v6.0
  endpointConfig: Json?       // config do engine declarativo
  // + campos de controle: flowToken, flowCta, flowAction, categories, endpointUrl, etc.
  publishLogs: WhatsAppFlowPublishLog[]
  webhookLogs: WhatsAppFlowWebhookLog[]
}

WhatsAppFlowDeferredJob {
  id, whatsAppFlowId
  jobKey (unique)           // deduplica redeliveries do stream
  publishedToolId, inputs: Json
  status                    // pending | in_progress | completed | failed
  attempts, maxAttempts (10), baseDelaySec (120), nextAttemptAt
}

WhatsAppFlowDeferredJob executa tools que só fazem sentido minutos após o atendimento (ex.: agendar O.S. no IXC), com retry/backoff. jobKey é único e deduplica redeliveries do stream wa-flow-logs.


Grupo: Eventos e observabilidade

EventDefinition e EventOccurrence

prisma
EventDefinition {
  id, routerId → Router
  name, description?, variables: String[]  // nomes das variáveis capturadas
  deletedAt?                               // soft delete
}

EventOccurrence {
  id, eventDefinitionId → EventDefinition
  eventName, routerId, contactId, chatId, flowId, blockId
  capturedVariables: Json
  occurredAt: DateTime
}

Rastreamento de eventos de domínio disparados pelo engine. EventDefinition define o schema do evento; EventOccurrence registra cada ocorrência com as variáveis capturadas no momento.

SessionRuntimeError

prisma
SessionRuntimeError {
  id, sessionId, chatId?, flowId?, blockId, phase, message
  occurredAt, resolvedAt?
}

Sem FK para Chat ou FlowsessionId é uma string que pode referenciar sessões já expiradas do Redis. Permite histórico de erros mesmo após a sessão sumir.

Report

prisma
Report {
  id, routerId, name, description?
  ownerId?, visibility: String @default("private")
  definition: Json   // widgets e consultas de agregação
}

Relatório personalizado (builder). routerId/ownerId são strings simples sem relação FK — mesmo padrão de EventOccurrence, mantendo a migration enxuta.

ObservabilitySettings (singleton)

prisma
ObservabilitySettings {
  id: String @id @default("singleton")   // sempre 1 linha
  enabled: Boolean
  logfireTokenEncrypted?   // AES-256-CBC (flow-ai-core é a fronteira de cripto)
  otlpEndpoint?, sampleRate: Float, captureContent: Boolean
  serviceToggles?: Json    // { "agent": true, "core": false }
}

Config global de OpenTelemetry → Logfire. Singleton com id fixo "singleton". A UI escreve aqui e o flow-ai-core publica no Redis (observability:config); os serviços leem via initTelemetry no boot.

MetaDataDeletionRequest

prisma
MetaDataDeletionRequest {
  id, confirmationCode (unique)   // usado na URL pública de status
  metaUserId?                     // user_id do Facebook do signed_request
  source: String @default("meta_callback")  // meta_callback | manual
  status: String @default("pending")         // pending | completed | failed
}

Solicitação de exclusão de dados recebida via Data Deletion Callback da Meta (signed_request HMAC-SHA256). confirmationCode permite ao usuário consultar o status em /data-deletion/{code}, formato exigido pela Meta.


Grupo: Agentes de IA

Subsistema multi-agente LLM scoped a Router. O provider vem da LlmCredential do Router (Router.llmCredentialId).

LlmCredential

prisma
LlmCredential {
  id, label, provider          // "openai" | "anthropic" | ...
  apiKeyEncrypted              // AES-256-CBC (flow-ai-core é a fronteira de cripto)
  models: String[]             // IDs de modelos compatíveis
  isActive
  routers: Router[]            // credencial global reutilizada por N routers
}

Credencial global (não scoped a Router) — gerenciada apenas por admin. Cada Router referencia uma via llmCredentialId.

AiAgent, rotas e vínculos

prisma
AiAgent {
  id, routerId → Router
  name, type          // "orchestrator" | "specialist" | "system"
  task?               // papel de agentes de sistema (ex: "prompt-helper")
  model, specificPrompt?, description?
  isActive, ragTags: String[]
  routesFrom/routesTo: AiAgentRoute[]   // grafo de transferência entre agentes
  tools: AiAgentTool[]                  // → Tool publicada
  systemPrompts: AgentSystemPrompt[]    // → SystemPrompt (com ordem)
  guardrails: Guardrail[]
}

AiAgentRoute  { fromAgentId → AiAgent, toAgentId → AiAgent, @@unique([fromAgentId, toAgentId]) }
AiAgentTool   { aiAgentId → AiAgent, toolId → Tool, @@unique([aiAgentId, toolId]) }
SystemPrompt  { id, routerId → Router, name, content @db.Text, isActive }
AgentSystemPrompt { agentId → AiAgent, systemPromptId → SystemPrompt, order, @@id([agentId, systemPromptId]) }
Guardrail     { id, routerId → Router, agentId? → AiAgent, instruction @db.Text, order }

type distingue orquestrador, especialista e agente de sistema. AiAgentRoute é um grafo direcionado — fromAgent conhece o caminho para toAgent, injetado no prompt de quem pode transferir. Guardrail com agentId = null é Router-level (aplica a todos os agentes); com agentId preenchido, Agent-level.

RAG, uso e debugging

prisma
AgentRagDocument { id, routerId → Router, name, tags: String[], originalContent @db.Text, chunks: AgentRagChunk[] }
AgentRagChunk {
  id, documentId → AgentRagDocument, chunkIndex, content @db.Text
  embedding: Unsupported("vector(1536)")?   // pgvector — inserido via SQL raw
}

LlmUsage  { id, aiAgentId? → AiAgent, toolId? → Tool, routerId, sessionId, chatId, provider, model, promptTokens, completionTokens, totalTokens, estimatedCostUsd? }
AgentTurn { id, sessionId, chatId → Chat, aiAgentId → AiAgent, turnIndex, userMessage?, assistantContent?, toolCallsJson, instructionsSnapshot?, promptTokens, completionTokens }

AgentRagChunk.embedding usa o tipo pgvector vector(1536) — o Prisma não suporta nativamente (Unsupported(...)), então os vetores são gerenciados via SQL bruto e há um índice HNSW para busca por similaridade coseno. LlmUsage alimenta o dashboard de custos (fonte pode ser um AiAgent ou um bloco callAI dentro de uma Tool). AgentTurn alimenta o AI Debugger (retenção de 30 dias via audit-retention.worker).


Grupo: Flows, tools e integrações

Flow e PublishedFlow estão documentados no grupo de roteamento — ver Flow e PublishedFlow. O padrão draft/publish permanece o mesmo.

Tool, PublishedTool e ToolExecutionLog

prisma
Tool {
  id, name, description? @db.Text
  kind: String @default("custom")   // "custom" (mini-flow) | "native" (função do executor)
  nativeName? (unique)              // canônico do executor quando kind="native"
  definition?: Json                 // ToolDefinition serializada (kind="custom")
  publishedTool?: PublishedTool
  agentBindings: AiAgentTool[]
}

PublishedTool    { id, toolId (unique) → Tool, definition: Json }  // lido pelo engine via tool:{id}
ToolExecutionLog { id, toolId, publishedToolId, durationMs, success }  // sem FK — auditoria

Uma Tool é um mini-flow reutilizável (kind="custom") ou uma função nativa do executor (kind="native"). O engine lê o snapshot PublishedTool via Redis. ToolExecutionLog não tem FK para Tool — o log persiste mesmo após a tool ser deletada.

IntegrationServer, IntegrationEndpoint e Integration

prisma
IntegrationServer   { id, name, baseUrl, headers: Json, endpoints: IntegrationEndpoint[] }
IntegrationEndpoint { id, serverId → IntegrationServer, name, path, method, params: Json, responses: Json }

Integration       { id, provider, name, baseUrl?, credentialsEncrypted? @db.Text, isActive }
RouterIntegration { routerId → Router, integrationId → Integration, isEnabled, @@id([routerId, integrationId]) }

IntegrationServer/IntegrationEndpoint pré-preenchem blocos httpCall no editor de Tools. Integration/RouterIntegration são integrações globais (Wiki.js, Notion, Confluence) ativáveis por Router.

ImportDraft

prisma
ImportDraft {
  id, routerId → Router, createdById → User
  source        // "blip"
  status        // preprocessing | reviewing | finalizing | completed | failed
  originalJson: Json, draftBlocks: Json, ...
}

Estado intermediário do wizard de importação de bots externos (ex: Blip) entre o JSON original e um FlowDefinition nativo.


Grupo: Templates e campanhas

MessageTemplate

prisma
MessageTemplate {
  id, routerId → Router, wabaId?
  name, language, category   // UTILITY | MARKETING | AUTHENTICATION
  status: String  // LOCAL | PENDING | APPROVED | REJECTED | PAUSED | DISABLED
  components: Json   // estrutura dos componentes HSM (header, body, footer, buttons)
  metaTemplateId? (unique), qualityScore?
  @@unique([routerId, name, language])
}

Templates HSM para envio fora da janela de 24h da Meta. O status reflete o estado de aprovação na Meta; qualityScore (GREEN/YELLOW/RED) vem do webhook message_template_quality_update.

Campaign e CampaignRecipient

prisma
Campaign {
  id, routerId → Router, whatsAppNumberId? → WhatsAppNumber (SetNull)
  name, mode                 // "template" | "freeform"
  templateId? → MessageTemplate (SetNull)   // obrigatório quando mode=template
  freeformContent?: Json                     // obrigatório quando mode=freeform
  targetFlowId?/targetBlockId?/targetQueueId?/targetUserId?  // roteamento pós-recepção
  status: String @default("draft")  // draft | scheduled | in_progress | paused | completed | cancelled | failed
  scheduledAt?, rateLimitPerSec (10)
  totalRecipients/sentCount/failedCount/cancelledCount/sentByTemplateCount/sentByFreeformCount
  recipients: CampaignRecipient[]
}

CampaignRecipient {
  id, campaignId → Campaign, contactId? → Contact (SetNull)
  phoneNumber, name?, variables: Json   // ordem dos {{N}} do template
  status: String @default("pending")    // pending | sending | sent | delivered | read | failed | cancelled
  wamid? (unique)                        // cruzado com stream:status para delivered/read
}

Disparo ativo de mensagens para 1..N destinatários. CampaignRecipient funciona como fila durável — o worker pega pending, marca sending, depois sent/failed; delivered/read vêm via stream:status cruzando por wamid. targetFlowId/targetQueueId permitem rotear a resposta do destinatário direto para um flow/fila em vez do initialFlowId do router.

ScriptTemplate e FlowTag

prisma
ScriptTemplate { id, name, description?, source }  // Biblioteca de scripts reutilizáveis
FlowTag { id, title (unique), color }              // Tags visuais para organização no builder

Sem relações FK — são entidades standalone usadas pela UI.


Grupo: DeskActions

Ações rápidas configuráveis para operadores no helpdesk, scoped a Router.

prisma
DeskActionCategory   { id, routerId → Router, name, icon?, sortOrder, actions: DeskAction[] }
DeskAction {
  id, routerId → Router, categoryId → DeskActionCategory
  name, description?, executorType   // "tool" | "agent"
  agentId?, formConfig: Json         // FormField[]
  pipeline: Json                     // PipelineStep[]
  agentGroups: DeskActionAgentGroup[]
  departments: DeskActionDepartment[]
}
DeskActionAgentGroup { deskActionId → DeskAction, agentGroupId → AgentGroup, @@unique }
DeskActionDepartment { deskActionId → DeskAction, departmentId → Department, @@unique }

Cada DeskAction executa via tool ou via agente (executorType) e é visível para agentes conforme os AgentGroup/Department vinculados.


Grupo: Atendimento (AgentGroup e Agent)

Vínculo operacional entre Users e um Router como atendentes.

prisma
AgentGroup {
  id, routerId → Router, name
  canActiveDispatch, loginSchedule?: Json
  onScheduleEnd: String @default("block-login")  // "block-login" | "soft-kick"
  agents: Agent[]
}
Agent {
  id, userId → User, routerId → Router, agentGroupId → AgentGroup
  canActiveDispatch?, loginSchedule?, onScheduleEnd?  // null = herda do AgentGroup
  @@unique([userId, routerId])
}

AgentGroup define defaults de permissão/agenda; os campos nulos de Agent herdam do grupo. Distinto de QueueAgent/DepartmentAgent, que são apenas vínculos de pertencimento a filas/departamentos.


Grupo: Chat-Test framework

Testes declarativos de flows executados por canal.

prisma
FlowGuide { id, flowId → Flow, name, channels: String[], blocking, runOnPublish, stepTimeoutMs, steps: FlowGuideStep[], runs: TestRun[] }
FlowGuideStep { id, guideId → FlowGuide, stepOrder, send: Json, expectMessages, assertions: Json }
TestRun { id, guideId → FlowGuide, channelType, triggeredBy, status, startedAt, completedAt?, steps: TestRunStep[] }
TestRunStep { id, testRunId → TestRun, stepIndex, status, sentMessage?: Json, receivedMessages?: Json, rttMs?, assertionResults?: Json }

Um FlowGuide é um cenário com N steps enviados sequencialmente pelo runner; blocking=true bloqueia a publicação do flow quando falha. TestRun/TestRunStep guardam o resultado por execução (canal + gatilho).


Enums de banco

O schema declara 10 enums PostgreSQL (valores verbatim):

EnumValores
ChatStatusopen, finished, expired
MessageSendercustomer, flow, agent, human
DistributionStrategyleast_loaded, round_robin, manual_pickup
TicketStatuswaiting, assigned, closed
TicketOpenedByKindflow, agent, system, ai_agent
TicketOpenReasonhuman_handoff, agent_transfer, queue_transfer, ai_session
TicketCloseKindresolved, agent_transfer, queue_transfer, handoff_replaced, system, meta_window_expired, abandoned
AuditActionLOGIN, TOKEN_REFRESH, LOGOUT, PASSWORD_CHANGE, CREATE, UPDATE, DELETE, PUBLISH, UNPUBLISH, CAMPAIGN_DISPATCHED, RETENTION_CLEANUP
AuditEntityTypeUSER, ROUTER, WHATSAPP_NUMBER, FLOW, PUBLISHED_FLOW, TOOL, PUBLISHED_TOOL, AGENT, AGENT_GROUP, AI_AGENT, DEPARTMENT, QUEUE, LLM_CREDENTIAL, INTEGRATION_SERVER, INTEGRATION_SERVER_ENDPOINT, MESSAGE_TEMPLATE, CAMPAIGN, SCRIPT_TEMPLATE, SYSTEM_PROMPT, GUARDRAIL, FLOW_VARIABLE, ROUTER_VARIABLE, API_CLIENT, WEBCHAT_CHANNEL
AuditStatussuccess, failure

Muitos campos de status são String livre (não enums de banco) — ex.: MessageTemplate.status, Campaign.status, WhatsAppFlow.publishState, ImportDraft.status. Os valores válidos estão documentados nos comentários do schema e validados na app layer via Zod.


Convenções do schema

ConvençãoDetalhe
IDsCUID gerado pelo Prisma (@default(cuid()))
TimestampscreatedAt @default(now()), updatedAt @updatedAt em toda entidade principal
Soft deleteSó em EventDefinition (deletedAt?) — demais entidades usam delete hard
CascadeonDelete: Cascade na maioria das relações filho → pai scoped a Router (ex: RouterVariable, QueueAgent, WhatsAppNumber, AiAgent). Nem toda relação é Cascade: Ticket.chat e Message.chat, por exemplo, não propagam delete.
SetNullonDelete: SetNull onde a FK opcional não deve propagar delete (ex: Router.initialFlowId)
JSONCampos estruturados mas sem schema rígido usam Json (contactVariables, conditions, components)
@db.TextCampos longos como chaves PEM e definições de flow usam @db.Text para evitar limite de varchar(255)
TabelasNomes em snake_case via @@map() — todos os modelos mapeiam para plural snake_case

Arquivos relevantes

ArquivoPapel
packages/flow-ai-database/prisma/schema.prismaSchema completo (77 models, 10 enums) — fonte da verdade
packages/flow-ai-database/prisma/migrations/~97 migrações com histórico evolutivo
packages/flow-ai-types/src/index.tsTipos TypeScript dos streams e DTOs (não espelham o schema diretamente)
packages/flow-ai-types/src/session.tsSessionState/SessionMode (flow|human|agent) — estado efêmero no Redis, não persistido no Postgres

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