Skip to content

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 continua

Escopo: 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: callEndpoint também existe como action de flow (CallEndpointAction), não só como bloco de tool. As duas variantes resolvem o IntegrationServer do 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 → A via chain de publishedToolId.
  • 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) e responses esperadas.

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 publishToolToCache gravavam a chave usando Tool.id. O código atual limpa essa entrada legada automaticamente ao publicar/invalidar.


Segurança e isolamento

  • Blocos executeScript dentro de tools usam o mesmo sandbox V8 Isolate que os scripts de flow — sem acesso a filesystem, network nativo ou módulos Node.

  • Blocos httpCall fazem fetch no mesmo processo, sujeitos ao timeout do bloco.

  • Leitura: a tool enxerga o contexto pai — o executor monta o toolCtx fazendo spread de parentCtx.session.variables sob os inputVariables. Tokens {{...}} de sessão, router e flow resolvem normalmente dentro dela.

  • Escrita: as gravações da tool (toolVars) são locais — não persistem no SessionState do contato. Só o valor do bloco return "escapa", gravado pelo chamador em outputVariable.

    Nota: o endpoint interno POST /internal/tools/execute (usado pelo flow-ai-meta-api em steps tool de WhatsApp Flows) monta um ToolExecutionContext "puro" com variables vazio — 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 = 50 por tool.


Cenários de erro

CenárioComportamento
Tool não encontrada no RedisErro tool não encontrada: publishedToolId=… propagado
Definição JSON inválidaErro tool com definição inválida: publishedToolId=…
Sem onboardingBlockId (não publicada)Erro tool não publicada: publishedToolId=…
currentBlockId aponta para bloco inexistenteErro tool com bloco inválido: blockId=… publishedToolId=…
Bloco com type desconhecidoErro 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 atingidoErro tool timeout (>30s): publishedToolId=… propagado
> MAX_TOOL_HOPS (50) saltosErro tool loop detectado (>50 hops): publishedToolId=…
Ciclo de tools (A → B → A) via callToolErro 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:

EventoPayload
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 com definition (blocos + onboardingBlockId). É o que este guia descreve. Só custom pode ser publicado (PublishedTool).
  • native: função interna do executor, identificada por nativeName (único no workspace, ex.: updateSentiment). Não tem definition, 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 ligar enabledForAI.

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 isso warmupToolCaches (boot do core) filtra por tool.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. AiAgentTool e PublishedTool caí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, o include de tools em populateAiAgentCache precisa do filtro, e excluir/restaurar reemite o cache de todo agente vinculado.
  • Publicar valida a lixeira. Um bloco callTool que 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:

  1. 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.
  2. Recusa com 409 enquanto alguma tool publicada ainda a chamar num bloco callTool, nomeando quais. A referência é um publishedToolId solto 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étodoRotaDescrição
GET/tools/nativeLista tools nativas (kind="native")
GET/toolsLista tools custom com status, métricas e flags de IA. ?includeDeleted=true inclui a lixeira
POST/toolsCria tool vazia
GET/tools/:idRetorna tool + publishedTool (404 se estiver na lixeira)
PATCH/tools/:idSalva rascunho (name, description, definition)
DELETE/tools/:idMove a tool para a lixeira (soft delete)
POST/tools/:id/restoreRestaura uma tool da lixeira
DELETE/tools/:id/permanentExclui em definitivo (só o que já está na lixeira)
POST/tools/:id/publishPublica versão atual (valida ciclos, callTool, callAI)
POST/tools/test-flowExecuta o fluxo da tool a partir de um bloco, devolve o trace
POST/tools/test-blockExecuta um único bloco isolado (painel de teste do builder)
GET/integration-serversLista servidores com endpoints e métricas (usedBy)
POST/integration-serversCria servidor
PATCH/integration-servers/:idAtualiza servidor
DELETE/integration-servers/:idRemove servidor
GET/integration-servers/:id/endpointsLista endpoints
POST/integration-servers/:id/endpointsCria endpoint manual
PATCH/integration-servers/:id/endpoints/:epIdAtualiza endpoint
DELETE/integration-servers/:id/endpoints/:epIdRemove endpoint
POST/integration-servers/:id/endpoints/:epId/testExecuta endpoint salvo contra a API real
POST/integration-servers/:id/endpoints/testExecuta endpoint ad-hoc contra a API real
POST/integration-servers/:id/import-openapiImporta spec → cria endpoints em batch

Nota: existe ainda POST /internal/tools/execute — endpoint interno servidor-a-servidor (usado pelo flow-ai-meta-api em steps tool de WhatsApp Flows). Fica fora do prefixo público (/internal/*) e exige x-internal-secret.


Arquivos relevantes

ArquivoPapel
packages/flow-ai-types/src/tools.tsToolBlock, ToolDefinition, ToolInput
packages/flow-ai-types/src/flow-definition.tsExecuteToolAction, CallEndpointAction, CallEndpointParamMapping
packages/flow-ai-types/src/integration.tsIntegrationServerCacheConfig, IntegrationHeader
packages/flow-ai-types/src/stream-events.tsToolExecutionStreamEvent
packages/flow-ai-types/src/debug.tsToolBlockDebugEvent, ToolDebugCallback
packages/flow-ai-database/prisma/schema.prismaModels Tool, PublishedTool, IntegrationServer, IntegrationEndpoint, ToolExecutionLog
packages/flow-ai-runtime/src/tool-executor.tsExecutor principal da tool (executeTool)
services/flow-ai-engine/src/runtime/actions.tsCases executeTool e callEndpoint + emissão de métrica
services/flow-ai-core/src/http/routes/tools.routes.tsCRUD + publish de tools
services/flow-ai-core/src/http/routes/integration-servers.routes.tsServidores e endpoints
services/flow-ai-core/src/http/routes/internal-tools.routes.tsEndpoint interno POST /internal/tools/execute
services/flow-ai-core/src/services/tool.service.tsPublica/invalida/warm-up do cache tool:{publishedToolId}
services/flow-ai-core/src/consumers/tool-executions.consumer.tsPersiste stream:tool-executions em tool_execution_log
apps/flow-ai-core-ui/app/(studio)/router/[id]/ferramentas/[toolId]/UI do builder de tools

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