O Universal Flow é um motor de fluxo leve e orientado a estado, projetado para integração universal. Seu princípio fundamental é orquestrar fluxos de trabalho onde cada passo (um "nó" ou node) é um processo independente e autônomo que se comunica exclusivamente através de um gerenciador de estado centralizado.
O objetivo principal do Universal Flow é fornecer um motor simples e poderoso para gerenciar fluxos de trabalho complexos em qualquer aplicação. Em vez de passar dados diretamente entre funções ou serviços, os nós são completamente desacoplados. Eles não recebem entradas diretas nem retornam saídas diretas. Em vez disso, eles:
- Consultam uma API central para obter os dados de que precisam.
- Executam sua lógica de negócio.
- Chamam a API central novamente para reportar seu resultado e determinar o próximo passo no fluxo.
Isso torna o sistema altamente modular, escalável e fácil de depurar, pois todo o estado de cada fluxo de trabalho é persistido e auditável em todos os momentos.
Para executar o motor do Universal Flow, simplesmente execute o seguinte comando no diretório raiz do projeto:
go run main.goO servidor será iniciado na porta 8080.
O motor é controlado através de uma API REST simples.
Para iniciar um novo fluxo de trabalho, você envia uma requisição POST com a estrutura do fluxo. O motor irá salvá-lo e começar imediatamente a executar o primeiro nó.
Endpoint: POST /api/flow-state/create-flow-to-run
Corpo (Body):
{
"id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"name": "Meu Primeiro Fluxo",
"url_base_server": "http://localhost:3000",
"nodes": [
{
"id": "a1b2c3d4-e5f6-a7b8-c9d0-e1f2a3b4c5d6",
"name": "Nó Inicial",
"script_path": "node /path/to/your/start-script.js",
"output_node": ["b2c3d4e5-f6a7-b8c9-d0e1-f2a3b4b5c6d7"]
},
{
"id": "b2c3d4e5-f6a7-b8c9-d0e1-f2a3b4b5c6d7",
"name": "Nó Intermediário",
"script_path": "server my-node-name",
"output_node": ["c3d4e5f6-a7b8-c9d0-e1f2a3b4b5c6d7", "d4e5f6a7-b8c9-d0e1-f2a3b4b5c6d8"]
},
{
"id": "c3d4e5f6-a7b8-c9d0-e1f2a3b4b5c6d7",
"name": "Nó Final (Caminho A)",
"script_path": "node /path/to/your/end-script-A.js",
"output_node": []
},
{
"id": "d4e5f6a7-b8c9-d0e1-f2a3b4b5c6d8",
"name": "Nó Final (Caminho B)",
"script_path": "node /path/to/your/end-script-B.js",
"output_node": []
}
]
}O campo output_node é fundamental para definir a estrutura do seu fluxo. Ele é um array de strings que contém os IDs de todos os nós que são destinos possíveis a partir do nó atual.
- Fluxos Lineares: Se um nó tem apenas um caminho possível, o
output_nodeconterá um único ID. - Bifurcações e Condicionais: Se um nó pode levar a múltiplos caminhos diferentes (ex: "Aprovar" vs. "Rejeitar"), o
output_nodeconterá os IDs de todos os nós de destino possíveis. - Nós Finais: Um nó que finaliza um fluxo (ou um caminho do fluxo) terá um array vazio
[].
A responsabilidade de escolher qual caminho seguir, dentre as opções listadas no output_node, é da lógica interna do script do nó. Ao chamar o endpoint finish-node, o script deve passar o ID do nó escolhido no campo next_node_id.
Para suportar a orquestração de nós que são serviços externos (microsserviços), você pode definir o campo opcional url_base_server no corpo da criação do fluxo.
url_base_server: Uma string contendo a URL base para todos os nós do tiposervernesse fluxo.
Quando um nó é definido com um script_path que começa com server , o motor de fluxo não executará um script local. Em vez disso, ele fará uma requisição HTTP para o serviço correspondente.
Sintaxe do script_path: server <nome-do-no-no-servidor>
Exemplo:
Se url_base_server for "http://localhost:3000" e o script_path for "server my-node", o motor fará a seguinte requisição:
GET http://localhost:3000/my-node?flow_id=<FLOW_ID>&node_id=<NODE_ID>
O serviço externo deve retornar um status 200 OK para que o nó seja considerado completed. Qualquer outro status resultará em um failed. Os parâmetros flow_id e node_id são injetados para que o serviço possa interagir de volta com a API do Universal Flow, por exemplo, para finalizar o nó.
Você pode recuperar o estado completo e em tempo real de qualquer fluxo de trabalho a qualquer momento. Isso é útil para monitoramento e depuração.
Endpoint: GET /api/flow-state/get-flow-state/:id
:idé o ID do fluxo que você deseja inspecionar.
Este é o endpoint mais crítico para desenvolvedores de nós. Quando o script de um nó termina sua tarefa, ele deve chamar este endpoint para informar ao motor que terminou e o que deve acontecer a seguir.
Endpoint: PATCH /api/flow-state/finish-node?flowId=<FLOW_ID>
Para marcar o nó como completed (concluído) e informar ao motor qual nó executar em seguida.
Corpo (Body):
{
"node_id": "b2c3d4e5-f6a7-b8c9-d0e1-f2a3b4b5c6d7",
"next_node_id": "c3d4e5f6-a7b8-c9d0-e1f2a3b4b5c6d7",
"node_output": "{\"resultado\":\"ok\"}"
}Para marcar o nó como failed (falhou), o que também irá parar todo o fluxo e marcá-lo como failed.
Corpo (Body):
{
"node_id": "b2c3d4e5-f6a7-b8c9-d0e1-f2a3b4b5c6d7",
"error_message": "Falha ao conectar com o serviço externo."
}Um nó é simplesmente um comando executável (ex: node meu-script.js, python process.py). O motor em si não conhece nem se importa com a lógica interna do nó. Ele apenas se preocupa com a comunicação do nó com a API.
Quando o motor do Universal Flow executa um nó, ele injeta o contexto de execução como variáveis de ambiente:
FLOW_ID: O ID do fluxo atualmente em execução.NODE_ID: O ID da instância específica do nó que está sendo executada.
Seu script deve ler essas variáveis para interagir com a API corretamente.
- O motor executa seu script (ex:
node meu-script.js). - Dentro do seu script, você lê as variáveis de ambiente:
process.env.FLOW_IDeprocess.env.NODE_ID. - (Opcional) Seu script pode fazer uma requisição
GETpara/api/flow-state/get-flow-state/:idpara obter dados produzidos por nós anteriores. - Seu script executa sua lógica de negócio.
- Seu script faz uma requisição
PATCHpara/api/flow-state/finish-nodepara sinalizar sua conclusão, escolhendo o próximo nó a ser executado ou reportando um erro.
Aqui está um exemplo completo de um script de nó. Ele lê seu contexto, executa uma tarefa e reporta sua conclusão ao motor do Universal Flow.
// Um script de nó simples para o Universal Flow
// Esta é a função que fará o trabalho principal.
async function main() {
// 1. Ler o contexto das variáveis de ambiente.
const flowId = process.env.FLOW_ID;
const nodeId = process.env.NODE_ID;
if (!flowId || !nodeId) {
console.error("Erro: FLOW_ID e NODE_ID devem ser definidos.");
// Se o contexto estiver faltando, não podemos prosseguir.
// Em um cenário real, você poderia querer reportar isso como uma falha.
return;
}
console.log(`Executando o nó ${nodeId} para o fluxo ${flowId}`);
// 2. (Opcional) Buscar o estado atual do fluxo para obter dados.
// Neste exemplo, assumimos que este nó precisa da saída do nó anterior.
// const flowStateResponse = await fetch(`http://localhost:8080/api/flow-state/get-flow-state/${flowId}`);
// const flowState = await flowStateResponse.json();
// console.log("Estado atual do fluxo:", flowState);
// 3. Executar a lógica de negócio.
// Para este exemplo, vamos apenas esperar um segundo para simular trabalho.
await new Promise(resolve => setTimeout(resolve, 1000));
const resultado = { message: "Tarefa concluída com sucesso!", timestamp: new Date().toISOString() };
// 4. Finalizar o nó chamando a API.
// Este nó decidirá mover para o próximo nó, que vamos definir aqui para simplificar.
// Em um cenário real, o `nextNodeId` pode ser determinado pela lógica de negócio.
const nextNodeId = "c3d4e5f6-a7b8-c9d0-e1f2a3b4b5c6d7"; // ID de exemplo
const finishUrl = `http://localhost:8080/api/flow-state/finish-node?flowId=${flowId}`;
const finishBody = {
node_id: nodeId,
next_node_id: nextNodeId,
node_output: JSON.stringify(resultado)
};
try {
const response = await fetch(finishUrl, {
method: 'PATCH',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(finishBody)
});
if (response.ok) {
console.log(`Nó ${nodeId} finalizado com sucesso.`);
} else {
const errorData = await response.json();
console.error(`Falha ao finalizar o nó ${nodeId}:`, errorData);
}
} catch (error) {
console.error("Erro ao chamar a API de finish-node:", error);
}
}
// Executa a função principal.
main();Este exemplo demonstra o padrão principal: ler o contexto, fazer o trabalho e reportar de volta ao motor. Isso mantém seus nós simples, sem estado (stateless) e universalmente compatíveis com o motor de fluxo.
Aqui está um exemplo simples de um nó escrito em Go. Ele demonstra como obter o contexto, consultar o estado do fluxo e sinalizar sua conclusão.
package main
import (
"bytes"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
)
// NodeOutput representa a estrutura da saída de um nó.
type NodeOutput struct {
FileName string `json:"fileName"`
Message string `json:"message"`
}
// FinishNodePayload é o payload enviado ao endpoint de finalização do nó.
type FinishNodePayload struct {
NodeID string `json:"node_id"`
NextNodeID string `json:"next_node_id,omitempty"`
NodeOutput json.RawMessage `json:"node_output,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
// Exemplo simplificado de como um nó Go interage com o Universal Flow.
func main() {
// 1. Ler o contexto das variáveis de ambiente.
flowID := os.Getenv("FLOW_ID")
nodeID := os.Getenv("NODE_ID")
if flowID == "" || nodeID == "" {
log.Fatal("Erro: FLOW_ID e NODE_ID devem ser definidos nas variáveis de ambiente.")
}
fmt.Printf("Executando o nó %s para o fluxo %s\n", nodeID, flowID)
// 2. (Opcional) Buscar o estado atual do fluxo (apenas para demonstração, não usado aqui).
// Em um nó real, você faria uma requisição GET para /api/flow-state/get-flow-state/:flowID
// para obter dados de nós anteriores.
// 3. Executar a lógica de negócio.
// Para este exemplo, vamos simular uma tarefa e gerar uma saída.
outputData := NodeOutput{
FileName: "report_2026-01-02.pdf",
Message: "Relatório gerado com sucesso.",
}
outputJSON, err := json.Marshal(outputData)
if err != nil {
log.Fatalf("Erro ao serializar output: %v", err)
}
// 4. Finalizar o nó chamando a API do Universal Flow.
// Em um cenário real, `nextNodeID` seria determinado pela lógica de negócio.
// Usaremos um ID de exemplo.
nextNodeID := "c3d4e5f6-a7b8-c9d0-e1f2a3b4b5c6d7" // ID de exemplo do próximo nó
finishURL := fmt.Sprintf("http://localhost:8080/api/flow-state/finish-node?flowId=%s", flowID)
payload := FinishNodePayload{
NodeID: nodeID,
NextNodeID: nextNodeID,
NodeOutput: outputJSON,
}
payloadBytes, err := json.Marshal(payload)
if err != nil {
log.Fatalf("Erro ao serializar payload: %v", err)
}
req, err := http.NewRequest("PATCH", finishURL, bytes.NewBuffer(payloadBytes))
if err != nil {
log.Fatalf("Erro ao criar requisição: %v", err)
}
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
log.Fatalf("Erro ao fazer requisição PATCH: %v", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
log.Fatalf("Falha ao finalizar nó. Status: %s", resp.Status)
}
fmt.Printf("Nó %s finalizado com sucesso para o fluxo %s.\n", nodeID, flowID)
}