Workflows modernos não são mais apenas automações lineares. Eles envolvem uma variedade de APIs e serviços, fazem loop sobre a saída de um modelo, pausam para decisões humanas e retomam horas ou dias depois exatamente de onde pararam.
Projetamos o executor do Sim para tornar esses padrões naturais. Este post compartilha a arquitetura que acabamos tendo, os desafios que encontramos ao longo do caminho e o que isso permite para equipes construindo sistemas agenticos em escala.
Estabelecendo a Base
Há uma única filosofia orientadora que usamos ao projetar o executor: workflows devem ler como o trabalho que você pretende fazer, não como a bagunça de cabos atrás de uma TV. A complexidade de fiação e encanamento deve ser abstraída, e construir um workflow performático de ponta a ponta deve ser fácil, modular e perfeito.
É por isso que o executor do Sim serve tanto como orquestrador quanto como camada de tradução, transformando representações de workflow amigáveis ao usuário em um DAG executável nos bastidores.
Motor principal
Em seu núcleo, o executor descobre quais blocos podem executar, os executa e depois repete. Parece simples em teoria, mas pode se tornar surpreendentemente complexo quando você considera roteamento condicional, loops aninhados e paralelismo verdadeiro.
Compilando Workflows para Grafos
Antes da execução começar, compilamos o workflow visual em um grafo acíclico direcionado (DAG). Cada bloco se torna um nó e cada conexão se torna uma aresta. Loops e subfluxos paralelos se expandem em estruturas mais complexas (nós sentinela para loops, nós indexados por ramo para paralelos) que preservam a propriedade DAG enquanto permitem iteração e concorrência.
Esta compilação antecipada se paga imediatamente: toda a topologia é concretamente definida antes do primeiro bloco executar.
A Fila de Execução
Uma vez que temos o DAG, a execução se torna orientada a eventos. Mantemos uma fila pronta: nós cujas dependências estão todas satisfeitas. Quando um nó completa, removemos suas arestas de saída dos conjuntos de arestas de entrada dos nós downstream. Qualquer nó que atinge zero arestas de entrada vai direto para a fila. Em seu núcleo, ordenação topológica.
A diferença chave aqui das abordagens tradicionais de execução de workflow: não esperamos uma "camada" terminar. Se três nós na fila são independentes, lançamos todos os três imediatamente e deixamos o runtime lidar com a concorrência.
Resolução de Dependências
Em nossos protótipos anteriores, escaneávamos o array de conexões após cada execução de bloco para ver o que ficou pronto. No entanto, conforme o número de nós e arestas escala, o desempenho é afetado.
O DAG inverte esse modelo. Cada nó rastreia suas próprias arestas de entrada em um conjunto. Quando uma dependência completa, removemos um elemento do conjunto. Quando o conjunto atinge zero, o nó está pronto. Sem escaneamento, sem filtragem, sem verificações repetidas.
Esta otimização se compõe quando você tem muitos ramos paralelos ou estruturas profundamente aninhadas. Cada nó conhece sua própria prontidão sem perguntar ao resto do grafo.
Resolução de Variáveis
Blocos referenciam dados de diferentes fontes: itens de loop (<loop.iteration>, <loop.item>), índices de ramo paralelo (<parallel.index>), saídas de blocos upstream (<blockId.output.content>), variáveis de workflow (<workflow.variableName>), e variáveis de ambiente (${API_KEY}). O resolvedor tenta cada escopo em ordem—loop primeiro, depois paralelo, depois workflow, depois ambiente, depois saídas de blocos. Escopos internos sombreiam os externos, correspondendo à semântica de escopo padrão. Isso torna as variáveis previsíveis: o contexto em que você está determina o que você vê, sem colisão de nomes ou prefixos manuais.
Múltiplos Acionadores e Compilação Seletiva
Um workflow pode ter múltiplos pontos de entrada. Webhooks escutam em caminhos diferentes, agendamentos executam em cadências diferentes, e alguns acionadores podem disparar da UI. Cada um representa um ponto de partida válido, mas apenas um importa para qualquer execução dada.
O construtor de DAG lida com isso através de compilação seletiva. Quando um workflow executa, recebemos um ID de bloco acionador. O construtor começa desse nó e constrói apenas o subgrafo alcançável. Blocos que não estão downstream do acionador nunca entram no DAG.
Isso mantém a execução focada. Um workflow com cinco acionadores de webhook diferentes não compila todos os cinco caminhos toda vez. A topologia se adapta ao contexto automaticamente.
Executando do Cliente
O executor vive no servidor. Usuários constroem workflows no cliente. Conforme iteram e testam, precisam ver entradas e saídas de blocos, assistir o progresso da execução em tempo real e entender quais caminhos o workflow toma.
Polling adiciona latência. Duplicar lógica de execução no cliente cria deriva. Precisávamos de uma forma de transmitir o estado de execução conforme acontece.
O executor emite eventos em pontos-chave de execução—inícios de blocos, conclusões, conteúdo em streaming, erros. Esses eventos fluem através de SSE para clientes conectados. O cliente reconstrói o estado de execução do stream, renderizando logs e saídas conforme os blocos completam.
Paralelismo
Quando um workflow se ramifica para chamar múltiplas APIs, comparar saídas de diferentes modelos, ou processar itens independentemente, esses ramos devem executar ao mesmo tempo. Não intercalados, não sequencialmente—realmente concorrentes.
A maioria das plataformas de workflow lida com ramos diferentemente. Algumas os executam um após o outro (o modo v1 do n8n completa ramo 1, depois ramo 2, depois ramo 3). Outras intercalam execução (executam o primeiro nó de cada ramo, depois o segundo nó de cada ramo). Ambas as abordagens são determinísticas, mas nenhuma dá paralelismo verdadeiro.
As soluções alternativas tipicamente envolvem acionar sub-workflows separados com "aguardar conclusão" desabilitado, depois coletar resultados manualmente. Isso funciona, mas significa coordenar estado de execução entre múltiplas instâncias de workflow, lidar com falhas independentemente e costurar saídas de volta juntas.
Como abordamos isso
A fila pronta nos dá paralelismo por padrão. Quando um bloco paralelo executa, ele se expande em nós indexados por ramo no DAG. Cada ramo é uma cópia separada dos blocos dentro do escopo paralelo, indexada por número de ramo.
Todos os nós de entrada em todos os ramos entram na fila pronta simultaneamente. O executor os lança concorrentemente—são nós independentes com dependências satisfeitas. Conforme cada ramo progride, seus nós downstream ficam prontos e executam. O orquestrador paralelo rastreia conclusão contando nós terminais em todos os ramos.
Quando todos os ramos terminam, agregamos suas saídas em ordem de ramo e continuamos. Sem sobrecarga de coordenação, sem coleta manual de resultados—apenas execução concorrente com agregação determinística.
O que isso permite
Um workflow que chama cinquenta APIs diferentes as processa concorrentemente. Comparações paralelas de modelos retornam resultados conforme chegam em streaming, não após o mais lento terminar.
O DAG não distingue entre "ramos paralelos" e "blocos independentes que por acaso estão prontos ao mesmo tempo." Ambos executam concorrentemente. Paralelismo simplesmente emerge da estrutura do workflow.
Subfluxos paralelos para autoria mais limpa
Para trabalho paralelo repetitivo, adicionamos subfluxos paralelos. Em vez de duplicar blocos visualmente para cada ramo na tela, você define um único subfluxo e configura o bloco paralelo para executá-lo N vezes ou uma vez por item em uma coleção.
Nos bastidores, isso se expande para a mesma estrutura DAG indexada por ramo. O executor não distingue entre ramos paralelos criados manualmente e gerados por subfluxo—ambos se tornam nós independentes que executam concorrentemente. Mesmo modelo de execução, experiência de autoria mais limpa.
Loops
Como loops compilam para DAGs
Loops apresentam um desafio para DAGs: grafos são acíclicos, mas loops repetem. Lidamos com isso expandindo loops em nós sentinela durante a compilação.
Loops se expandem em nós sentinela de início e fim. A aresta reversa só ativa quando o loop continua, preservando a propriedade acíclica do DAG.
Um loop é delimitado por dois nós: um sentinela de início e um sentinela de fim. O sentinela de início ativa os primeiros blocos dentro do loop. Quando blocos terminais completam, eles roteiam para o sentinela de fim. O sentinela de fim avalia a condição do loop e retorna "continue" (que roteia de volta para o início) ou "exit" (que ativa blocos após o loop).
A aresta reversa do fim para o início não conta como dependência inicialmente—ela só ativa se o loop continuar. Isso preserva a propriedade DAG enquanto permite iteração.
Estado de iteração e escopo de variáveis
Quando um loop continua, o executor não reexecuta blocos do zero. Ele limpa seu estado de execução (marcando-os como ainda não executados) e restaura suas arestas de entrada, então eles ficam prontos para a próxima passada. Escopo do loop atualiza: iteração incrementa, o próximo item carrega (para forEach), saídas da iteração anterior se movem para os resultados agregados.
Blocos dentro do loop acessam variáveis de loop através da cadeia de resolvedor. <loop.iteration> resolve antes de verificar saídas de blocos ou variáveis de workflow, então o contexto de iteração sombreia escopos externos. Isso torna o acesso a variáveis previsível—você sempre obtém o estado atual do loop.
Condições e Roteadores
Workflows se ramificam com base em decisões em tempo de execução. Um bloco de condição avalia expressões e roteia para caminhos diferentes. Um bloco de roteador permite que um modelo de IA escolha qual caminho tomar com base no contexto. Ambos são fundamentais para construir workflows adaptativos.
Roteamento orientado por LLM
Blocos de roteador representam um padrão moderno em orquestração de workflows. Em vez de codificar lógica com cadeias if/else, você descreve as opções e deixa um modelo de linguagem decidir. O modelo vê o contexto da conversa, avalia qual caminho faz sentido e retorna uma seleção.
O executor trata essa seleção como uma decisão de roteamento. Cada aresta de saída de um roteador carrega metadados sobre qual bloco alvo ela representa. Quando o roteador completa, retorna o ID do bloco escolhido. O gerenciador de arestas ativa apenas a aresta correspondente a esse ID; todas as outras arestas desativam.
Isso torna o roteamento orientado por IA determinístico e rastreável. Você pode inspecionar o log de execução e ver exatamente qual caminho o modelo escolheu, por quê (do raciocínio do modelo) e quais alternativas foram podadas.
Seleção de arestas e poda de caminhos
Quando uma condição ou roteador executa, avalia sua lógica e retorna uma única seleção. O gerenciador de arestas verifica cada aresta de saída para ver se seu rótulo corresponde à seleção. A aresta correspondente ativa; o resto desativa.
Quando uma condição seleciona um caminho, a aresta escolhida ativa enquanto caminhos não selecionados desativam recursivamente, impedindo que blocos inalcançáveis executem.
A desativação em cascata. Se uma aresta desativa, o executor recursivamente desativa todas as arestas downstream de seu alvo—a menos que esse alvo tenha outras arestas de entrada ativas. Esta poda automática impede que blocos inalcançáveis entrem na fila pronta.
O benefício: trabalho desperdiçado cai para zero. Caminhos que não executarão não consomem recursos, não esperam na fila e não poluem logs de execução. O DAG reflete o que realmente executou, não o que poderia ter executado.
Convergência e reunificação de caminhos
Workflows frequentemente divergem e reconvergem. Múltiplos ramos de condição podem levar a diferentes etapas de processamento, depois se fundir em um bloco de agregação comum. O executor lida com isso através de contagem de arestas.
Quando caminhos convergem, o bloco alvo tem múltiplas arestas de entrada—uma de cada caminho upstream. O gerenciador de arestas rastreia quais arestas ativam. Se uma condição poda um ramo, essa aresta desativa e a contagem de arestas de entrada do alvo diminui. O alvo fica pronto apenas quando todas as arestas de entrada ativas restantes completam.
Isso funciona para topologias complexas: condições aninhadas, roteadores alimentando outros roteadores, ramos paralelos que reconvergem após diferentes quantidades de trabalho. O rastreamento de dependências se adapta automaticamente.
Humano no loop
Workflows de IA não são totalmente automatizados. Eles pausam para aprovações, aguardam feedback humano ou param para deixar alguém revisar a saída do modelo antes de continuar. Essas pausas podem acontecer em qualquer lugar—meio do ramo, dentro de um loop, através de múltiplos caminhos paralelos ao mesmo tempo.
Detecção de pausa e captura de estado
Quando um bloco retorna metadados de pausa, o executor para de processar suas arestas de saída. Em vez de continuar para blocos downstream, captura o estado atual de execução: cada saída de bloco, cada iteração de loop, o progresso de cada ramo paralelo, cada decisão de roteamento e a topologia exata de dependências restantes no DAG.
Cada ponto de pausa recebe um ID de contexto único que codifica sua posição. Uma pausa dentro de um loop na iteração 5 recebe um ID diferente do mesmo bloco na iteração 6. Uma pausa no ramo paralelo 3 recebe um ID diferente do ramo 4. Isso torna o direcionamento de retomada preciso—você pode retomar pontos de pausa específicos independentemente.
O executor suporta múltiplas pausas simultâneas. Se três ramos paralelos cada um encontra um bloco de aprovação, todos os três pausam, cada um com seu próprio ID de contexto. A execução retorna com todos os três pontos de pausa e seus links de retomada. Retomar qualquer um dispara continuação desse ponto específico.
Serialização de snapshot
O snapshot captura tudo necessário para retomar. Estados de blocos, logs de execução, escopos de loop e paralelo, decisões de roteamento, variáveis de workflow—tudo serializa para JSON. A peça crítica: arestas de entrada do DAG. Salvamos quais dependências cada nó ainda tem pendentes.
Quando você serializa o estado das arestas do DAG, está congelando o momento exato em que a execução pausou. Isso inclui loops parcialmente completos (iteração 7 de 100), ramos paralelos em voo (12 de 50 completos) e caminhos condicionais já podados.
Retomada e continuação
Retomar reconstrói o DAG, restaura o estado do snapshot e enfileira os nós acionadores de retomada. O executor marca blocos já executados para prevenir reexecução, restaura arestas de entrada para refletir dependências restantes e continua de onde parou.
Se múltiplos pontos de pausa existem, cada um pode retomar independentemente. A primeira retomada não invalida as outras—cada pausa tem seu próprio nó acionador no DAG. Quando todas as pausas retomam, o workflow continua normalmente, coletando saídas de cada ramo retomado.
Coordenação e atomicidade
O executor usa um bloqueio de fila para prevenir condições de corrida. Quando um nó completa com metadados de pausa, adquirimos o bloqueio antes de verificar pausas. Isso garante que múltiplos ramos pausando simultaneamente não interfiram na captura de estado uns dos outros.
O bloqueio também impede que um nó retomado corra com outros nós executando. Quando um acionador de retomada dispara, entra na fila como qualquer outro nó. O padrão de fila pronta lida com coordenação—nós retomados executam quando suas dependências são limpas, assim como nós na execução original.
Exemplo
Um padrão comum: agente gera saída, pausa para revisão humana, roteador decide aprovar/reprovar com base no feedback, salva em variável de workflow, e loop continua até aprovação.
Um loop while executa um agente com feedback anterior como contexto. A saída do agente vai para um bloco humano-no-loop, que pausa a execução e envia uma notificação. O usuário revisa a saída e fornece feedback via o link de retomada.
Quando retomado, o feedback flui para um roteador que avalia se a saída passa ou precisa de revisão. Se falha, o roteador salva o feedback em uma variável de workflow e roteia de volta para continuar o loop. O agente recebe esse feedback na próxima iteração e tenta novamente. Se passa, o roteador sai do loop e continua downstream.
A condição do loop while verifica a variável de workflow. Enquanto o status for "fail," o loop continua. Quando o roteador o define como "pass," o loop sai. Cada peça—loops, pausa/retomada, roteamento, variáveis—compõe sem cola porque são todos conceitos de primeira classe do executor.
Múltiplos revisores aprovando ramos diferentes funciona da mesma forma. Cada ramo pausa independentemente, revisores aprovam em qualquer ordem, e a execução continua conforme cada aprovação chega. O orquestrador paralelo coleta os resultados quando todos os ramos completam.
— Sid @ Sim

