<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Rogério Maciel</title>
    <description>The latest articles on DEV Community by Rogério Maciel (@rogeriomaciel).</description>
    <link>https://dev.to/rogeriomaciel</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4068148%2F3024387a-f6e8-409d-9de2-e8fbdd60039b.jpg</url>
      <title>DEV Community: Rogério Maciel</title>
      <link>https://dev.to/rogeriomaciel</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/rogeriomaciel"/>
    <language>en</language>
    <item>
      <title>From Visual Workflows to Native Code in Production: The Complete Journey of an n8n Backend That Couldn't Stop Evolving</title>
      <dc:creator>Rogério Maciel</dc:creator>
      <pubDate>Sun, 09 Aug 2026 14:54:01 +0000</pubDate>
      <link>https://dev.to/rogeriomaciel/from-visual-workflows-to-native-code-in-production-the-complete-journey-of-an-n8n-backend-that-508j</link>
      <guid>https://dev.to/rogeriomaciel/from-visual-workflows-to-native-code-in-production-the-complete-journey-of-an-n8n-backend-that-508j</guid>
      <description>&lt;p&gt;I'll tell this story in the order it happened. It doesn't start with a compiler. It starts with two n8n servers and a synchronization problem I had to solve before I could sleep peacefully.&lt;/p&gt;




&lt;h2&gt;
  
  
  Act 1 — The problem nobody mentions when they recommend n8n
&lt;/h2&gt;

&lt;p&gt;CoreAutoCRM is an auto repair shop management SaaS operating entirely via WhatsApp. AI customer service, scheduling, quotes, yard management, intelligent follow-ups—all of this lives in n8n workflows. I chose n8n for the obvious reason: speed. In a few months, I went from zero to a real production product, built entirely alone.&lt;/p&gt;

&lt;p&gt;n8n is great for that. The problem starts when you have a real product, with a real client, and you need two environments: one to test without fear of breaking what works, and another for what the client actually uses.&lt;/p&gt;

&lt;p&gt;Staging and production. Two separate n8n servers.&lt;/p&gt;

&lt;p&gt;And then comes the problem nobody mentions: &lt;strong&gt;how do you ensure that what you tested in staging is exactly what goes to production?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;In n8n, a workflow lives inside the server's database. It's not a file. It has no history. It has no diff. If you open the visual editor and change a &lt;code&gt;Switch&lt;/code&gt; condition, a SQL query parameter, or a node's AI prompt—that change exists only in that server's UI. There's no way to know what changed, who changed it, or when. There's no way to revert. There's no way to review before deploying to production.&lt;/p&gt;

&lt;p&gt;The "manual" way to sync staging with production would be: open each workflow in staging, export the JSON, open the same workflow in production, import the JSON. With 74 workflows, this is unfeasible. And even if it were, it would be a manual process prone to human error—the kind of thing that wakes you up at 2 AM because someone (you) forgot to sync a critical workflow.&lt;/p&gt;

&lt;p&gt;I needed a solution that:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Captured the current state of all n8n workflows as files&lt;/li&gt;
&lt;li&gt;Versioned these files in Git like any code&lt;/li&gt;
&lt;li&gt;Automated the deployment of approved changes directly to the production server&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This didn't exist out of the box. I built it from scratch.&lt;/p&gt;




&lt;h2&gt;
  
  
  Act 2 — GitOps for n8n: How I turned visual workflows into versionable code
&lt;/h2&gt;

&lt;p&gt;n8n's REST API has endpoints for everything: listing workflows, fetching a specific workflow's JSON, creating, updating, activating, deactivating. What I needed was an automation that used this API in both directions.&lt;/p&gt;

&lt;p&gt;I built a synchronization skill that does the following:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direction 1 — n8n → Git (download):&lt;/strong&gt;&lt;br&gt;
The automation connects to the staging n8n server via API, downloads each workflow's JSON, organizes them into a folder structure in the local project (&lt;code&gt;/agente/workflows/&lt;/code&gt;), and prepares a merge request with the changes. I can do this at any time—after a development cycle in the visual editor, when I want to "commit" the backend's current state.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direction 2 — Git → n8n (upload):&lt;/strong&gt;&lt;br&gt;
The reverse also works. I can edit a JSON directly in the repository—with AI assistance, since it's just structured text—and publish that change back to the n8n server. For quick tweaks, editing the JSON is sometimes faster than navigating the visual interface.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The CI/CD pipeline:&lt;/strong&gt;&lt;br&gt;
When a merge request is approved in Git, the CI/CD pipeline kicks in automatically. It uses the production n8n API to publish each changed workflow directly to the production server, with no manual intervention. What was tested in staging is exactly what hits production—because it's the exact same JSON, versioned, reviewed, and approved.&lt;/p&gt;

&lt;p&gt;The result was a setup I didn't expect to be so good: &lt;strong&gt;visual backend development with traditional software engineering discipline&lt;/strong&gt;. Each backend feature became a branch. Each change had a readable Git diff. Each deploy had full traceability. And all this without giving up the n8n visual editor, which remained the most productive tool for building and testing flows quickly.&lt;/p&gt;

&lt;p&gt;With this structure running, something important happened quietly: I now had 74 workflows—the entire product backend at the time—as structured, updated, versioned, and synced JSON files in the repository. The backend source code existed in a machine-readable format.&lt;/p&gt;

&lt;p&gt;I still didn't know what to do with this besides versioning. But I was about to find out.&lt;/p&gt;


&lt;h2&gt;
  
  
  Act 3 — The problem GitOps doesn't solve: Speed and scale
&lt;/h2&gt;

&lt;p&gt;The GitOps structure completely solved governance. I never lost trace of a change again. I never feared syncing staging with production again. The pipeline worked beautifully.&lt;/p&gt;

&lt;p&gt;Meanwhile, the product kept growing. The original 74 workflows became 126—new features, new AI modules, new operational flows. All versioned, all synced, all going through the same pipeline. The GitOps structure scaled naturally with the product's growth.&lt;/p&gt;

&lt;p&gt;But n8n was still the production runtime. And as the volume grew, the cost of that became more visible.&lt;/p&gt;

&lt;p&gt;Every n8n node serializes and deserializes the entire state between executions. It's the inherent cost of any visual orchestration engine that needs to be generic enough to serve everyone. Every subworkflow call—and I had many, because my n8n microservices architecture used subworkflows heavily—turned into an internal HTTP call with authentication, serialization, and transport overhead. Result: 180ms to 450ms average latency, 1.2 GB to 2.5 GB of RAM per instance.&lt;/p&gt;

&lt;p&gt;For a SaaS that handles real-time WhatsApp messages, this is a real problem. The feeling of "taking too long to respond" starts showing up in the product before you have enough volume to justify a traditional rewrite.&lt;/p&gt;

&lt;p&gt;The conventional options:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Rewrite in native code.&lt;/strong&gt; Six months of work, a complete halt on new features, a risky migration, and—most importantly—the end of the visual development speed that got me where I was. Discarded.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Scale horizontally with more n8n instances.&lt;/strong&gt; Multiplies an already high cost without solving per-request latency. Discarded.&lt;/p&gt;

&lt;p&gt;And then the realization that changed everything: &lt;strong&gt;I already had the 74 workflows as structured JSON in the repository&lt;/strong&gt;. If there is a program capable of reading these JSONs and understanding what each workflow does—what each node receives, processes, and outputs—that program can generate equivalent TypeScript code. Code that has no serialization between nodes. That makes no internal HTTP calls. That runs straight in the process, with no orchestration engine in the middle.&lt;/p&gt;

&lt;p&gt;The GitOps I built to solve versioning had inadvertently created the prerequisite for the next step: a compiler.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Compiler — And why every decision was forced by a real problem
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Problem 1: The order of nodes in the JSON is not the execution order
&lt;/h3&gt;

&lt;p&gt;The first version of the compiler followed the nodes in the order they appear in the JSON. It broke immediately.&lt;/p&gt;

&lt;p&gt;Visual workflows have no linear order. A JWT authentication node might appear at position 3 in the JSON, but needs to execute before the database node at position 1. An &lt;code&gt;If&lt;/code&gt; node triggers only one of two branches. A node receiving input from two different branches must wait for at least one of them to execute.&lt;/p&gt;

&lt;p&gt;Visual workflows are Directed Acyclic Graphs—DAGs. The only way to correctly resolve the execution order is with topological sorting.&lt;/p&gt;

&lt;p&gt;I used Kahn's Algorithm: start with nodes that have no predecessors (the triggers), process them, mark them as resolved, free the nodes that depended on them, repeat. The result is a linear queue ensuring every node only executes after all its predecessors have already executed.&lt;/p&gt;

&lt;p&gt;The compiler also identifies SINK nodes—nodes that end execution and return the HTTP response (&lt;code&gt;Respond to Webhook&lt;/code&gt;). These nodes are placed at the end of the topological queue, allowing the response to the client to be returned in less than 5ms while secondary asynchronous tasks continue in the background.&lt;/p&gt;
&lt;h3&gt;
  
  
  Problem 2: In-memory subworkflows and the silent tree-shaking bug
&lt;/h3&gt;

&lt;p&gt;My first implementation of subworkflows used standard ES Module imports. It worked in development. It broke silently in the production bundle.&lt;/p&gt;

&lt;p&gt;The problem is twofold.&lt;/p&gt;

&lt;p&gt;First: ES modules with relative paths of different depths can result in separate &lt;code&gt;Map&lt;/code&gt; instances in the same process. Two modules thinking they share the same registry might be talking to different registries with no visible errors.&lt;/p&gt;

&lt;p&gt;Second: bundlers like &lt;code&gt;bun build&lt;/code&gt; and &lt;code&gt;esbuild&lt;/code&gt; perform tree-shaking—removing code unreferenced in static analysis. Subworkflows called dynamically by name (&lt;code&gt;executeSubworkflow("COREAUTOCRM-PANEL-ACTION-GET-OS-DETAILS", ...)&lt;/code&gt;) are invisible to the bundler. The name is a string at runtime. The bundler doesn't know that string corresponds to a function. The bundle reached production without the subworkflows, and the dynamic calls failed silently.&lt;/p&gt;

&lt;p&gt;Solution for both problems at once: Global Singleton Registry tied to &lt;code&gt;globalThis&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__COREAUTO_WORKFLOWS_REGISTRY__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__COREAUTO_WORKFLOWS_REGISTRY__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflowsRegistry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__COREAUTO_WORKFLOWS_REGISTRY__&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;globalThis&lt;/code&gt; is guaranteed to be unique per process. To solve tree-shaking, the compiler automatically injects a static export referencing all 126 workflows, forcing the bundler to include them all in the bundle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 3: The expression parser was the hardest part
&lt;/h3&gt;

&lt;p&gt;n8n uses its own expression syntax: &lt;code&gt;={{ $json.body.osId }}&lt;/code&gt;, &lt;code&gt;={{ $('NodeName').item.json.field }}&lt;/code&gt;, &lt;code&gt;Text: {{ $json.name }}&lt;/code&gt;. Converting this to TypeScript has three distinct problems.&lt;/p&gt;

&lt;p&gt;First is safe chaining. &lt;code&gt;$json.user.store_id&lt;/code&gt; crashes with a &lt;code&gt;TypeError&lt;/code&gt; if &lt;code&gt;user&lt;/code&gt; is null. The compiler needs to convert to &lt;code&gt;item?.json?.user?.store_id&lt;/code&gt; for paths of arbitrary depth.&lt;/p&gt;

&lt;p&gt;Second is mixing text and expressions. &lt;code&gt;"Text: {{ $json.name }}"&lt;/code&gt; must become &lt;code&gt;`Text: ${item?.json?.name ?? ''}`&lt;/code&gt;—a template string with a fallback.&lt;/p&gt;

&lt;p&gt;Third, I didn't expect: AI prompts. The prompts feeding Gemini in the workflows contain Markdown code blocks with triple backticks. When the compiler places these prompts inside TypeScript template strings, the internal backticks break the syntax. The parser had to learn to identify these tokens and escape each internal backtick with &lt;code&gt;\&lt;/code&gt;`.&lt;/p&gt;

&lt;p&gt;The solution was a real tokenizer—not a regex over the entire text, but a parser that identifies the &lt;code&gt;{{&lt;/code&gt; and &lt;code&gt;}}&lt;/code&gt; delimiters, extracts the expression content, converts n8n's grammar to TypeScript, and reconstructs the text with proper escaping for each context.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problem 4: How to guarantee the generated code does the same as n8n
&lt;/h3&gt;

&lt;p&gt;Generating code that compiles is not enough. I need to guarantee that the generated code produces exactly the same result n8n would with the same data.&lt;/p&gt;

&lt;p&gt;The n8n API exposes the full execution history: &lt;code&gt;GET /api/v1/executions&lt;/code&gt;. For each execution, it returns the input payload, intermediate data for each node, and final output payload.&lt;/p&gt;

&lt;p&gt;The &lt;code&gt;TestGenerator&lt;/code&gt; uses this API as an oracle: I run a workflow in staging n8n with real data, the generator fetches this execution, captures input and output for each node, and generates a &lt;code&gt;.test.ts&lt;/code&gt; file that verifies the compiled TypeScript function produces an identical output, field by field.&lt;/p&gt;

&lt;p&gt;If any field diverges, the test fails, and the CI blocks the deploy. Staging n8n isn't just the visual IDE—it's the correctness oracle for everything going to production.&lt;/p&gt;




&lt;h2&gt;
  
  
  The numbers, without the marketing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Metric&lt;/th&gt;
&lt;th&gt;n8n on VPS&lt;/th&gt;
&lt;th&gt;Compiled Fastify&lt;/th&gt;
&lt;th&gt;Difference&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Average Latency&lt;/td&gt;
&lt;td&gt;180ms – 450ms&lt;/td&gt;
&lt;td&gt;3ms – 8ms&lt;/td&gt;
&lt;td&gt;~35x faster&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM per instance&lt;/td&gt;
&lt;td&gt;1.2 GB – 2.5 GB&lt;/td&gt;
&lt;td&gt;80 MB – 120 MB&lt;/td&gt;
&lt;td&gt;~95% less&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput per vCPU&lt;/td&gt;
&lt;td&gt;~120 req/s&lt;/td&gt;
&lt;td&gt;&amp;gt; 4,500 req/s&lt;/td&gt;
&lt;td&gt;~37x more scale&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Compile Time (126 workflows)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;77ms&lt;/td&gt;
&lt;td&gt;Instantaneous&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;The production bundle is 2.6 MB. It boots with PM2, sits behind Nginx, and is deployed automatically by CI when all tests pass.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this whole journey means
&lt;/h2&gt;

&lt;p&gt;Looking back, what happened was a sequence where each solved problem created the conditions for the next step:&lt;/p&gt;

&lt;p&gt;The problem of syncing two n8n servers forced me to build GitOps automation. GitOps gave me 74 workflows as versioned JSONs in the repo. The JSONs in the repo created the prerequisite for the compiler. The compiler turned those JSONs into native code with automatic correctness guarantees.&lt;/p&gt;

&lt;p&gt;None of these steps were planned from the start. Each solved a real problem and created, as a side effect, the infrastructure the next step needed.&lt;/p&gt;

&lt;p&gt;Today the full flow is:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;I develop visually in staging n8n—with all the speed Low-code offers&lt;/li&gt;
&lt;li&gt;The sync skill downloads workflows as JSON and opens a Git merge request&lt;/li&gt;
&lt;li&gt;CI/CD approves and deploys to production n8n—the operational environment keeps running&lt;/li&gt;
&lt;li&gt;In parallel, &lt;code&gt;bun run transpile&lt;/code&gt; compiles the JSONs into native TypeScript&lt;/li&gt;
&lt;li&gt;Auto-generated tests verify field-by-field equivalence with real n8n executions&lt;/li&gt;
&lt;li&gt;If they all pass, the Fastify bundle goes to the VPS&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;n8n remains the development tool. Fastify is the production runtime. The JSONs in the repo are the contract connecting the two—and the same contract that, since Act 2, gave me governance, traceability, and peace of mind.&lt;/p&gt;

&lt;p&gt;A lean startup isn't one that chooses between speed and quality. It's the one that builds the infrastructure to have both—and, with any luck, discovers that every solved problem was the stepping stone for the next.&lt;/p&gt;




&lt;p&gt;CoreAutoCRM is already running in production across dozens of auto shops in Brazil. I built this engineering for my own ecosystem, but I know the pain of scaling automations on rigid servers keeps many CTOs and founders awake at night.&lt;/p&gt;

&lt;p&gt;My main focus is the expansion of my SaaS, making my technical schedule almost non-existent. However, I've decided to open an exclusive time window this month to structure the architecture of only 2 operations that need to solve this exact problem: scale n8n to millions of requests, cut AWS/VPS costs by 90%, and drop latency to milliseconds.&lt;/p&gt;

&lt;p&gt;If your n8n is eating up your server's memory, if your automation crashes during peak hours, or if you've simply hit the Low-Code ceiling and don't want to rewrite everything from scratch, text me on WhatsApp (&lt;a href="https://wa.me/556296232227" rel="noopener noreferrer"&gt;https://wa.me/556296232227&lt;/a&gt;). Let's do a quick X-ray of your infrastructure and solve this.&lt;/p&gt;

</description>
      <category>node</category>
      <category>architecture</category>
      <category>typescript</category>
      <category>n8n</category>
    </item>
    <item>
      <title>De workflows visuais a código nativo em produção: a jornada completa de um backend n8n</title>
      <dc:creator>Rogério Maciel</dc:creator>
      <pubDate>Fri, 07 Aug 2026 23:11:03 +0000</pubDate>
      <link>https://dev.to/rogeriomaciel/de-workflows-visuais-a-codigo-nativo-em-producao-a-jornada-completa-de-um-backend-n8n-1ecb</link>
      <guid>https://dev.to/rogeriomaciel/de-workflows-visuais-a-codigo-nativo-em-producao-a-jornada-completa-de-um-backend-n8n-1ecb</guid>
      <description>&lt;p&gt;Vou contar essa história na ordem que aconteceu. Não começa com um compilador. Começa com dois servidores n8n e um problema de sincronização que eu precisava resolver antes de conseguir dormir tranquilo.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ato 1 — O problema que ninguém conta quando te recomendam o n8n
&lt;/h2&gt;

&lt;p&gt;O CoreAutoCRM é um SaaS de gestão de oficinas mecânicas que opera inteiramente pelo WhatsApp. Atendimento por IA, agendamento, orçamentos, gestão de pátio, follow-ups inteligentes — tudo isso vive em workflows do n8n. Escolhi o n8n pela razão óbvia: velocidade. Em meses fui do zero a um produto em produção real, construído sozinho.&lt;/p&gt;

&lt;p&gt;O n8n é ótimo para isso. O problema começa quando você tem um produto real, com um cliente real, e precisa de dois ambientes: um para testar sem medo de quebrar o que está funcionando, e outro para o que o cliente usa de verdade.&lt;/p&gt;

&lt;p&gt;Staging e produção. Dois servidores n8n separados.&lt;/p&gt;

&lt;p&gt;E aí aparece o problema que ninguém conta: &lt;strong&gt;como você garante que o que você testou em staging é exatamente o que vai para produção?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No n8n, um workflow vive dentro do banco de dados do servidor. Não é um arquivo. Não tem histórico. Não tem diff. Se você abre o editor visual e muda uma condição de um &lt;code&gt;Switch&lt;/code&gt;, um parâmetro de uma query SQL, ou o prompt de IA de um nó — essa mudança existe só na UI daquele servidor. Não há como saber o que mudou, quem mudou, ou quando. Não há como reverter. Não há como revisar antes de ir para produção.&lt;/p&gt;

&lt;p&gt;A forma "manual" de sincronizar staging com produção seria: abrir cada workflow no staging, exportar o JSON, abrir o mesmo workflow na produção, importar o JSON. Com 74 workflows, isso é inviável. E mesmo se fosse viável, seria um processo manual sujeito a erro humano — o tipo de coisa que te acorda às 2 da manhã porque alguém (você mesmo) esqueceu de sincronizar um workflow crítico.&lt;/p&gt;

&lt;p&gt;Precisava de uma solução que:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Capturasse o estado atual de todos os workflows do n8na como arquivos&lt;/li&gt;
&lt;li&gt;Versionasse esses arquivos no Git como qualquer código&lt;/li&gt;
&lt;li&gt;Automatizasse a publicação de alterações aprovadas diretamente no servidor de produção&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Não existia isso pronto. Construí do zero.&lt;/p&gt;




&lt;h2&gt;
  
  
  Ato 2 — GitOps para n8n: como transformei workflows visuais em código versionável
&lt;/h2&gt;

&lt;p&gt;A API REST do n8n tem endpoints para tudo: listar workflows, buscar pelo JSON de um workflow específico, criar, atualizar, ativar, desativar. O que eu precisava era de uma automação que usasse essa API nos dois sentidos.&lt;/p&gt;

&lt;p&gt;Construí uma skill de sincronização que faz o seguinte:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direção 1 — n8n → Git (download):&lt;/strong&gt;&lt;br&gt;
A automação se conecta ao servidor n8n de staging via API, baixa o JSON de cada workflow, organiza em uma estrutura de pastas no projeto local (&lt;code&gt;/agente/workflows/&lt;/code&gt;), e prepara um merge request com as alterações. Posso fazer isso a qualquer momento — depois de um ciclo de desenvolvimento no editor visual, quando quero "commitar" o estado atual do backend.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Direção 2 — Git → n8n (upload):&lt;/strong&gt;&lt;br&gt;
O inverso também funciona. Posso editar um JSON diretamente no repositório — com assistência de IA, já que é só texto estruturado — e publicar essa alteração de volta no servidor n8n. Para ajustes pontuais, às vezes é mais rápido editar o JSON do que navegar na interface visual.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;O pipeline de CI/CD:&lt;/strong&gt;&lt;br&gt;
Quando um merge request é aprovado no Git, o pipeline de CI/CD entra em ação automaticamente. Ele usa a API do n8n de produção para publicar cada workflow alterado diretamente no servidor de produção, sem intervenção manual. O que foi testado em staging é exatamente o que chega à produção — porque é o mesmo JSON, versionado, revisado, aprovado.&lt;/p&gt;

&lt;p&gt;O resultado foi uma estrutura que eu não esperava que ficasse tão boa: &lt;strong&gt;desenvolvimento de backend visual com disciplina de engenharia de software tradicional&lt;/strong&gt;. Cada feature de backend virava um branch. Cada alteração tinha diff legível no Git. Cada deploy tinha rastreabilidade completa. E tudo isso sem abrir mão do editor visual do n8n, que continuava sendo a ferramenta mais produtiva para construir e testar fluxos rapidamente.&lt;/p&gt;

&lt;p&gt;Com essa estrutura funcionando, algo importante aconteceu silenciosamente: eu passei a ter 74 workflows — todo o backend do produto naquele momento — como arquivos JSON estruturados, atualizados, versionados, e sincronizados no repositório. O código-fonte do backend existia em formato legível por máquina.&lt;/p&gt;

&lt;p&gt;Eu ainda não sabia o que fazer com isso além de versionamento. Mas estava prestes a descobrir.&lt;/p&gt;


&lt;h2&gt;
  
  
  Ato 3 — O problema que o GitOps não resolve: velocidade e escala
&lt;/h2&gt;

&lt;p&gt;A estrutura de GitOps resolveu governança completamente. Nunca mais perdi rastreabilidade de uma alteração. Nunca mais tive medo de sincronizar staging com produção. O pipeline funcionava lindamente.&lt;/p&gt;

&lt;p&gt;Enquanto isso, o produto continuou crescendo. Os 74 workflows originais viraram 126 — novas features, novos módulos de IA, novos fluxos operacionais. Tudo versionado, tudo sincronizado, tudo passando pelo mesmo pipeline. A estrutura de GitOps escalonava naturalmente com o crescimento do produto.&lt;/p&gt;

&lt;p&gt;Mas o n8n continuava sendo o runtime de produção. E conforme o volume cresceu, o custo disso ficou mais visível.&lt;/p&gt;

&lt;p&gt;Cada nó do n8n serializa e desserializa o estado completo entre execuções. É o custo inerente de qualquer engine de orquestração visual que precisa ser genérico o suficiente para servir a todo mundo. Cada chamada de subworkflow — e eu tinha muitas, porque minha arquitetura de microsserviços no n8n usava subworkflows ativamente — virava uma chamada HTTP interna com autenticação, serialização e overhead de transporte. Resultado: 180ms a 450ms de latência média, 1,2 GB a 2,5 GB de RAM por instância.&lt;/p&gt;

&lt;p&gt;Para um SaaS que atende mensagens de WhatsApp em tempo real, isso é um problema real. A sensação de "demora para responder" começa a aparecer no produto antes de você ter volume suficiente para justificar uma reescrita tradicional.&lt;/p&gt;

&lt;p&gt;As opções convencionais:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Reescrever em código nativo.&lt;/strong&gt; Seis meses de trabalho, pausa total em novas features, migração arriscada, e — mais importante — fim da velocidade de desenvolvimento visual que me deixou chegar onde cheguei. Descartei.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Escalar horizontalmente com mais instâncias de n8n.&lt;/strong&gt; Multiplica um custo que já é alto, sem resolver a latência por requisição. Descartei.&lt;/p&gt;

&lt;p&gt;E então a percepção que mudou tudo: &lt;strong&gt;eu já tinha os 74 workflows como JSON estruturado no repositório&lt;/strong&gt;. Se existe um programa capaz de ler esses JSONs e entender o que cada workflow faz — o que cada nó recebe, processa e produz — esse programa pode gerar código TypeScript equivalente. Código que não tem serialização entre nós. Que não faz chamadas HTTP internas. Que roda direto no processo, sem engine de orquestração no meio.&lt;/p&gt;

&lt;p&gt;O GitOps que eu construí para resolver versionamento havia, sem querer, criado o pré-requisito para o próximo passo: um compilador.&lt;/p&gt;


&lt;h2&gt;
  
  
  O compilador — e por que cada decisão foi forçada por um problema real
&lt;/h2&gt;
&lt;h3&gt;
  
  
  Problema 1: a ordem dos nós no JSON não é a ordem de execução
&lt;/h3&gt;

&lt;p&gt;A primeira versão do compilador seguia os nós na ordem em que aparecem no JSON. Quebrou imediatamente.&lt;/p&gt;

&lt;p&gt;Workflows visuais não têm ordem linear. Um nó de autenticação JWT pode aparecer na posição 3 no JSON, mas precisa executar antes do nó de banco de dados na posição 1. Um nó &lt;code&gt;If&lt;/code&gt; ativa apenas um dos dois ramos. Um nó que recebe entrada de dois ramos diferentes precisa esperar que pelo menos um deles tenha executado.&lt;/p&gt;

&lt;p&gt;Workflows visuais são Grafos Direcionados Acíclicos — DAGs. A única forma de resolver a ordem de execução corretamente é com ordenação topológica.&lt;/p&gt;

&lt;p&gt;Usei o Algoritmo de Kahn: começa pelos nós sem predecessores (os triggers), processa-os, marca-os como resolvidos, libera os nós que dependiam deles, repete. O resultado é uma fila linear que garante que todo nó só execute depois que todos os seus predecessores já executaram.&lt;/p&gt;

&lt;p&gt;O compilador também identifica nós SINK — nós que terminam a execução e devolvem a resposta HTTP (&lt;code&gt;Respond to Webhook&lt;/code&gt;). Esses nós ficam no final da fila topológica, permitindo que a resposta ao cliente seja devolvida em menos de 5ms enquanto tarefas assíncronas secundárias continuam em background.&lt;/p&gt;
&lt;h3&gt;
  
  
  Problema 2: subworkflows em memória e o bug silencioso do tree-shaking
&lt;/h3&gt;

&lt;p&gt;Minha primeira implementação de subworkflows usava importações ES Module normais. Funcionou no desenvolvimento. Quebrou silenciosamente no bundle de produção.&lt;/p&gt;

&lt;p&gt;O problema tem duas partes.&lt;/p&gt;

&lt;p&gt;Primeiro: módulos ES com caminhos relativos de profundidades diferentes podem resultar em instâncias de &lt;code&gt;Map&lt;/code&gt; separadas no mesmo processo. Dois módulos que acham que compartilham o mesmo registry podem estar falando com registries diferentes sem nenhum erro visível.&lt;/p&gt;

&lt;p&gt;Segundo: bundlers como &lt;code&gt;bun build&lt;/code&gt; e &lt;code&gt;esbuild&lt;/code&gt; fazem tree-shaking — removem código não referenciado na análise estática. Subworkflows chamados dinamicamente pelo nome (&lt;code&gt;executeSubworkflow("COREAUTOCRM-PANEL-ACTION-GET-OS-DETAILS", ...)&lt;/code&gt;) são invisíveis para o bundler. O nome é uma string em runtime. O bundler não sabe que aquela string corresponde a uma função. O bundle chegava à produção sem os subworkflows, e as chamadas dinâmicas falhavam silenciosamente.&lt;/p&gt;

&lt;p&gt;Solução para os dois problemas ao mesmo tempo: Global Singleton Registry atrelado ao &lt;code&gt;globalThis&lt;/code&gt;.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;globalThis&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__COREAUTO_WORKFLOWS_REGISTRY__&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__COREAUTO_WORKFLOWS_REGISTRY__&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nb"&gt;Map&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nb"&gt;Function&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;workflowsRegistry&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;g&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;__COREAUTO_WORKFLOWS_REGISTRY__&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;globalThis&lt;/code&gt; é garantidamente único por processo. Para resolver o tree-shaking, o compilador injeta automaticamente uma exportação estática que referencia todos os 126 workflows, forçando o bundler a incluir todos no bundle.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problema 3: o parser de expressões foi o mais trabalhoso
&lt;/h3&gt;

&lt;p&gt;O n8n usa uma sintaxe de expressão própria: &lt;code&gt;={{ $json.body.osId }}&lt;/code&gt;, &lt;code&gt;={{ $('NomeDoNo').item.json.campo }}&lt;/code&gt;, &lt;code&gt;Texto: {{ $json.nome }}&lt;/code&gt;. Converter isso para TypeScript tem três problemas distintos.&lt;/p&gt;

&lt;p&gt;O primeiro é safe chaining. &lt;code&gt;$json.user.loja_id&lt;/code&gt; quebra com &lt;code&gt;TypeError&lt;/code&gt; se &lt;code&gt;user&lt;/code&gt; for nulo. O compilador precisa converter para &lt;code&gt;item?.json?.user?.loja_id&lt;/code&gt; para caminhos de profundidade arbitrária.&lt;/p&gt;

&lt;p&gt;O segundo é mistura de texto e expressão. &lt;code&gt;"Texto: {{ $json.nome }}"&lt;/code&gt; precisa virar &lt;code&gt;`Texto: ${item?.json?.nome ?? ''}`&lt;/code&gt; — template string com fallback.&lt;/p&gt;

&lt;p&gt;O terceiro eu não esperava: os prompts de IA. Os prompts que alimentam o Gemini nos workflows contêm blocos de código Markdown com crases triplas (&lt;code&gt;&lt;/code&gt;&lt;code&gt;json `). Quando o compilador coloca esses prompts dentro de template strings TypeScript, as crases internas quebram a sintaxe. O parser precisou aprender a identificar esses tokens e escapar cada crase interna com `\&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A solução foi um tokenizador real — não regex sobre o texto inteiro, mas um parser que identifica os delimitadores &lt;code&gt;{{&lt;/code&gt; e &lt;code&gt;}}&lt;/code&gt;, extrai o conteúdo de expressão, converte a gramática do n8n para TypeScript, e reconstrói o texto com escaping adequado para cada contexto.&lt;/p&gt;

&lt;h3&gt;
  
  
  Problema 4: como garantir que o código gerado faz o mesmo que o n8n
&lt;/h3&gt;

&lt;p&gt;Gerar código que compila não é suficiente. Preciso garantir que o código gerado produz exatamente o mesmo resultado que o n8n produziria com os mesmos dados.&lt;/p&gt;

&lt;p&gt;A API do n8n expõe o histórico completo de execuções: &lt;code&gt;GET /api/v1/executions&lt;/code&gt;. Para cada execução, ela retorna payload de entrada, dados intermediários de cada nó, e payload de saída final.&lt;/p&gt;

&lt;p&gt;O &lt;code&gt;TestGenerator&lt;/code&gt; usa essa API como oráculo: executo um workflow no n8n de staging com dados reais, o gerador busca essa execução, captura input e output de cada nó, e gera um arquivo &lt;code&gt;.test.ts&lt;/code&gt; que verifica que a função TypeScript compilada produz output idêntico, campo a campo.&lt;/p&gt;

&lt;p&gt;Se qualquer campo divergir, o teste falha e o CI bloqueia o deploy. O n8n de staging não é só a IDE visual — é o oráculo de correctitude de tudo que vai para produção.&lt;/p&gt;




&lt;h2&gt;
  
  
  Os números, sem marketing
&lt;/h2&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Métrica&lt;/th&gt;
&lt;th&gt;n8n em VPS&lt;/th&gt;
&lt;th&gt;Fastify compilado&lt;/th&gt;
&lt;th&gt;Diferença&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Latência média&lt;/td&gt;
&lt;td&gt;180ms – 450ms&lt;/td&gt;
&lt;td&gt;3ms – 8ms&lt;/td&gt;
&lt;td&gt;~35x mais rápido&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;RAM por instância&lt;/td&gt;
&lt;td&gt;1,2 GB – 2,5 GB&lt;/td&gt;
&lt;td&gt;80 MB – 120 MB&lt;/td&gt;
&lt;td&gt;~95% menos&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Throughput por vCPU&lt;/td&gt;
&lt;td&gt;~120 req/s&lt;/td&gt;
&lt;td&gt;&amp;gt; 4.500 req/s&lt;/td&gt;
&lt;td&gt;~37x mais escala&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Tempo de compilação (126 workflows)&lt;/td&gt;
&lt;td&gt;—&lt;/td&gt;
&lt;td&gt;77ms&lt;/td&gt;
&lt;td&gt;Instantâneo&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;O bundle de produção tem 2,6 MB. Sobe com PM2, fica atrás de Nginx, deploy automatizado pelo CI quando todos os testes passam.&lt;/p&gt;




&lt;h2&gt;
  
  
  O que essa jornada inteira significa
&lt;/h2&gt;

&lt;p&gt;Olhando para trás, o que aconteceu foi uma sequência onde cada problema resolvido criou as condições para o próximo passo:&lt;/p&gt;

&lt;p&gt;O problema de sincronizar dois servidores n8n me forçou a construir a automação de GitOps. O GitOps me deu 74 workflows como JSONs versionados no repositório. Os JSONs no repositório criaram o pré-requisito para o compilador. O compilador transformou esses JSONs em código nativo com garantia automática de correctitude.&lt;/p&gt;

&lt;p&gt;Nenhum desses passos foi planejado desde o início. Cada um resolveu um problema real e criou, como efeito colateral, a infraestrutura que o próximo passo precisava.&lt;/p&gt;

&lt;p&gt;Hoje o fluxo completo é:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Desenvolvo visualmente no n8n de staging — com toda a velocidade que o Low-code oferece&lt;/li&gt;
&lt;li&gt;A skill de sincronização baixa os workflows como JSON e abre um merge request no Git&lt;/li&gt;
&lt;li&gt;O CI/CD aprova e publica em produção no n8n — o ambiente operacional continua funcionando&lt;/li&gt;
&lt;li&gt;Em paralelo, &lt;code&gt;bun run transpilar&lt;/code&gt; compila os JSONs em TypeScript nativo&lt;/li&gt;
&lt;li&gt;Os testes gerados automaticamente verificam equivalência campo a campo com execuções reais do n8n&lt;/li&gt;
&lt;li&gt;Se todos passam, o bundle Fastify vai para a VPS&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;O n8n continua sendo a ferramenta de desenvolvimento. O Fastify é o runtime de produção. Os JSONs no repositório são o contrato que conecta os dois — e o mesmo contrato que, desde o Ato 2, me deu governança, rastreabilidade e paz de espírito.&lt;/p&gt;

&lt;p&gt;Startup enxuta não é aquela que escolhe entre velocidade e qualidade. É a que constrói a infraestrutura para ter as duas — e, com sorte, descobre que cada problema resolvido era o degrau para o próximo.&lt;/p&gt;




&lt;p&gt;O CoreAutoCRM já roda em produção em dezenas de oficinas pelo Brasil. Eu construí essa engenharia para o meu próprio ecossistema, mas sei que a dor de escalar automações em servidores engessados tira o sono de muito CTO e fundador por aí.&lt;/p&gt;

&lt;p&gt;O meu foco principal é a expansão do meu SaaS, o que torna minha agenda técnica quase inexistente. No entanto, decidi abrir uma janela de tempo exclusiva neste mês para estruturar a arquitetura de apenas 2 operações que precisam resolver esse exato problema: escalar o n8n para milhões de requisições, reduzir o custo de AWS/VPS em 90% e derrubar a latência para a casa dos milissegundos.&lt;/p&gt;

&lt;p&gt;Se o seu n8n está consumindo a memória do seu servidor, se a sua automação trava em horário de pico, ou se você simplesmente bateu no teto do Low-Code e não quer reescrever tudo do zero, me chame no whatsapp (&lt;a href="https://wa.me/556296232227" rel="noopener noreferrer"&gt;https://wa.me/556296232227&lt;/a&gt;). Vamos fazer um raio-x rápido da sua infraestrutura e resolver isso.&lt;/p&gt;

</description>
      <category>node</category>
      <category>architecture</category>
      <category>fastify</category>
      <category>n8nbrightdatachallenge</category>
    </item>
  </channel>
</rss>
