Skip to content

Robôs de Automação ​

Nível de acesso requerido

Admin ou Super Admin

O que é um robô? ​

Um robô é um programa automatizado que executa tarefas sem intervenção humana. Robôs podem:

  • Iniciar processos automaticamente em horários agendados
  • Buscar dados de sistemas externos e alimentar o Flowi Agentic
  • Executar etapas que não dependem de uma pessoa
  • Integrar com ERPs, sistemas fiscais e outras plataformas

Cada robô é escrito em Java ou em Python — o runtime, escolhido no cadastro. Um robô Java é um arquivo JAR compilado; um robô Python é um pacote .zip com o código e um manifesto. Os dois rodam da mesma forma: num runner, sob demanda ou por agendamento CRON, com os mesmos parâmetros, segredos, estado, log e limites.

Atenção: Robôs ≠ Agentes

Os "Robôs" tratam de integração sistêmica rígida (código). Se o que você precisa é de uma automação cognitiva (ler PDFs, interpretar formulários livres ou decidir caminhos flexíveis com base em IA), acesse o manual de Agentes de IA.

Listando robôs ​

Acesse Integrações e automação → Robôs na barra lateral.

A lista responde de relance a pergunta que importa — algo quebrou? — com uma linha por robô:

ColunaO que mostra
Nomenome e a chave do robô, o runtime (Java ou Python) e a etiqueta Global quando o robô é da plataforma
Situaçãoum interruptor de ativo/inativo — veja Ativando e desativando
Agendamentoa expressão cron e a próxima execução, ou sob demanda quando não há
Última execuçãoo resultado e quando foi
Artefatoa versão ativa do JAR ou do pacote, ou o aviso de que falta enviar

Robô que nunca rodou aparece como Nunca executou, e não em branco: ausência de resultado e resultado bom não devem ter a mesma aparência.

Clicar na linha abre a página do robô.

Cadastrando um novo robô ​

  1. Clique em Novo robô
  2. Preencha:
    • Chave — identificador do robô, só letras minúsculas, números e hífen (ex.: sincroniza-cadastros). runtimes e sdk são reservadas, porque já são endereços da tela de robôs; um robô cadastrado com a chave runtimes antes desta versão deixa de abrir pela página de detalhe (a lista e as outras rotas continuam) e precisa ser recriado com outra chave
    • Nome — identificação do robô
    • Runtime — Java ou Python. Fica fixo depois do envio do primeiro artefato: para trocar de linguagem, crie outro robô. Quando a instalação ainda não roda Python, a opção aparece desabilitada e a tela diz qual configuração o administrador da plataforma precisa ligar
    • Classe principal (Java) — a classe do JAR que implementa FlowIaRobot, ex.: com.exemplo.MeuRobo; ou Ponto de entrada (Python) — modulo:funcao, ex.: invoice_sync.main:run, igual ao entryPoint do manifesto
    • Agendamento (cron, opcional) — expressão cron de seis campos (ex.: 0 0 8 * * MON = toda segunda às 8h)
    • Parâmetros — configurações opcionais em formato JSON (a coluna é jsonb: um objeto malformado é recusado ao salvar, e não na primeira execução agendada)
    • O que o robô pode acessar — as capacidades, cada uma com o nome do que ela libera
  3. Clique em Criar

A tela abre a página do robô recém-criado com o aviso do próximo passo: enviar o JAR (ou o pacote, num robô Python). O arquivo não vai no formulário de cadastro porque é versionado por robô — ele é enviado em Upload JAR (.jar) ou Enviar pacote (.zip), na página do robô, e só então o robô tem o que executar.

O arquivo tem um limite de 200 MB por padrão (ROBOTS_MAX_ARTIFACT_SIZE_MB); esse limite vale só para o envio do artefato do robô, e o resto da plataforma continua em 10 MB (UPLOAD_MAX_FILE_SIZE). O arquivo é lido em fluxo, sem ir inteiro para a memória, e acima do limite o envio é recusado com 413, com o tamanho e o limite na mensagem — a tela diz o teto da instalação. Com envios demais ao mesmo tempo na instalação (ROBOTS_MAX_CONCURRENT_UPLOADS, 4 por padrão) a resposta é 429 e a tela pede para tentar de novo em um minuto. O proxy na frente da plataforma precisa aceitar o mesmo tamanho. A plataforma guarda o JAR com um nome saneado (só letras, números, ., _ e -), então o nome do arquivo no seu computador pode ser qualquer um.

Pacote Python

Um robô Python recebe um pacote: um .zip com o flowia-robot.json na raiz, o código e, se houver dependências, as rodas (.whl) em wheels/ — veja Desenvolvimento do pacote Python. O ponto de entrada do robô precisa ser igual ao entryPoint do manifesto. Um JAR enviado a um robô Python, ou um pacote enviado a um robô Java, é recusado pelo conteúdo, com a mensagem dizendo com o que o arquivo se parece. A plataforma não executa o pacote para conferi-lo: ela lê o diretório do ZIP e recusa, dizendo a entrada e o que fazer, nomes que saem da pasta (.., caminho absoluto, barra invertida), links simbólicos, arquivos compactados fora de wheels/, qualquer arquivo em wheels/ que não seja .whl (um .tar.gz executaria código ao instalar), manifesto ausente ou inválido, ponto de entrada que não existe no pacote e pacotes que parecem bomba de descompressão. Esta instalação só aceita pacotes Python com ROBOTS_PYTHON_ENABLED=true, o que só deve ser ligado quando os runners tiverem Python.

Expressão cron — seis campos

Formato: segundo minuto hora dia-do-mês mês dia-da-semana — é o formato que o agendador da plataforma lê, com o segundo na frente.
Exemplos:

  • 0 0 8 * * * — todo dia às 8h
  • 0 0 8 * * MON — toda segunda às 8h
  • 0 */30 * * * * — a cada 30 minutos

Uma expressão inválida é recusada ao salvar, com a mensagem dizendo o que está errado. A de cinco campos (0 8 * * MON, o formato do crontab do Linux) também é recusada, e a mensagem já sugere a versão com o segundo na frente. Com o cron aceito e o robô ativo, a lista e a página do robô mostram quando ele roda da próxima vez.

Um robô cadastrado antes dessa regra pode ter guardado um cron que o agendador não lê mais. Ligado, ele aparece na lista e na página com o aviso Este agendamento não roda mais no lugar da próxima execução: abra Editar robô e salve o agendamento em seis campos.

Um tique só dispara se o anterior já terminou

Um robô de 1 em 1 minuto que leva 5 minutos não roda cinco vezes ao mesmo tempo, e também não acumula fila: o tique é pulado enquanto a execução anterior daquele robô, naquele tenant, ainda não terminou. O motivo fica no log da aplicação.

O disparo manual não é a exceção: Executar agora com uma execução em andamento é recusado, com a mensagem dizendo isso — veja Executando manualmente. A diferença entre os dois é só o que se vê: o tique some no log, o clique recebe a recusa na tela.

O bloqueio é por tenant: um robô global lento num assinante não segura o tique dos outros.

O JAR é conferido no envio ​

Ao enviar o JAR, a plataforma o varre e verifica se a classe principal cadastrada existe dentro dele e implementa FlowIaRobot. Se não existir, o envio é recusado na hora e a mensagem lista as classes de robô que o arquivo realmente contém — normalmente é só copiar o nome certo para o campo.

A varredura lê os metadados do bytecode e não carrega as classes no processo do servidor. Carregar código enviado por um tenant executaria o inicializador estático dele dentro da plataforma, que é justamente o que a sandbox existe para impedir.

Sem essa conferência, uma classe principal digitada errado só apareceria quando o robô executasse — no caso de um robô agendado, horas depois, como falha de execução em vez de erro de cadastro.

No mesmo envio a plataforma calcula a impressão digital (SHA-256) do arquivo e a guarda junto da versão. Toda vez que o JAR precisa ser recuperado — depois de um deploy, por exemplo, quando o arquivo já não está no disco do servidor — a plataforma confere essa impressão antes de executar. Se não bater, a execução é recusada com uma mensagem dizendo que o arquivo não está íntegro, em vez de falhar adiante como "classe principal não encontrada", que apontaria para o lado errado.

Cada execução fica presa à versão do JAR que estava ativa quando ela foi enfileirada. Se você enviar uma versão nova enquanto uma execução espera por um runner, ela roda a versão antiga; a nova vale a partir da próxima execução. Execuções criadas antes dessa regra rodam a versão ativa no momento.

Parâmetros declarados pelo JAR ​

O robô pode declarar quais configurações precisa — um robô Java direto na classe, um robô Python em parameters do flowia-robot.json (mesmos campos: name, label, required, secret):

java
@RobotParameter(name = "erpUrl",   label = "URL do ERP", required = true)
@RobotParameter(name = "erpToken", label = "Token",      required = true, secret = true)
public class SincronizaCadastros implements FlowIaRobot { … }

A plataforma lê essa declaração no envio do JAR e monta o formulário a partir dela — em vez de pedir um JSON livre onde uma chave digitada errada só aparece quando o robô executa.

Preencher e salvar esse formulário — e mesmo abri-lo para ver o que está gravado — é ação de Admin do tenant ou Super Admin, o mesmo papel exigido pelo resto do cadastro do robô. Um gerente ou um usuário comum recebe recusa: a configuração nomeia cada parâmetro declarado e mostra para onde a referência de um parâmetro secreto aponta, o que já é meio caminho para o segredo.

Um parâmetro secreto aponta para onde o segredo mora ​

Um campo marcado secret = true não aceita o valor. Escreva nele uma referência, e o segredo continua morando na variável do tenant que já foi feita para guardá-lo. Um valor literal — a senha digitada ali — é recusado ao salvar, tanto na configuração quanto na edição do robô, e a tela já avisa no próprio campo, em vermelho, antes de você clicar; é a mesma regra da senha da caixa de entrada de e-mail. Um valor literal gravado antes dessa regra nunca volta para a tela: aparece como ********, com o aviso de que continua guardado e escondido. Salvar sem mexer nele mantém o que está guardado; troque-o por uma referência quando puder:

ReferênciaDe onde o valor vem
${var.erpToken}uma variável de Variáveis e segredos — e, se não houver, uma variável cadastrada dentro de um processo —, inclusive as marcadas como secretas
${config.erp.baseUrl}a configuração do tenant, por caminho pontuado

A referência é resolvida no momento em que o runner pede os parâmetros, dentro do contexto do tenant. O que fica gravado no cadastro do robô é ${var.erpToken} — que é inofensivo num backup, na tela e no log de auditoria. Os parâmetros também não viajam mais na linha de comando do processo isolado: o sandbox os busca pelo mesmo canal autenticado por execução que já carrega os logs.

Ao salvar os parâmetros, a plataforma já confere cada ${var.…}: uma variável que não existe recusa o salvamento nomeando a que falta. Enquanto a variável apontada não existe, o botão Criar variável abaixo do campo secreto abre uma janela de criação com a chave preenchida, sem sair da página do robô nem perder os outros parâmetros que você estava editando; com a variável existente, o botão some.

Uma referência que não resolve para a execução e diz qual referência era. Ela nunca é trocada por vazio: um token em branco faria o robô autenticar e reportar o que o outro lado dissesse sobre isso, em vez de falhar onde o problema está.

O que continua sem proteção

Uma vez resolvido, o valor está na memória do robô — e o robô é código que o próprio tenant enviou. Ele pode registrar o valor no log, mandá-lo para qualquer lugar ou gravá-lo em arquivo, e nenhum desenho impede isso a não ser não entregar o segredo ao robô.

O que este desenho garante é mais estreito: o segredo não está na linha de comando, não está no cadastro do robô, não sai em GET /a/robots e não é escrito pela plataforma no log da execução. Use mesmo assim credenciais de escopo mínimo e rotacionáveis, nunca a senha principal de um sistema — e, ao escrever um robô, nunca registre o valor de um parâmetro que possa ser um token.

O token da execução também saiu da linha de comando: o agente o entrega ao sandbox pela entrada padrão. Isso vale a partir do agente atualizado — um agente antigo ainda passa o token como argumento, e nesse caso o log da execução traz um aviso dizendo exatamente isso. Se você vir esse aviso, atualize o runner-agent daquela máquina.

O que o robô alcança também deixou de ser tudo. Cada robô declara suas capacidades — ler os próprios parâmetros, escrever o próprio log e ler e gravar o próprio estado valem sempre, e por isso aparecem marcadas e travadas; ler ou escrever no CMS, iniciar processo, contar instâncias, listar membros e subir arquivo são marcadas uma a uma. Cada caixa traz o nome do que libera e uma linha explicando — o código técnico (cms:write etc.) não aparece na tela. O token da execução carrega só o que foi marcado, e a rota recusa o resto com 403, nomeando a capacidade que falta e dizendo onde marcá-la.

Robô que já existia recebeu todas as capacidades na atualização, para nenhuma automação parar de funcionar sem aviso. Robô novo nasce com o mínimo e você marca o que ele precisa — na tela de cadastro, ao criar, e depois na tela do robô, em Salvar acessos. Se uma execução falhar dizendo que falta capacidade, é aí que se resolve, e a mensagem diz qual falta. Salvar acessos grava só as capacidades: não reenvia os parâmetros nem o estado de ativo. Se o servidor recusar — um cron antigo que não é mais aceito, por exemplo —, a mensagem aparece logo abaixo do botão.

Robô global não é editável pelo tenant: quem o mantém é a plataforma, e as capacidades dele vêm junto.

De todo modo, trate a máquina do runner como máquina de confiança: quem tem conta nela lê a memória do processo, e nenhum desenho impede isso.

Robôs globais e a configuração de cada tenant ​

Um robô da plataforma aparece para todos os tenants, mas o JAR é um só e a configuração não pode ser. Por isso a divisão: o JAR declara a forma, cada tenant fornece os valores.

Na página do robô global, um administrador de tenant preenche os campos declarados e salva. Esses valores valem só para o tenant dele.

O mesmo formulário serve o robô do próprio tenant — a diferença está em onde o valor é gravado: no robô, porque ele tem um dono só. Quem usa a tela não precisa saber a diferença.

Alterar o robô em si — nome, agendamento, classe principal, enviar novo JAR, apagar — é do administrador da plataforma. Para um administrador de tenant esses comandos não aparecem na página do robô global: no lugar deles fica um aviso dizendo de quem é a alteração e apontando para o formulário de assinatura logo abaixo. Antes eles apareciam e devolviam uma recusa; oferecer um botão que só sabe recusar é pior do que não oferecer.

O que continua disponível é Executar agora, e não por descuido: um robô global só roda pela assinatura do tenant, então executar é justamente o que o administrador do tenant pode fazer com ele. Para o administrador da plataforma, nada muda — ele vê todos os comandos.

Na lista e no topo da página, a etiqueta Global identifica o robô da plataforma, do mesmo jeito que os processos globais já são marcados.

Um robô global só executa dentro de um tenant ​

O robô global é o registro do JAR, não algo que roda por si. Não existe execução "global": toda execução acontece dentro de um tenant que assinou o robô, com os parâmetros daquele tenant.

Por isso, um tenant que nunca preencheu a configuração não executa o robô — nem pelo botão, nem no agendamento. A recusa diz qual tenant é e onde configurar. Um tenant que desligou a própria assinatura também não executa, mesmo que alguém aperte o botão.

Os valores padrão que aparecem no robô global são um modelo oferecido a quem for assinar, nunca o que roda. Se fossem usados na execução, todo assinante rodaria com a configuração da plataforma — e, no caso de um campo secreto, com a credencial dela.

Quando uma versão nova pede mais do que a anterior ​

Se o administrador da plataforma envia um JAR que declara um obrigatório novo, todas as assinaturas que não têm esse valor ficam incompletas. O envio funciona — a versão nova precisa poder ser publicada — mas o robô não executa naquele tenant até alguém preencher, nem no agendamento nem pelo botão.

Isso é proposital, e é a escolha entre duas coisas ruins: um robô parado com o motivo escrito, ou um robô rodando com metade da configuração e relatando sucesso. A segunda é pior porque ninguém procura.

Agendamento de robô global ​

O agendamento pertence ao robô, e cada tenant pode desligar o dele. No horário marcado a plataforma dispara uma execução por tenant assinante — dez assinantes, dez execuções, cada uma no contexto e com os parâmetros do seu tenant. A falha de um não interrompe os demais, e assinatura incompleta é pulada com o motivo no log.

Ativando e desativando ​

Use o interruptor de ativo/inativo na coluna Situação da listagem para controlar se o agendamento está em execução. Um robô desativado não dispara pelo agendamento, mas permanece cadastrado e pode ser executado à mão. Num robô global, só o administrador da plataforma liga e desliga; salvar os parâmetros da assinatura do tenant não muda se ela está ligada ou desligada.

Editando um robô ​

Na página do robô, Editar robô abre um diálogo com o nome, a descrição, o agendamento, o tempo limite (em segundos), onde ele roda (qualquer runner ou dedicado — veja Um robô pode recusar o runner compartilhado), o interruptor de ativo, a classe principal (ou o ponto de entrada) e o runtime. O runtime só muda enquanto o robô não tem artefato: depois do primeiro envio o campo fica travado, com o motivo — um pacote Python não roda como Java, e o servidor recusa a troca com 400. A chave não muda por ali. Uma recusa do servidor — um cron inválido, por exemplo — aparece no próprio diálogo, sem fechar o que você digitou. Robô global não oferece o botão ao admin do tenant.

Pela API, um PUT /api/a/robots/{chave} que não mande enabled, parameters ou timeoutSeconds mantém o valor salvo de cada um — uma integração que só troca o nome não desliga o robô nem apaga os parâmetros dele. Para limpar os parâmetros, mande parameters como texto vazio.

Parâmetros livres e parâmetros extras ​

Um robô tem dois jeitos de receber configuração, e os dois chegam ao robô do mesmo modo, por context.getParameters():

  • Campos declarados pelo JAR — o formulário montado a partir de @RobotParameter (veja Parâmetros declarados pelo JAR). Quem define a chave é o JAR, com obrigatório e secreto; num robô global, cada tenant preenche os seus.
  • Parâmetros livres — chaves que o administrador escreve, sem declaração. É o JSON digitado no cadastro, e ele pode ser corrigido depois: na página do robô, o cartão Parâmetros é um editor de chave e valor, com Adicionar parâmetro, a lixeira de cada linha e Salvar parâmetros.

Quando o JAR ativo declara campos, o cartão passa a se chamar Parâmetros extras e mostra só as chaves que o JAR não declara — as declaradas ficam no formulário acima. Uma chave que o JAR declara não entra como extra: a linha fica vermelha e Salvar parâmetros fica desabilitado, porque dois editores para a mesma chave se sobrescreveriam. Cada editor grava só as próprias chaves: salvar o formulário declarado não apaga os extras, e salvar os extras não reescreve os campos declarados — nem quando o formulário acima acabou de ser salvo, nem a partir de outra aba.

Nos extras, a referência a uma variável (${var.…}) é recomendada para segredo, mas só é exigida nos campos que o JAR declara secretos. Uma chave com cara de segredo (token, senha, password, secret, key…) com valor escrito por extenso ganha um aviso em amarelo, sem travar o salvar — o valor fica em claro nesta tela. Uma chave que algum JAR anterior do robô declarou secreta continua mascarada como ******** mesmo depois que um JAR novo deixar de declará-la. Ao renomear a chave de uma linha mascarada, o valor é apagado e precisa ser digitado de novo — a máscara nunca é gravada como valor, e o servidor recusa ******** sob uma chave que não tem segredo guardado.

Robô global não mostra o cartão: os parâmetros do próprio robô global não chegam a execução nenhuma — cada tenant executa com os valores da sua assinatura.

Executando manualmente ​

Clique em Executar agora para disparar o robô fora do agendamento. Útil para testes. O botão fica desabilitado enquanto o robô não tem JAR ativo — sem ele não há o que executar.

Quando o disparo é aceito, a tela abre o log da execução que acabou de nascer — pela lista, ela leva à página do robô com essa execução aberta. Quando é recusado, a mensagem do servidor aparece na tela, dizendo o motivo: execução já em andamento, parâmetro obrigatório faltando, assinatura desligada.

Uma execução por vez, por robô e por empresa

Se já existe execução em andamento deste robô nesta empresa, o disparo manual é recusado com essa mensagem. Até 2026-09-02 ele começava assim mesmo, e isso fazia sentido enquanto uma execução não lembrava de nada. Com o Estado guardado entre execuções (abaixo), duas execuções simultâneas leem a mesma posição, processam o mesmo trecho e a última a gravar vence — registros somem sem erro nenhum. Espere a atual terminar ou cancele-a.

Estado guardado entre execuções ​

Um robô que percorre um feed incremental — a próxima página de uma API, o último id já importado — precisa lembrar onde parou. Esse é o Estado: um conjunto de pares chave/valor que o robô grava durante a execução e lê na execução seguinte.

O estado é por robô e por empresa. Num robô global, cada empresa assinante tem o seu: dez assinantes não dividem uma posição de paginação — cada um retoma de onde ele mesmo parou.

Ele é diferente dos parâmetros declarados, e a diferença importa: parâmetro é o que uma pessoa decidiu e digitou no formulário; estado é o que o robô descobriu sozinho. Por isso são duas coisas separadas na tela — um robô não escreve no formulário do administrador, e o administrador editando o formulário não sobrescreve o cursor do robô.

Cada gravação do robô vale na hora. Se ele grava a página depois de processar cada uma e cai na página 7 de 20, a próxima execução começa na 8 — e não da 0.

Na página do robô, o cartão Estado guardado entre execuções mostra o conteúdo atual em JSON. Você pode editá-lo e salvar (o dia em que o sistema de origem renumerar as páginas, é aqui que se corrige) ou usar Limpar estado para zerar. Sem isso, um valor errado só sairia com acesso ao banco.

Dois limites, ditos de propósito:

  • 64 KB no estado inteiro, chaves e valores somados — não é teto por chave. Estado é onde o robô anota onde parou, não onde guarda o resultado. A gravação que faria o conjunto passar do teto é recusada, com o tamanho e o limite na mensagem, e o valor anterior continua valendo. Volume vai para o CMS ou para o armazenamento; aqui fica o ponteiro.
  • Não é lugar de segredo. O estado não é mascarado e aparece nesta tela. Token e senha vão para uma variável do tenant, referenciada por um parâmetro secreto.

Apagar o robô apaga o estado dele junto.

A página do robô ​

Abrindo um robô você vê a classe principal, o agendamento, os parâmetros e a versão do JAR ativo, e é ali que o upload do JAR acontece — depois que o robô existe, porque o arquivo é versionado por robô.

Abaixo fica a tabela de execuções: situação, tipo de disparo (Manual ou Agendada), início, duração, o runner em que ela rodou e o erro quando houve. Uma execução pendente mostra, no lugar do erro, por que ainda não rodou — nenhum runner elegível disponível, robô dedicado sem runner do tenant, ou atribuição que venceu sem ser buscada —, e o log aberto repete o motivo e o runner. Passar o mouse sobre a situação e o disparo explica cada um em uma linha. Enquanto houver execução em aberto — PENDENTE ou EXECUTANDO — a tabela se atualiza sozinha a cada poucos segundos, e para de perguntar quando todas terminam. Não é preciso recarregar a página depois de clicar em Executar agora. Clicar numa execução abre o log dela, e o diálogo tem Baixar .log: um arquivo de texto com o log inteiro, na ordem em que aconteceu, com a origem de cada linha marcada. Vale para grep, para anexar num chamado e para ler fora da tela.

O arquivo completo é selado no armazenamento de objetos quando a execução termina — é o runner que o envia, e a plataforma confere que ele chegou antes de enxugar o banco, que passa a guardar só as últimas linhas para a tela. Uma execução que ainda não selou baixa direto do banco: o botão funciona nos dois casos. As linhas de log são buscadas só nesse momento; a lista não as carrega, para não trazer todas as linhas de todas as execuções a cada abertura da página.

Cancelando uma execução ​

Toda execução ainda em aberto — sem hora de término, seja PENDENTE ou EXECUTANDO — traz um botão Cancelar execução na própria linha da tabela. Execução já terminada não oferece o botão: não há o que cancelar numa linha fechada, e reabri-la apagaria o desfecho que o runner reportou.

A confirmação diz qual dos dois casos é o seu, porque eles não são a mesma coisa:

SituaçãoO que acontece ao confirmar
PENDENTE — nenhum runner coletou aindaResolve na hora. A execução sai da fila, fica como CANCELADO e não chega a rodar
EXECUTANDO — um runner já a coletouO pedido é registrado agora, a linha ganha a marca Cancelamento pedido e continua EXECUTANDO. O agente instalado na máquina descobre no próximo heartbeat, encerra o robô e reporta CANCELADO

Como a tabela já se atualiza sozinha enquanto houver execução em aberto, a mudança de estado aparece na tela sem recarregar. Fica registrado quem cancelou e quando o pedido foi feito.

Cancelado não é falha. A execução termina com o status CANCELADO, sem mensagem de erro e sem aviso de falha para quem a disparou. Ler um cancelamento como falha manda alguém depurar um robô que fez exatamente o que mandaram.

O limite: a plataforma não alcança a máquina do cliente por dentro

Cancelar uma execução em curso é um pedido que o agente precisa vir buscar. Se aquele agente nunca mais chamar — máquina desligada, rede cortada, processo morto —, o robô pode continuar rodando numa máquina que a plataforma não administra.

O que a plataforma faz, e faz sempre: para de despachar aquela execução e para de contar com ela. O lease vence, a linha é fechada como CANCELADO com a explicação escrita nela, e o runner deixa de ser considerado ocupado por causa dela. O que ela não faz é encerrar um processo no servidor de outra pessoa — para isso não existe caminho, porque a plataforma nunca abre conexão para dentro da rede do cliente; é sempre o runner que liga para ela. Quem administra aquela máquina é quem encerra o processo.

Monitorando execuções ​

O histórico de execuções de cada robô exibe:

  • Data e hora de início e fim
  • Status: SUCESSO, FALHA ou CANCELADO
  • Log de saída ou mensagem de erro

O histórico não é eterno: uma execução terminada há mais de 180 dias sai, com o log, na limpeza noturna. A última execução de cada robô fica sempre, e execução em andamento nunca sai. O prazo é da instalação — quem a opera pode aumentá-lo ou desligar a limpeza (Retenção de dados).

Em caso de falha

Falhas de robôs são registradas no log mas não interrompem outros processos. Verifique o log para diagnosticar o problema e corrija o JAR antes de reativar.

Quando a máquina do runner some no meio ​

Duas coisas diferentes podem acontecer, e a plataforma trata cada uma de um jeito — porque o risco não é o mesmo.

A máquina parou de responder antes de buscar o trabalho. O robô não chegou a começar. A execução volta para a fila e a plataforma a entrega a outro runner elegível assim que houver um. Nada a fazer.

A máquina parou de responder com o robô já rodando. Aqui a plataforma não redespacha, e é deliberado: ela não alcança o processo por dentro e não tem como saber se ele ainda está de pé na máquina do cliente. Rodar o mesmo robô de novo faria as duas execuções lerem a mesma posição guardada em Estado, processarem o mesmo trecho, e a última a gravar venceria — registros somem sem erro nenhum. Então a execução é encerrada como FALHA, com o motivo escrito nela, e o robô fica liberado para a próxima. Se o trabalho precisa mesmo ser refeito, dispare o robô pela tela depois de conferir o que a execução interrompida chegou a gravar.

O log tem duas origens, e a tela separa as duas ​

  • [INFO], [WARN], [ERROR] — o que o robô escolheu registrar, chamando context.log(...).
  • [processo] — o que a JVM imprimiu: System.out, System.err, rastreamento de exceção. Aparece recuado e em cinza, porque ler um rastreamento de pilha como se o robô o tivesse escrito de propósito leva a conclusão errada.

As duas sobem juntas, em lotes durante a execução e não só no fim, e por isso ficam na tela na ordem exata em que aconteceram: o context.log("vou baixar o arquivo") aparece imediatamente antes do rastreamento de pilha que explica por que o download falhou.

O nível de um context.log(...) é um entre TRACE, DEBUG, INFO, WARN e ERROR. Qualquer outra coisa é registrada como INFO.

Linhas seguidas de mesma origem e mesmo nível aparecem sob um único [INFO], e as quebras de linha são preservadas: um context.log(...) com um rastreamento de pilha dentro continua legível linha a linha, em vez de virar um parágrafo só.

Um robô que imprime demais tem o começo e o fim guardados, com uma linha no meio dizendo quantas foram descartadas. O começo explica a configuração; o fim explica a falha. O teto vale para as duas origens somadas — assim um robô que registra muito não empurra para fora o rastreamento de pilha que explica a própria falha.

O que o robô imprime vai para a tela como está

Nada mascara essa saída. Um robô que imprimir uma senha coloca a senha no log da execução e no banco. Registre o que aconteceu, nunca a credencial usada.

context.log(...) não depende da rede

O registro de log sai pela saída padrão do processo do robô, e quem o entrega à plataforma é o agente do runner, no mesmo lote da saída do processo. Duas consequências práticas:

  • Um robô encerrado à força no timeout não leva o log junto. Quem tem as linhas nesse momento é o agente, não o processo que acabou de ser morto.
  • context.log(...) não falha por causa da rede, e não tenta reenviar. A garantia que ele dá é "o agente entregou o lote", não "a plataforma confirmou esta linha".

As outras chamadas do RobotContext falam com a plataforma e derrubam a execução quando não conseguem: gravar no CMS, abrir processo, anexar arquivo. Um robô que não conseguiu fazer o que foi mandado fazer não termina como Sucesso.

Runners — a máquina onde o robô roda ​

Um runner é a máquina que de fato executa um robô. A plataforma agenda, atribui e mostra o resultado; quem abre o JAR e roda o código é sempre um runner.

Todo robô roda num runner. A aplicação agenda e mostra o resultado; quem abre o JAR e executa o código é sempre uma máquina registrada como runner. É isso que mantém o código de um cliente fora do disco que guarda a configuração, as credenciais e os anexos dos outros.

A consequência prática é direta: sem nenhum runner cadastrado e respondendo, as execuções ficam PENDENTE e a tela diz por quê, em vez de rodarem em algum lugar que ninguém escolheu.

Numa instalação por Helm já existe runner. Desde 2026-08-29 o chart sobe runner.replicaCount runners (1 por padrão) que geram a própria chave e se inscrevem sozinhos como compartilhados — runner.replicaCount é também o botão de paralelismo, porque um runner compartilhado roda uma execução por vez. Fora do Helm, registrar ao menos um runner faz parte de subir a instalação.

Por que cadastrar um runner próprio ​

Para alcançar um sistema dentro da rede do cliente. Um robô existe, quase sempre, para falar com o ERP, o sistema fiscal ou uma API interna. Sem runner próprio, é a plataforma que precisa chegar até essa rede: regra de firewall de entrada, túnel ou ERP exposto. Um runner instalado dentro da rede do cliente só faz conexões de saída, e a pergunta deixa de existir em vez de ser respondida.

Para não misturar o código de um cliente com o de outro. No runner compartilhado, o JAR de um tenant roda na mesma máquina que o de outro — é exatamente a exposição descrita no aviso O que a sandbox ainda não isola: o disco, mais abaixo. Um runner próprio tira o código daquele cliente de lá. Se cada tenant da sua instalação é um cliente diferente, esse é o motivo mais forte da lista.

Um robô pode recusar o runner compartilhado ​

Na página do robô, o campo Runner diz onde aquele robô aceita rodar:

ValorO que significa
Qualquer runner disponívelo padrão — aceita inclusive o runner compartilhado da instalação
Dedicado — recusa o runner compartilhadosó roda em runner do próprio tenant

Marcar um robô como dedicado é uma decisão de segurança, não de desempenho: é o que impede o JAR daquele cliente de rodar na mesma máquina que o de outro. O preço é que, sem runner do tenant disponível, a execução fica pendente em vez de cair no compartilhado — e a tela de execuções diz exatamente isso, em vez de silenciosamente rodar em outro lugar.

Robôs Python: o que o runner precisa ter ​

Um robô em Python só vai para um runner que declarou ter Python 3.10 ou mais novo (e a versão que o pacote do robô exige). Um runner sem Python nunca recebe robô Python: a execução fica pendente e a tela diz que nenhum runner disponível roda Python. O runner que acompanha o chart e a imagem runner-agent já trazem Python 3.12 (a imagem usa uma base glibc, porque quase toda biblioteca binária do PyPI é manylinux). Num runner que você instalou na máquina do cliente, instale o Python 3.10+ com venv e pip e, se ele não estiver no PATH como python3, aponte com --python.

Cada execução faz tudo do zero, dentro do próprio diretório de trabalho: cria um ambiente virtual, instala só as rodas que o pacote traz em wheels/ (sem rede, sem dependências implícitas, sem rodar código de instalação) e roda o robô. Nada fica de uma execução para a seguinte, e o tempo de preparar o ambiente conta no tempo limite do robô: um pacote com pandas leva de 10 a 30 segundos só para instalar.

Opção do agentePara que serve
--python <caminho>O interpretador a usar. Sem ela, o agente procura python3 (no Windows, python3 e depois python)
--python-env NOME=VALORLibera uma variável de ambiente aos robôs Python, por exemplo PLAYWRIGHT_BROWSERS_PATH=/opt/ms-playwright. Pode ser repetida. Quem decide é o operador do runner: o robô nunca define variáveis. PATH, HOME, os temporários, BASH_ENV, ENV e as famílias PYTHON*, LD_*, DYLD_* e PIP_* não podem ser definidas
--python-max-memory-mb <n>Teto de memória virtual de cada robô Python (padrão 2048)

O teto de memória só existe no Linux. No Linux ele é um limite de verdade (RLIMIT_AS) e um robô que passa dele leva MemoryError. No macOS e no Windows o sistema não o impõe: o agente avisa na subida e cada execução registra um aviso no log, e o robô segue sem limite de memória. Não é equivalente ao -Xmx dos robôs Java. Um robô que abre um navegador (Chromium) reserva muito mais memória virtual do que usa: dê a ele um valor maior, como 8192 ou mais.

Navegadores não vêm na imagem. O Playwright e o Selenium precisam de binários que não são rodas. Instale-os na máquina do runner e libere o caminho com --python-env; o pacote do robô não consegue mudar isso.

O mesmo ambiente mínimo dos robôs Java vale aqui: o robô vê PATH, as variáveis que o operador liberou e um HOME, um diretório de temporários e um cache dentro do diretório da execução. Ele roda com o mesmo usuário do agente, como os robôs Java: o aviso O que a sandbox ainda não isola: o disco vale igual, e mudar o HOME só evita que bibliotecas escrevam por acidente na pasta do agente.

Cadastrando um runner ​

Acesse Integrações e automação → Runners na barra lateral — é uma tela irmã da lista de robôs, e não da área da plataforma: quem cuida dos robôs de um tenant cuida também dos runners dele.

  1. Baixe o agente pelo botão Baixar o agente (flowia-runner-agent.jar), no topo da tela de Runners, e copie o arquivo para a máquina. Ela precisa ter Java 21 ou mais recente instalado.

  2. Na máquina, gere o par de chaves:

    java -jar flowia-runner-agent.jar --init

    O comando imprime a chave pública e o fingerprint dela. A chave privada nunca sai daquele disco.

  3. Clique em Novo Runner — ou em Abrir o formulário de Novo Runner, logo abaixo do passo 2 —, dê um nome (ex.: erp-fiscal-01), cole a chave pública e escolha o escopo.

  4. Na máquina, suba o agente:

    java -jar flowia-runner-agent.jar --url https://<sua-instalacao> --name erp-fiscal-01

A tela tem botão de copiar em cada um dos dois comandos.

O agente confere, ao subir, se alcança a plataforma. Antes de reivindicar qualquer execução ele tenta o endereço de RPC que a plataforma informou. Se não alcançar, ele para ali e diz qual endereço tentou. É quase sempre configuração da plataforma: app.api.base-url precisa ser o endereço da plataforma visto de fora dela — localhost ali é a própria plataforma, nunca a máquina do runner.

A chave nasce na máquina, e por isso nada secreto viaja

A plataforma guarda apenas a chave pública — que é pública por definição. Esta tela não exibe, não gera e não entrega token nenhum: se você estiver procurando onde copiar um segredo, é porque o sentido é o contrário do esperado. Uma chave pública colada no lugar errado não vaza nada.

Escopo — o que aquela máquina aceita rodar ​

EscopoAceita
Todos os robôs deste tenantqualquer robô do tenant dono do runner
Apenas os robôs selecionadossó os robôs escolhidos na hora do cadastro

Todos os robôs deste tenant é o caso comum: uma máquina dentro da rede do cliente que pode rodar o que for daquele cliente. Apenas os robôs selecionados é para a máquina que é especial — a que alcança o sistema fiscal, ou a que tem um dongle de licença espetado — onde "roda tudo" é errado e a lista é curta e deliberada. Ao escolher esse escopo, a tela pede quais robôs.

O log do próprio runner ​

Cada linha da lista tem Log: o que o agente daquela máquina relatou sobre si mesmo — aperto de mão recusado, digest de JAR que não bateu, execução recebida, código de saída do filho, reconexão depois de uma queda. É o que responde "por que meu runner não está pegando trabalho" sem precisar de acesso à máquina.

Ele guarda evento, não rotina: um agente parado, perguntando por trabalho a cada 25 segundos, não escreve nada. E o que aconteceu enquanto a plataforma estava fora do ar não chega — o agente segura as últimas linhas e manda quando volta, mas um silêncio longo aparece como silêncio.

O log do runner compartilhado é da plataforma

Ele nomeia execuções e classes principais de todos os tenants que atendeu, então só o administrador da plataforma lê. O log das execuções do seu tenant continua na tela do robô.

A lista e o estado de cada runner ​

ColunaO que mostra
Nomeo nome dado no cadastro, e a versão do Java da máquina, declarada pelo agente a cada sessão
Pythona versão do Python que o agente achou, e a plataforma da máquina (ex.: linux_x86_64) — é para ela que as rodas do pacote precisam ter sido construídas. Não disponível quando o agente não achou Python 3.10 ou mais novo
Escopoo que aquele runner aceita, e quais robôs quando a lista é explícita
Fingerprinta impressão digital da chave pública, para conferir contra o que o --init imprimiu
Visto por últimoquando o agente fez o último poll
Situaçãoonline, offline, aguardando a primeira conexão ou revogado

"Aguardando a primeira conexão" é normal, não é falha

O cadastro vem antes da máquina: você registra a chave pública e só depois sobe o agente — às vezes dias depois, às vezes em uma máquina que ainda nem foi provisionada. Enquanto isso o runner aparece como Aguardando a primeira conexão, e some desse estado sozinho assim que o agente fizer o primeiro poll. Não há nada a corrigir: enquanto houver runner nesse estado, a lista se atualiza sozinha a cada poucos segundos, sem recarregar a página.

Runners revogados ficam fora da lista por padrão. Quando existe algum, aparece o interruptor Mostrar runners revogados (N) para trazê-los de volta à vista.

Runner sem Python não recebe robô Python

O agente procura python3 no PATH (ou o interpretador indicado em --python) ao subir e declara à plataforma o que achou. Um robô cujo runtime é Python nunca é atribuído a um runner que não declarou Python 3.10 ou mais novo: a execução fica PENDENTE, com o motivo "Nenhum runner disponível executa Python". Robôs Java não são afetados. O envio de pacotes Python (um .zip com o flowia-robot.json) já é aceito quando ROBOTS_PYTHON_ENABLED está ligado; veja o quadro "Pacote Python" em "Cadastrando um novo robô". A coluna Python da lista mostra quais runners recebem robô Python.

Trocando a chave e revogando ​

Máquina reinstalada — rode o --init de novo na máquina nova e cole a chave nova em Editar runner. Preencher a chave revoga a antiga por construção; não é preciso apagar e recadastrar.

Revogar — o botão Revogar pede confirmação e diz o que acontece: o runner falha no próximo poll e para de receber trabalho, e uma execução em curso não é interrompida. Revogar não mata nada no meio do caminho; ele só deixa de ser escolhido daí em diante.

Desenvolvimento do JAR (Native API) ​

O JAR carrega num URLClassLoader próprio, dentro do processo filho, e conversa com a plataforma apenas por RPC autenticado com um token efêmero daquela execução. Se o robô travar, consumir toda a memória ou chamar System.exit, quem cai é o processo dele; a plataforma registra a falha e segue.

O que a sandbox limita:

LimiteO que significa para o código do tenant
Variáveis de ambienteO processo recebe só o mínimo para a JVM subir. Credencial de banco e chave de provedor de IA não são visíveis
MemóriaSobe com -Xmx (padrão 512 MB, configurável); um robô desgovernado morre sozinho
TempoAo estourar o limite o processo é encerrado à força, não apenas marcado como expirado
Disco de trabalhoDiretório de trabalho e de temporários só daquela execução, apagado ao terminar, inclusive em falha. Um arquivo em /tmp não sobrevive de uma execução para a seguinte
AlcanceSó o que o RobotContext expõe, sempre dentro do tenant dono do robô

O que a sandbox ainda não isola: o disco

O processo do robô roda no runner, com o mesmo usuário de sistema operacional do agente de runner que o executa — desde 2026-08-27 nenhum robô roda mais na máquina da plataforma —, e portanto com as mesmas permissões de arquivo desse usuário. Ambiente, memória e tempo de execução são limitados; o sistema de arquivos não é.

Se os tenants da instalação representam partes diferentes — os clientes da sua empresa, por exemplo, um tenant por cliente — e compartilham um runner, isso significa que o código enviado por um tenant pode ler o que o usuário do agente alcança, incluindo a chave privada do agente e o que a execução de outro tenant deixou na máquina. Não é uma limitação teórica e não há configuração que a desligue.

Vale reforçar porque a leitura natural é a oposta: o fato de a instalação ser de uma empresa só não diminui isso. Quando cada tenant é um cliente seu, a fronteira entre eles é um compromisso de confidencialidade entre partes distintas — e aí ela é mais estrita, não menos.

O isolamento de verdade exige que a execução do robô rode no próprio contêiner — as outras três saídas não funcionam neste modelo de implantação (dropar privilégio exige rodar como root, sandbox de sistema operacional exige capacidades que o runtime gerenciado não concede, e o SecurityManager foi removido no Java 21). Esse trabalho ainda não foi implementado.

Até lá: trate o envio de JAR como uma permissão de confiança. Só não há exposição entre tenants quando os tenants são divisões da mesma organização. Se cada tenant é um cliente diferente, conceda ADMIN de tenant apenas a quem você confiaria com acesso de leitura ao servidor — e, na prática, apenas à sua própria equipe.

Acessando Dados, CMS e Variáveis de Processo (JSONB) ​

Os robôs podem interagir de forma rica com os dados de instâncias de processos (utilizando variáveis estruturadas em JSON/JSONB), e também com o banco de dados sem esquema do Flow.IA (CMS).

Para criar o seu robô, implemente a interface FlowIaRobot:

java
import ia.flow.engine.FlowIaRobot;
import ia.flow.engine.RobotContext;
import java.util.Map;

public class MeuRobo implements FlowIaRobot {
    @Override
    public void execute(RobotContext context) {
        context.log("Iniciando varredura no CMS...");

        // 1. Acesso aos parâmetros estáticos configurados na tela do robô
        Map<String, String> params = context.getParameters();
        
        // 2. Acesso Native API: Consumir ou escrever no CMS do Tenant
        context.getCms().getRecordsByCollectionSlug("faturas")
               .forEach(record -> {
                   context.log("Processando fatura: " + record);
               });

        // 3. Acesso Native API: Iniciar novos processos
        // É possível passar variáveis complexas (JSONB) diretamente no mapa de inicialização.
        Map<String, Object> processVariables = Map.of(
            "origem", "robo_agendado",
            "detalhesProcessamento", Map.of("urgente", true, "categoria", "A") // Variável que será convertida em JSONB
        );
        context.getProcesses().startProcess("faturamento-workflow", processVariables);

        // 4. Acesso Native API: Consultar membros do tenant e dados gerais
        var members = context.getTenant().getMemberEmails();
        context.log("Membros notificados: " + members.size());

        // 5. Acesso Native API: Fazer upload de arquivos 
        // Ideal para casos em que o robô baixa arquivos externos e inicia processos com anexos
        byte[] pdfContent = new byte[]{ /* ... bytes do arquivo baixado ... */ };
        java.util.UUID attachmentId = context.getStorage().uploadAttachment(
            "instance", "uuid-da-instancia-criada", "fatura.pdf", "application/pdf", pdfContent
        );
        context.log("Fatura anexada com sucesso: " + attachmentId);
    }
}

O RobotContext é sua fachada isolada para o Flow.IA. Ele garante que qualquer operação (como getCms(), getProcesses() ou getStorage()) esteja restrita apenas aos dados pertencentes ao Tenant do robô ativo, prevenindo que um robô vaze dados de empresas vizinhas na plataforma. Você tem autonomia para consultar processos públicos do tenant, membros, etc., com segurança e agilidade.

De onde vem a anotação ​

@RobotParameter não é publicada em repositório Maven: declare a sua própria cópia, com o mesmo pacote e o mesmo nome. O código da anotação e o porquê estão em Java Delegates → De onde vem a anotação — a única diferença é que @RobotParameter tem secret e @DelegateParameter não.

Desenvolvimento do pacote Python ​

Um robô Python é um .zip com esta forma:

invoice-sync.zip
├── flowia-robot.json          obrigatório
├── src/                       o código (qualquer organização; o ponto de entrada é resolvido aqui)
│   └── invoice_sync/
│       ├── __init__.py
│       └── main.py            def run(ctx): ...
├── requirements.txt           opcional
└── wheels/                    opcional: cada dependência como .whl

O manifesto, flowia-robot.json:

json
{
  "manifestVersion": 1,
  "entryPoint": "invoice_sync.main:run",
  "requiresPython": ">=3.10",
  "sourceRoot": "src",
  "parameters": [
    { "name": "erpUrl",   "label": "URL do ERP", "required": true },
    { "name": "erpToken", "label": "Token do ERP", "required": true, "secret": true }
  ]
}
  • entryPoint é modulo:funcao. A função recebe um argumento, o contexto. Um robô que retorna terminou com sucesso; uma exceção termina a execução como FALHOU, e sys.exit(n) passa o código n.
  • requiresPython aceita só >=3.X ou >=3.X.Y, com no mínimo 3.10. A página do robô mostra esse mínimo ao lado da versão do pacote, e a execução só vai para um runner com Python igual ou mais novo.
  • sourceRoot é opcional; sem ele, o código é procurado na raiz do pacote.
  • parameters monta o mesmo formulário de configuração que o @RobotParameter monta para um JAR.

Dependências vão junto, como rodas. Gere a pasta com pip download --only-binary=:all: -d wheels -r requirements.txt numa máquina com a mesma plataforma do runner (a coluna Python da tela de runners mostra qual é). Como a instalação roda sem dependências implícitas, o requirements.txt precisa listar também as dependências das dependências. Cada linha é só nome ou nome==versão: opções do pip, URLs e caminhos são recusados pelo runner. O runner instala sem acessar a internet e sem executar código de instalação; por isso um .tar.gz em wheels/ é recusado no envio. Uma roda construída para outra plataforma falha na instalação, com a mensagem do pip no log da execução. O que o runner precisa ter, e as opções do agente para Python, estão em Robôs Python: o que o runner precisa ter.

O contexto.

python
def run(ctx):
    ctx.log("Iniciando varredura no CMS...")
    erp_url = ctx.parameters["erpUrl"]
    ultimo = ctx.state.get("cursor")
    for registro in ctx.cms.records("clientes"):
        ctx.log("cliente " + str(registro.get("id")) + " enviado para " + erp_url, "debug")
    ctx.state.put("cursor", str(int(ultimo or 0) + 1))

ctx tem a mesma superfície do RobotContext Java: ctx.tenant_slug, ctx.parameters, ctx.log(mensagem, nivel="INFO"), ctx.cms.records(slug) e ctx.cms.create(slug, dados), ctx.processes.start(chave, variaveis) e ctx.processes.count_active(chave), ctx.tenant.member_emails(), ctx.state.get/put/remove e ctx.storage.upload_attachment(tipo, id, nome, content_type, conteudo). Uma chamada que a plataforma recusa (por exemplo um recurso que o robô não tem permissão para usar) levanta RobotRpcError.

O robô registra o que quiser no log com ctx.log: as duas origens (o que o robô registrou e o que o processo imprimiu) aparecem separadas na tela, como nos robôs Java. Quem chama um modelo de IA com a chave do próprio tenant, pela rede, gasta na conta do tenant: a plataforma não mede esse consumo.

SDK. Com Python ligado na instalação, a tela de robôs (e a página de um robô Python) oferece Baixar SDK Python: o RobotRunnerPython.zip, com o módulo flowia_robot e o flowia_robot.testing.FakeContext, para rodar o robô no seu computador sem a plataforma. O zip não traz manifesto: copie o exemplo de flowia-robot.json do começo desta seção.

Ligar Python na instalação. O envio do pacote exige que a instalação habilite Python em robôs (app.robots.python.enabled, ROBOTS_PYTHON_ENABLED), que vem desligado. Enquanto estiver desligado, o cadastro mostra a opção Python indisponível, com o nome da configuração. O pacote tem limites de entradas e de tamanho descompactado, e as recusas dizem a entrada e o que fazer.

Flowi Agentic — Plataforma de Gestão de Processos com IA