Skip to content

Ação executeScript — documentação completa

Este documento descreve a ação executeScript de ponta a ponta: tipo TypeScript, sandbox de execução, integração no engine, editor no frontend e API de templates.


Visão geral

A ação executeScript permite que o construtor de fluxos escreva código JavaScript que roda durante a execução de um bloco. O resultado do script pode ser salvo em uma variável de contexto da sessão para uso em condições, interpolações ou ações subsequentes.

O código é executado em um V8 Isolate completamente separado do processo do engine, sem acesso a módulos do host, variáveis de ambiente ou qualquer recurso de I/O.

A primitiva de sandbox vive em packages/flow-ai-runtime (pacote compartilhado) e é invocada tanto pelas actions do engine (flow-ai-engine) quanto pelo executor de Tools publicadas (executeTool, mini-flow). Ambos os serviços que dependem do runtime (flow-ai-engine e flow-ai-agent) reutilizam a mesma executeInSandbox.


Tipo TypeScript (flow-ai-types)

ts
type ExecuteScriptAction = ActionBase & {
  type: "executeScript"
  /** Código JS do script — arquivo completo ou apenas o corpo de `execute`. */
  source: string
  /** Mapeamentos nome→valor: `name` vira parâmetro da função; `value` é
      interpolado ({{...}}) antes de entrar no sandbox. */
  inputVariables: Array<{ name: string; value: string }>
  /** Variável de sessão onde o resultado de sucesso é gravado. */
  outputVariable: string
  /** Variável de sessão onde a mensagem de erro é gravada (apenas onError:"continue"). */
  errorVariable?: string
  /** "blocking" → congela a sessão / recupera via defaultException.
      "continue" → registra o erro e segue o fluxo. */
  onError: "blocking" | "continue"
  /** Mensagem interpolável enviada ao contato quando onError=blocking. Sem valor usa a default. */
  errorMessage?: string
  /** Em onError=blocking: true/ausente faz handoff para humano;
      false envia errorMessage e recupera no defaultExceptionBlockId. */
  errorHandoff?: boolean
}

Schema Zod (flow-schema.ts)

ts
const ExecuteScriptActionSchema = ActionBaseSchema.extend({
  type: z.literal("executeScript"),
  source: z.string().min(1),
  inputVariables: z.array(z.object({ name: z.string().min(1), value: z.string() })),
  outputVariable: z.string().min(1),
  errorVariable: z.string().optional(),
  onError: z.enum(["blocking", "continue"]),
})

Nota: o schema Zod valida apenas os campos acima. errorMessage e errorHandoff existem no tipo TypeScript (flow-definition.ts) mas não fazem parte do ExecuteScriptActionSchema (flow-schema.ts).


Sandbox de execução (isolated-vm)

Por que isolated-vm e não vm ou worker_threads + vm

AbordagemEscape possívelMotivo
vm.runInNewContext()Simvm não é uma barreira de segurança (doc oficial do Node). ({}).constructor.constructor('return process')() funciona dentro do contexto.
worker_threads + vmSimO worker vive no mesmo processo do engine, com process.env contendo credenciais Redis/Postgres. Escape do vm chega ao worker.
isolated-vmNãoV8 Isolate com heap completamente separado. Sem ponteiros para o heap do host. Sem caminho de acesso a require, process ou qualquer global do Node.

Arquivo: packages/flow-ai-runtime/src/sandbox.ts

Exportado como executeInSandbox em packages/flow-ai-runtime/src/index.ts.

executeInSandbox(
  source: string,
  inputs: Record<string, string>,
  timeoutMs?: number   // padrão: SANDBOX_TIMEOUT_MS (default 5000ms)
): Promise<string>

Globals disponíveis no contexto do isolate (built-ins do próprio V8 — o sandbox não injeta nada além de expor globalThis):

Math        — todos os métodos (abs, floor, ceil, round, max, min, pow, sqrt, random, PI...)
JSON        — parse, stringify
Date        — now, new Date()
parseInt / parseFloat / isNaN / isFinite
Object      — keys, values, entries, assign, fromEntries
Array       — isArray
String / Number / Boolean / RegExp

Ausentes (sem acesso): require, process, global, fetch, Buffer, console, __dirname, setTimeout, setInterval — nenhum global do host Node é exposto ao isolate.

Semáforo de concorrência: contador activeSandboxes limitado a MAX_PARALLEL (configurável via SANDBOX_MAX_PARALLEL, default 8). Exceder o limite lança imediatamente Error: Limite de {MAX_PARALLEL} scripts simultâneos atingido. Tente novamente em instantes.

Timeout: configurável via SANDBOX_TIMEOUT_MS (default 5000ms).

Limite de memória: SANDBOX_MEMORY_MB fixo em 32MB por isolate.

Assíncrono não é suportado: não há APIs assíncronas no isolate, e uma executeasync (ou que devolva uma Promise) falha com Script assíncrono não é suportado em vez de gravar {} em outputVariable.


Formato do source: arquivo completo ou corpo (#461)

Dois formatos convivem, sem flag no schema e sem migração de flow publicado:

FormatoComo é escritoOnde aparece
arquivo completoo autor declara function execute(...) e pode declarar funções auxiliares, constantes e classes fora delaformato oferecido pelo editor desde #461
corpo (legado)apenas o corpo de execute — o editor impunha a assinatura como cabeçalho fixo, não editáveltodo script salvo antes de #461

O montador do programa (packages/flow-ai-runtime/src/sandbox-program.ts, exportado como buildSandboxProgram) serve os dois com o mesmo texto, e não decide por inspeção do código — regex em código de usuário erra (function execute dentro de comentário, de string, de um helper aninhado). Quem decide é o escopo do JS:

  1. O source entra no corpo de uma execute gerada, cujos parâmetros são as inputVariables. Um source no formato corpo funciona porque é literalmente o que sempre foi — inclusive return no topo e recursão chamando execute(...), que resolve para a própria função gerada.
  2. Um source no formato arquivo completo declara a sua execute. A declaração é hasteada no escopo do corpo da função gerada e sombreia o nome da gerada ali dentro.
  3. A cauda apendada roda só quando o source não retornou nada — ou seja, no formato arquivo completo, onde só houve declarações. Ela compara execute com a referência da gerada: se são diferentes, a do autor sombreou e é ela que deve ser chamada; se são iguais, ninguém declarou nada e não há o que chamar (era um corpo que caiu fora sem return, caso que sempre gravou "").

Consequências práticas:

  • As inputVariables ficam visíveis por nome em todo o arquivo — inclusive dentro de funções auxiliares e mesmo que a execute do autor não as declare como parâmetro. É o que mantém verdadeiro o declare const <input>: string que o editor injeta no Monaco.
  • Os argumentos são passados por posição, na ordem das inputVariables.
  • Um const/let no topo do arquivo com o mesmo nome de uma inputVariable é SyntaxError de redeclaração de parâmetro (já era assim no formato corpo).

O "Testar" do builder de tools (services/flow-ai-core/src/services/tool-block-tester.ts) usa o mesmo buildSandboxProgram — só o motor difere (node:vm no core, V8 Isolate no engine), para que o teste aceite exatamente o que a produção aceita.


Tratamento de erros

Quando a action roda como parte de um flow (engine), onError tem dois caminhos. Em blocking, o comportamento padrão depende de errorHandoff:

  • blocking (default — errorHandoff ausente ou true): lança RuntimeUserError, congela a sessão e faz handoff para atendimento humano (a errorMessage custom, se houver, acompanha o freeze).
  • blocking + errorHandoff: false: envia a errorMessage (ou a default) ao contato e redireciona para o defaultExceptionBlockId do flow; a sessão permanece em mode: "flow".
  • continue: grava outputVariable = "" e, se definido, errorVariable = mensagem; o fluxo segue normalmente.

Em ambos os caminhos de blocking, se errorVariable estiver definido a mensagem também é gravada nele (para que {{errorVariable}} resolva).

SituaçãoonError: "blocking"onError: "continue"
Script lança exceçãoFreeze + handoff (default) / redireciona para defaultExceptionBlockId se errorHandoff:falseoutputVariable = "", errorVariable = mensagem
Timeout (loop infinito)idem acima (mensagem do isolate: Script execution timed out)outputVariable = "", errorVariable = mensagem do timeout
Limite de scripts simultâneos atingidoidem acimaoutputVariable = "", errorVariable = mensagem de limite
Memória excedida (>32MB)idem acimaoutputVariable = "", errorVariable = mensagem do isolate
SucessooutputVariable = resultadooutputVariable = resultado

Nota: quando o executeScript roda dentro de uma Tool publicada (mini-flow, via executeTool), o caminho blocking é mais simples — o erro é apenas relançado (throw) e propaga para quem chamou a Tool; não há freeze/handoff nesse nível. Em continue, grava errorVariable/outputVariable nas variáveis da Tool.


Templates de script

Templates são globais da plataforma — não têm workspaceId.

Rotas backend (flow-ai-core)

Todas as rotas exigem autenticação (authenticate). O prefixo global FLOW_AI_CORE_BASE_PATH (default /api) é aplicado no boot, então os paths efetivos são /api/script-templates.

MétodoPathComportamento
GET/api/script-templatesLista todos os templates ordenados por createdAt desc.
POST/api/script-templatesCria template. Body: { name, description?, source }. Responde 201.
DELETE/api/script-templates/:idRemove template. Responde 204.

Guia de sintaxe para o usuário

O sandbox chama execute(...). Escreva o arquivo inteiro: declare function execute(...) e o que mais precisar fora dela. Cada item de inputVariables vira um argumento posicional (o name é o nome no escopo); seu value é interpolado ({{...}}) e injetado como string. Use return para produzir o valor salvo em outputVariable.

js
// inputVariables: [{ name: 'cpf', value: '{{cpf}}' }]
function execute(cpf) {
  const digits = digitos(cpf)
  return valido(digits) ? 'valido' : 'invalido'
}

function digitos(valor) {
  return String(valor).replace(/\D/g, '')
}

function valido(digits) {
  return digits.length === 11
}
js
// inputVariables: [{ name: 'nome', value: '{{nome}}' }]
function execute(nome) {
  return nome.trim().split(' ').map(capitaliza).join(' ')
}

function capitaliza(parte) {
  return parte[0].toUpperCase() + parte.slice(1)
}

Objetos e arrays retornados são serializados com JSON.stringify. Scripts que não fazem return (ou retornam undefined/null) gravam "" em outputVariable.

Formato antigo (só o corpo) continua valendo

Scripts salvos antes de #461 são apenas o corpo de execute e rodam sem alteração — não há migração a fazer. O editor identifica o formato e oferece um botão para envolver o código em function execute(...) quando você quiser passar a declarar funções auxiliares.

js
// inputVariables: [{ name: 'vencimento', value: '{{vencimento}}' }]
const diasRestantes = Math.ceil((new Date(vencimento) - Date.now()) / 86400000)
return String(diasRestantes)

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