Appearance
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 ──── TestRunStepDomínios e models
| Domínio | Models |
|---|---|
| Identidade / RBAC / Auditoria | User, Group, PasswordRecovery, ApiClient, UserDevice, AuditLog |
| Router e configuração | Router, RouterVariable, WhatsAppNumber, LlmCredential, ObservabilitySettings, Integration, RouterIntegration, MetaDataDeletionRequest, ChannelTypeMapping |
| Conversação | Contact, Chat, Message, ContactOptIn |
| Helpdesk | Ticket, Queue, QueueAgent, QueueDistributionRule, Tag, TicketTag |
| Atendimento / respostas prontas | Department, DepartmentAgent, MessageGroup, DepartmentMessageGroup, QuickReply, AgentGroup, Agent |
| Flows e tools | Flow, PublishedFlow, FlowVariable, FlowTag, ScriptTemplate, Tool, PublishedTool, ToolExecutionLog, IntegrationServer, IntegrationEndpoint, ImportDraft, SessionRuntimeError |
| WhatsApp Flows / templates | WhatsAppFlow, WhatsAppFlowPublishLog, WhatsAppFlowWebhookLog, WhatsAppFlowDeferredJob, MessageTemplate |
| WebChat | WebChatChannel, WebChatIdentity |
InstagramAccount, InstagramCommentTrigger, InstagramCommentActivation | |
| Agentes de IA | AiAgent, AiAgentRoute, AiAgentTool, SystemPrompt, AgentSystemPrompt, Guardrail, AgentRagDocument, AgentRagChunk, LlmUsage, AgentTurn |
| DeskActions | DeskActionCategory, DeskAction, DeskActionAgentGroup, DeskActionDepartment |
| Eventos e relatórios | EventDefinition, EventOccurrence, Report |
| Campanhas | Campaign, CampaignRecipient |
| Chat-Test | FlowGuide, 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
Chatcom statusopenpor 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:
metaVerifyTokennã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 cachemeta: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 conversacionaiswhatsAppNumbers[]— números conectados à MetainstagramAccounts[]— contas Instagram BusinesschannelTypeMappings[]— mapeamento de tipos de mensagem entre canaiswhatsAppFlows[]— formulários interativos da MetawebChatChannels[]— canais de webchatqueues[],queueDistributionRules[]— helpdesk e roteamento de ticketsmessageTemplates[],campaigns[]— templates HSM e disparos ativoseventDefinitions[]— eventos rastreadoscontactOptIns[],variables[],chats[]— opt-ins, variáveis estáticas, chatsagentGroups[],agents[]— grupos operacionais de agentes humanosaiAgents[],guardrails[],systemPrompts[],ragDocuments[]— subsistema de agentes de IArouterIntegrations[]— integrações externas ativadasdeskActionCategories[],deskActions[]— ações rápidas do deskimportDrafts[]— 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 engineFlow.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 Flow — sessionId é 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
FlowePublishedFlowestã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 — auditoriaUma 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 builderSem 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):
| Enum | Valores |
|---|---|
ChatStatus | open, finished, expired |
MessageSender | customer, flow, agent, human |
DistributionStrategy | least_loaded, round_robin, manual_pickup |
TicketStatus | waiting, assigned, closed |
TicketOpenedByKind | flow, agent, system, ai_agent |
TicketOpenReason | human_handoff, agent_transfer, queue_transfer, ai_session |
TicketCloseKind | resolved, agent_transfer, queue_transfer, handoff_replaced, system, meta_window_expired, abandoned |
AuditAction | LOGIN, TOKEN_REFRESH, LOGOUT, PASSWORD_CHANGE, CREATE, UPDATE, DELETE, PUBLISH, UNPUBLISH, CAMPAIGN_DISPATCHED, RETENTION_CLEANUP |
AuditEntityType | USER, 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 |
AuditStatus | success, 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ção | Detalhe |
|---|---|
| IDs | CUID gerado pelo Prisma (@default(cuid())) |
| Timestamps | createdAt @default(now()), updatedAt @updatedAt em toda entidade principal |
| Soft delete | Só em EventDefinition (deletedAt?) — demais entidades usam delete hard |
| Cascade | onDelete: 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. |
| SetNull | onDelete: SetNull onde a FK opcional não deve propagar delete (ex: Router.initialFlowId) |
| JSON | Campos estruturados mas sem schema rígido usam Json (contactVariables, conditions, components) |
@db.Text | Campos longos como chaves PEM e definições de flow usam @db.Text para evitar limite de varchar(255) |
| Tabelas | Nomes em snake_case via @@map() — todos os modelos mapeiam para plural snake_case |
Arquivos relevantes
| Arquivo | Papel |
|---|---|
packages/flow-ai-database/prisma/schema.prisma | Schema 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.ts | Tipos TypeScript dos streams e DTOs (não espelham o schema diretamente) |
packages/flow-ai-types/src/session.ts | SessionState/SessionMode (flow|human|agent) — estado efêmero no Redis, não persistido no Postgres |