Appearance
Ferramentas (Tools)
Uma Tool é um mini-flow reutilizável de transformação de dados. É composta por blocos sequenciais — httpCall, executeScript, callEndpoint, callTool, fetchMedia, callAI e return — com condições de navegação entre eles, exatamente como os blocos de um flow normal, mas sem blocos de interação com o usuário (sem mensagens, sem inputs de resposta).
O resultado final de uma Tool é sempre um valor escalar (string), produzido pelo bloco return e gravado numa variável do flow que a chamou.
As primitivas de execução de Tool vivem no pacote compartilhado flow-ai-runtime e são usadas tanto pelo flow-ai-engine (action executeTool no flow) quanto pelo flow-ai-agent (tools expostas a agentes de IA — ver Native vs custom).
Visão geral
Flow
│ action executeTool { toolId, publishedToolId, inputVariables, outputVariable }
▼
flow-ai-engine/src/runtime/actions.ts (case "executeTool")
│ interpola inputVariables com ctx
│ chama executeTool(publishedToolId, inputs, parentCtx, toolId, onBlock, …)
▼
flow-ai-runtime/src/tool-executor.ts (executeTool)
│ carrega PublishedTool do Redis (tool:{publishedToolId})
│ monta toolCtx = { ...ctx, variables: { ...ctx.variables, ...inputs } }
│ percorre blocos a partir de onboardingBlockId
│ avalia outputConditions → pickToolTarget → navega
│ executa httpCall / executeScript / callEndpoint / callTool / fetchMedia / callAI
│ ao atingir bloco return → interpola value → retorna string
▼
flow-ai-engine/src/runtime/actions.ts
│ grava resultado em ctx.session.variables[outputVariable]
│ emite ToolExecutionStreamEvent em stream:tool-executions
▼
Flow continuaEscopo: global (workspace). O model Tool não tem routerId — uma Tool não pertence a um Router e pode ser referenciada por qualquer flow. No flow-ai-core-ui, porém, o builder de tools é acessado por uma rota sob o router (app/(studio)/router/[id]/…/ferramentas/).
Ciclo de vida: rascunho → publicada. O engine só executa versões publicadas (PublishedTool). Tools não publicadas geram erro em runtime (tool não publicada). O flow-ai-core mantém o cache Redis sincronizado a cada publicação/remoção e o repopula no boot via warmupToolCaches.
Tipos TypeScript relevantes
Definidos em packages/flow-ai-types/src/tools.ts. Todo bloco que não seja return carrega outputConditions: OutputCondition[] + defaultOutput: BlockTarget (mesmo shape de navegação dos blocos de flow) e um title? opcional para o canvas do builder.
ts
// Bloco de tool — discriminated union (packages/flow-ai-types/src/tools.ts)
type ToolBlock =
| {
type: "httpCall"
id: string
title?: string
url: string
method: HttpMethod // enum HTTP_METHODS, não string livre
headers?: Record<string, string>
body?: string
responseStatusVariable?: string
responseBodyVariable?: string
timeoutMs?: number
onError: "blocking" | "continue"
errorVariable?: string
outputConditions: OutputCondition[]
defaultOutput: BlockTarget
}
| {
type: "executeScript"
id: string
title?: string
source: string
inputVariables: Array<{ name: string; value: string }>
outputVariable: string
onError: "blocking" | "continue"
errorVariable?: string
outputConditions: OutputCondition[]
defaultOutput: BlockTarget
}
| {
type: "callEndpoint" // chama um IntegrationEndpoint cadastrado
id: string
title?: string
serverId: string // IntegrationServer no Postgres
endpointId: string // IntegrationEndpoint no Postgres
paramMappings: Array<CallEndpointParamMapping>
responseBodyVariable?: string
responseStatusVariable?: string
onError: "blocking" | "continue"
errorVariable?: string
outputConditions: OutputCondition[]
defaultOutput: BlockTarget
}
| {
type: "callTool" // chama outra Tool publicada (aninhamento)
id: string
title?: string
toolId: string // Tool no Postgres (UI)
publishedToolId: string // PublishedTool lido do Redis (tool:{id})
inputVariables: Array<{ name: string; value: string }>
outputVariable: string
onError: "blocking" | "continue"
errorVariable?: string
outputConditions: OutputCondition[]
defaultOutput: BlockTarget
}
| {
type: "fetchMedia" // baixa mídia do WhatsApp (mediaId) ou de uma URL
id: string
title?: string
mode: "whatsapp" | "url"
mediaIdVariable: string
urlVariable: string
authHeader?: string
base64Variable: string // saída: conteúdo base64
mimeTypeVariable: string // saída: MIME detectado
onError: "blocking" | "continue"
errorVariable?: string
outputConditions: OutputCondition[]
defaultOutput: BlockTarget
}
| ({
type: "callAI" // usa uma LlmCredential (transcrição, visão, doc, texto)
id: string
title?: string
llmCredentialId: string
outputVariable: string
outputFormat: "text" | "json"
maxTokens?: number
onError: "blocking" | "continue"
errorVariable?: string
outputConditions: OutputCondition[]
defaultOutput: BlockTarget
} & (
| { capability: "transcribeAudio"; inputVariable: string; mimeTypeVariable: string; language?: string }
| { capability: "analyzeImage"; inputVariable: string; mimeTypeVariable: string; model: string; prompt?: string }
| { capability: "analyzeDocument"; inputVariable: string; mimeTypeVariable: string; model: string; prompt: string; maxPages: number }
| { capability: "generateText"; model: string; systemPrompt?: string; userPrompt: string }
))
| {
type: "return"
id: string
title?: string
value: string // string interpolável — sem outputConditions
}
type ToolDefinition = {
blocks: Record<string, ToolBlock>
onboardingBlockId: string
inputs?: ToolInput[] // parâmetros de entrada declarados (auto-documentam a tool)
meta?: ToolDefinitionMeta // posições no canvas — ignorado pelo runtime
enabledForAI?: boolean // habilita como ferramenta interna de agentes LLM
slug?: string // nome técnico p/ function calling (^[a-zA-Z0-9_-]{1,64}$)
description?: string // descrição enviada ao LLM no function spec
returnDescription?: string // descrição do retorno, concatenada à description
}
// Action no flow que referencia uma tool (packages/flow-ai-types/src/flow-definition.ts)
type ExecuteToolAction = ActionBase & {
type: "executeTool"
toolId: string // ID do Tool no Postgres (para UI)
publishedToolId: string // ID do PublishedTool (lido do Redis pelo engine)
inputVariables: Array<{ name: string; value: string }>
outputVariable: string
onError: "blocking" | "continue"
errorVariable?: string
errorMessage?: string // mensagem interpolável enviada ao contato quando onError=blocking
errorHandoff?: boolean // blocking: true/ausente = handoff; false = recupera no defaultException
// ActionBase inclui `id` e `conditions?: Condition[]`
}Nota:
callEndpointtambém existe como action de flow (CallEndpointAction), não só como bloco de tool. As duas variantes resolvem oIntegrationServerdo mesmo cache Redis.
Blocos de Tool
Cada bloco (exceto return) navega para o próximo via outputConditions / defaultOutput avaliadas por pickToolTarget. Se o alvo não for do tipo block (ex.: endAttendance), o loop encerra e a tool retorna string vazia — só o bloco return produz um valor não-vazio.
httpCall
Fetch nativo no mesmo processo do runtime. Suporta URL, headers e body interpoláveis com as variáveis da tool, além de responseStatusVariable, responseBodyVariable, timeoutMs (default 10s, limitado ao deadline restante), onError e errorVariable.
executeScript
Executa JavaScript em sandbox V8 Isolate (executeInSandbox). inputVariables e outputVariable são variáveis locais da tool.
callEndpoint
Chama um IntegrationEndpoint cadastrado. Em runtime, o executor carrega o IntegrationServer do cache Redis integration:server:{serverId} (headers já decifrados), monta a URL a partir de baseUrl + path + paramMappings (route/query/body), constrói o body via buildEndpointBody e faz o fetch. onError/errorVariable/response variables funcionam como no httpCall.
callTool
Chama outra Tool publicada (aninhamento). O resultado da tool-filha é gravado em outputVariable. Guardrails de aninhamento aplicados por invocação:
MAX_TOOL_DEPTH = 5— profundidade máxima de tools aninhadas.- Detecção de ciclo
A → B → AviachaindepublishedToolId. MAX_TOOL_INVOCATIONS = 50— backstop de fan-out por execução de topo.- O deadline de 30s é compartilhado por toda a árvore de tools aninhadas.
fetchMedia
Baixa mídia via executeFetchMedia: modo whatsapp resolve o mediaId pela Graph API, modo url faz GET direto (com authHeader opcional). Grava conteúdo em base64Variable e o MIME em mimeTypeVariable — normalmente consumidos por um bloco callAI seguinte.
callAI
Usa uma LlmCredential (executeCallAI) para uma de quatro capacidades: transcribeAudio (Whisper), analyzeImage (visão), analyzeDocument (PDF) e generateText. outputFormat: "json" injeta response_format: json_object na chamada OpenAI. Emite uso em stream:llm-usage (rastreado por toolId).
return (terminador)
Único bloco sem outputConditions. O campo value (string interpolável) define o valor que será retornado ao flow chamador e gravado em outputVariable. A execução da tool para imediatamente ao atingir esse bloco.
Servidores e Endpoints de Integração
IntegrationServer e IntegrationEndpoint são chamados em runtime pelo bloco callEndpoint (e pela action callEndpoint do flow). Não são só pré-preenchimento de editor: o executor resolve o servidor do cache Redis integration:server:{serverId} a cada chamada e monta URL/headers/body a partir dele.
- IntegrationServer:
baseUrl+ headers globais (ex:Authorization). - IntegrationEndpoint:
path, método HTTP, headers adicionais,bodyTemplate(JSON estático),params(route/query/body declarados) eresponsesesperadas.
Headers marcados como secret são cifrados com AES-256-CBC (formato iv_hex:cipher_hex) antes de persistir e nunca voltam em GETs. O flow-ai-core é a única fronteira de cripto: ele decifra e escreve os headers em texto puro no cache integration:server:{serverId} (IntegrationServerCacheConfig), lido pelo engine/runtime.
O wizard de importação OpenAPI (POST /integration-servers/:id/import-openapi) parseia uma especificação e cria múltiplos IntegrationEndpoint em batch. O schema da spec não é persistido — apenas nome, path e método de cada operação.
Cache Redis
Ferramentas publicadas são armazenadas com o prefixo tool: seguido do id do PublishedTool — a mesma id que ExecuteToolAction.publishedToolId carrega e que o executor passa a getTool(). A chave é, portanto, tool:{publishedToolId} (sem TTL). O flow-ai-core grava via setTool(publishedTool.id, …), invalida ao re-publicar/deletar (invalidateToolCache) e repopula tudo no boot (warmupToolCaches). O runtime nunca consulta o Postgres durante a execução de uma tool — lê apenas do Redis.
Servidores de integração usados por blocos/actions callEndpoint ficam em integration:server:{serverId} (headers já decifrados), sincronizado a cada mutação de servidor/endpoint pelo core.
Nota: versões antigas do
publishToolToCachegravavam a chave usandoTool.id. O código atual limpa essa entrada legada automaticamente ao publicar/invalidar.
Segurança e isolamento
Blocos
executeScriptdentro de tools usam o mesmo sandbox V8 Isolate que os scripts de flow — sem acesso a filesystem, network nativo ou módulos Node.Blocos
httpCallfazem fetch no mesmo processo, sujeitos ao timeout do bloco.Leitura: a tool enxerga o contexto pai — o executor monta o
toolCtxfazendo spread deparentCtx.session.variablessob osinputVariables. Tokens{{...}}de sessão, router e flow resolvem normalmente dentro dela.Escrita: as gravações da tool (
toolVars) são locais — não persistem noSessionStatedo contato. Só o valor do blocoreturn"escapa", gravado pelo chamador emoutputVariable.Nota: o endpoint interno
POST /internal/tools/execute(usado peloflow-ai-meta-apiem stepstoolde WhatsApp Flows) monta umToolExecutionContext"puro" comvariablesvazio — justamente porque não há sessão/contato reais nesse caminho.Timeout total da execução da tool: 30s (
TOOL_TIMEOUT_MS). Backstop de navegação:MAX_TOOL_HOPS = 50por tool.
Cenários de erro
| Cenário | Comportamento |
|---|---|
| Tool não encontrada no Redis | Erro tool não encontrada: publishedToolId=… propagado |
| Definição JSON inválida | Erro tool com definição inválida: publishedToolId=… |
Sem onboardingBlockId (não publicada) | Erro tool não publicada: publishedToolId=… |
currentBlockId aponta para bloco inexistente | Erro tool com bloco inválido: blockId=… publishedToolId=… |
Bloco com type desconhecido | Erro bloco com tipo desconhecido na tool: publishedToolId=… |
Bloco httpCall/callEndpoint/callTool falha + onError: "blocking" | Erro propaga para a action executeTool, que aplica sua própria política onError/errorHandoff |
Bloco falha + onError: "continue" | responseBodyVariable/outputVariable fica vazio, errorVariable recebe a mensagem, execução segue no próximo bloco |
| Timeout de 30s atingido | Erro tool timeout (>30s): publishedToolId=… propagado |
> MAX_TOOL_HOPS (50) saltos | Erro tool loop detectado (>50 hops): publishedToolId=… |
Ciclo de tools (A → B → A) via callTool | Erro ciclo de tools detectado: … |
Profundidade > MAX_TOOL_DEPTH (5) | Erro profundidade máxima de tools atingida (>5) |
Bloco return nunca atingido (navegação sai da árvore) | Não é erro — o loop encerra e a tool retorna string vazia ("") |
Eventos de debug
Quando debug está ativo para um contato, a execução de executeTool emite:
| Evento | Payload |
|---|---|
action_start | { toolId, publishedToolId, inputs } — inputs interpolados |
action_result (por bloco) | actionType: "executeTool:{blockType}" com o ToolBlockDebugEvent (blockId, url/method/status, result/error, matchedConditionId, depth, toolId) |
action_result (final) | { inputs, result } no sucesso, ou { inputs, error } na falha |
Os eventos internos de cada bloco são propagados ao canal de debug do contato: o executor recebe um ToolDebugCallback (onBlock) e a actions.ts reemite cada evento como action_result com actionType prefixado por executeTool:. Eventos de tools aninhadas (bloco callTool) chegam com depth incrementado e o toolId da filha.
Métricas — stream:tool-executions
Após cada executeTool (sucesso ou falha), o engine publica um ToolExecutionStreamEvent ({ toolId, publishedToolId, durationMs, success, occurredAt }) em stream:tool-executions. Cada tool aninhada (bloco callTool) gera uma linha própria via o reporter onToolExecution. Falhas de publicação são logadas mas nunca interrompem a action.
O flow-ai-core consome no grupo core-tool-executions e persiste em tool_execution_log (sem FK para Tool — auditoria sobrevive à deleção da tool). A partir desse log, o Studio calcula runs7d, avgMs e successRate em janela móvel de 7 dias.
Native vs custom
O model Tool tem um campo kind:
custom(default): mini-flow comdefinition(blocos +onboardingBlockId). É o que este guia descreve. Sócustompode ser publicado (PublishedTool).native: função interna do executor, identificada pornativeName(único no workspace, ex.:updateSentiment). Não temdefinition, não é publicável e não passa pelo mini-flow.
Uma ToolDefinition (tool custom) pode ainda ser exposta a agentes de IA via enabledForAI: true + slug + description/returnDescription, virando uma função de tool calling. Tools nativas são associadas a agentes por AiAgentTool. O detalhamento do loop de tool-calling dos agentes fica no guia de agentes de IA — aqui basta saber que a ToolDefinition e o campo Tool.kind são o ponto de integração.
Descrição: dois campos, um fallback
Existem duas descrições e elas não são a mesma coisa:
Tool.description(coluna) — a descrição geral, editada no topo do builder, ao lado do nome. É a que aparece na lista de ferramentas.ToolDefinition.description(dentro do JSON) — editada no painel "Configuração para agentes", só visível depois de ligarenabledForAI.
No function spec enviado ao LLM vale definition.description quando preenchida e Tool.description caso contrário. Ou seja: preencher só a descrição geral já basta para o agente; o campo do painel de IA existe para quando o texto que orienta o LLM precisa ser diferente do que a equipe lê na lista.
returnDescription é concatenada à descrição escolhida, e cada ToolInput.description vira a descrição do parâmetro no schema da função.
Lixeira (soft delete)
DELETE /tools/:id não apaga a linha: grava Tool.deletedAt e remove a chave tool:{publishedToolId} do Redis. POST /tools/:id/restore desfaz as duas coisas.
O que sustenta isso:
- O Redis é o enforcement, não a coluna. O engine executa a partir de
tool:{publishedToolId}e nunca lê o Postgres. Por issowarmupToolCaches(boot do core) filtra portool.deletedAt: null— sem esse filtro o próximo restart devolveria ao ar uma ferramenta que já não aparece em lugar nenhum. - Os vínculos sobrevivem.
AiAgentToolePublishedToolcaíam por cascade no delete físico; com soft delete eles ficam, e é isso que faz a restauração devolver a ferramenta aos agentes que já a usavam. Em troca, oincludede tools empopulateAiAgentCacheprecisa do filtro, e excluir/restaurar reemite o cache de todo agente vinculado. - Publicar valida a lixeira. Um bloco
callToolque aponte para uma ferramenta excluída barra o publish, em vez de adiar o erro para a execução.
Exclusão definitiva
DELETE /tools/:id/permanent apaga a linha de verdade, e o cascade leva junto PublishedTool e os vínculos em AiAgentTool. ToolExecutionLog sobrevive de propósito — não tem FK, e o histórico de execução é auditoria.
Duas travas, e elas são o motivo de a rota ser separada do DELETE /tools/:id:
- Só apaga o que já está na lixeira (400 caso contrário). Não existe caminho de um clique só para apagar de vez — passar pela lixeira é obrigatório.
- Recusa com 409 enquanto alguma tool publicada ainda a chamar num bloco
callTool, nomeando quais. A referência é umpublishedToolIdsolto dentro do JSON, sem FK: apagar assim mesmo deixaria a chamadora quebrada só na hora da execução, na conversa do contato. É a mesma validação que o publish já faz, aplicada na origem. Chamadoras que estão na lixeira são ignoradas, senão dois descartes se travariam mutuamente.
Os vínculos com agentes são lidos antes do delete — depois do cascade não haveria como saber quais caches reemitir.
Rotas HTTP (flow-ai-core)
Prefixadas por ${FLOW_AI_CORE_BASE_PATH} (ex.: /api).
| Método | Rota | Descrição |
|---|---|---|
| GET | /tools/native | Lista tools nativas (kind="native") |
| GET | /tools | Lista tools custom com status, métricas e flags de IA. ?includeDeleted=true inclui a lixeira |
| POST | /tools | Cria tool vazia |
| GET | /tools/:id | Retorna tool + publishedTool (404 se estiver na lixeira) |
| PATCH | /tools/:id | Salva rascunho (name, description, definition) |
| DELETE | /tools/:id | Move a tool para a lixeira (soft delete) |
| POST | /tools/:id/restore | Restaura uma tool da lixeira |
| DELETE | /tools/:id/permanent | Exclui em definitivo (só o que já está na lixeira) |
| POST | /tools/:id/publish | Publica versão atual (valida ciclos, callTool, callAI) |
| POST | /tools/test-flow | Executa o fluxo da tool a partir de um bloco, devolve o trace |
| POST | /tools/test-block | Executa um único bloco isolado (painel de teste do builder) |
| GET | /integration-servers | Lista servidores com endpoints e métricas (usedBy) |
| POST | /integration-servers | Cria servidor |
| PATCH | /integration-servers/:id | Atualiza servidor |
| DELETE | /integration-servers/:id | Remove servidor |
| GET | /integration-servers/:id/endpoints | Lista endpoints |
| POST | /integration-servers/:id/endpoints | Cria endpoint manual |
| PATCH | /integration-servers/:id/endpoints/:epId | Atualiza endpoint |
| DELETE | /integration-servers/:id/endpoints/:epId | Remove endpoint |
| POST | /integration-servers/:id/endpoints/:epId/test | Executa endpoint salvo contra a API real |
| POST | /integration-servers/:id/endpoints/test | Executa endpoint ad-hoc contra a API real |
| POST | /integration-servers/:id/import-openapi | Importa spec → cria endpoints em batch |
Nota: existe ainda
POST /internal/tools/execute— endpoint interno servidor-a-servidor (usado peloflow-ai-meta-apiem stepstoolde WhatsApp Flows). Fica fora do prefixo público (/internal/*) e exigex-internal-secret.
Arquivos relevantes
| Arquivo | Papel |
|---|---|
packages/flow-ai-types/src/tools.ts | ToolBlock, ToolDefinition, ToolInput |
packages/flow-ai-types/src/flow-definition.ts | ExecuteToolAction, CallEndpointAction, CallEndpointParamMapping |
packages/flow-ai-types/src/integration.ts | IntegrationServerCacheConfig, IntegrationHeader |
packages/flow-ai-types/src/stream-events.ts | ToolExecutionStreamEvent |
packages/flow-ai-types/src/debug.ts | ToolBlockDebugEvent, ToolDebugCallback |
packages/flow-ai-database/prisma/schema.prisma | Models Tool, PublishedTool, IntegrationServer, IntegrationEndpoint, ToolExecutionLog |
packages/flow-ai-runtime/src/tool-executor.ts | Executor principal da tool (executeTool) |
services/flow-ai-engine/src/runtime/actions.ts | Cases executeTool e callEndpoint + emissão de métrica |
services/flow-ai-core/src/http/routes/tools.routes.ts | CRUD + publish de tools |
services/flow-ai-core/src/http/routes/integration-servers.routes.ts | Servidores e endpoints |
services/flow-ai-core/src/http/routes/internal-tools.routes.ts | Endpoint interno POST /internal/tools/execute |
services/flow-ai-core/src/services/tool.service.ts | Publica/invalida/warm-up do cache tool:{publishedToolId} |
services/flow-ai-core/src/consumers/tool-executions.consumer.ts | Persiste stream:tool-executions em tool_execution_log |
apps/flow-ai-core-ui/app/(studio)/router/[id]/ferramentas/[toolId]/ | UI do builder de tools |