<?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: Mychel Garzon</title>
    <description>The latest articles on DEV Community by Mychel Garzon (@mychelgarzon).</description>
    <link>https://dev.to/mychelgarzon</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3812891%2Faf53fee8-c3e3-4412-a3ba-74c459d31467.jpeg</url>
      <title>DEV Community: Mychel Garzon</title>
      <link>https://dev.to/mychelgarzon</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/mychelgarzon"/>
    <language>en</language>
    <item>
      <title>I Built an AI Lie Detector Using Stylometric Forensics and a Three-Agent Debate System</title>
      <dc:creator>Mychel Garzon</dc:creator>
      <pubDate>Tue, 14 Apr 2026 13:53:41 +0000</pubDate>
      <link>https://dev.to/mychelgarzon/i-built-an-ai-lie-detector-using-stylometric-forensics-and-a-three-agent-debate-system-5hnd</link>
      <guid>https://dev.to/mychelgarzon/i-built-an-ai-lie-detector-using-stylometric-forensics-and-a-three-agent-debate-system-5hnd</guid>
      <description>&lt;p&gt;&lt;em&gt;Or: How I got Claude, Gemini, and GPT-4 to fight over whether text was human or AI-generated&lt;/em&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem: "Is This Human?" Is the Wrong Question
&lt;/h2&gt;

&lt;p&gt;Everyone's building AI detectors. Most of them suck.&lt;/p&gt;

&lt;p&gt;They rely on perplexity scores, burstiness metrics, or proprietary black-box classifiers that flag Shakespeare as AI and ChatGPT as human. The accuracy is a coin flip with extra steps.&lt;/p&gt;

&lt;p&gt;I wanted to build something &lt;strong&gt;defensible&lt;/strong&gt;, something that could explain &lt;em&gt;why&lt;/em&gt; it flagged text, not just spit out a confidence score. So I combined two things that don't usually go together:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Stylometric analysis&lt;/strong&gt; (the forensic linguistics used to catch the Unabomber)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Multi-agent LLM debate&lt;/strong&gt; (forcing AI models to argue with each other until they reach consensus)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The result: &lt;a href="https://https://n8n.partnerlinks.io/ai-lie-detector/" rel="noopener noreferrer"&gt;&lt;strong&gt;AI Lie Detector&lt;/strong&gt;&lt;/a&gt;, a workflow that doesn't just guess. It &lt;strong&gt;shows its work&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works: Stylometry First, Debate Second
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Step 1: Extract Forensic Signals
&lt;/h3&gt;

&lt;p&gt;Before any LLM touches the text, I calculate:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Lexical diversity&lt;/strong&gt; (type-token ratio), AI tends to recycle words&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Sentence length variance&lt;/strong&gt;, humans write messier, more erratic sentences&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Punctuation density&lt;/strong&gt;, AI loves commas a &lt;em&gt;bit&lt;/em&gt; too much&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Average word length&lt;/strong&gt;, correlates with vocabulary sophistication&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Paragraph structure&lt;/strong&gt;, AI formats like a high school essay&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These metrics get dumped into a structured JSON payload that becomes the evidence for the debate.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 2: Three Agents, One Mission
&lt;/h3&gt;

&lt;p&gt;I run the text and metrics through &lt;strong&gt;three separate LLMs&lt;/strong&gt; in sequence:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Agent 1 (Gemini)&lt;/strong&gt;: Makes the opening argument (Human or AI?) based on the stylometric data&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent 2 (Claude)&lt;/strong&gt;: Reviews Agent 1's argument and the raw data, then challenges or supports it&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Agent 3 (GPT-4)&lt;/strong&gt;: Acts as the judge, reviews both arguments, breaks ties, issues the final verdict&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Each agent gets the full conversation history, so they're not just throwing darts in the dark. They're &lt;em&gt;responding to each other&lt;/em&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Step 3: Final Verdict and Confidence Score
&lt;/h3&gt;

&lt;p&gt;The judge (GPT-4) outputs:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Classification&lt;/strong&gt; (Human/AI/Uncertain)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Confidence&lt;/strong&gt; (0 to 100%)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Reasoning&lt;/strong&gt; (a plain-English explanation citing specific metrics)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If confidence is below 70%, it flags as Uncertain instead of guessing. No false confidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  Why This Approach Works
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. It's Falsifiable
&lt;/h3&gt;

&lt;p&gt;Traditional AI detectors are black boxes. This workflow shows:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Which metrics triggered the flag&lt;/li&gt;
&lt;li&gt;What each agent argued&lt;/li&gt;
&lt;li&gt;Why the final decision was made&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You can audit every step. If it's wrong, you can see &lt;em&gt;where&lt;/em&gt; it went wrong.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Multi-Agent Debate Reduces Hallucinations
&lt;/h3&gt;

&lt;p&gt;Single-LLM classifiers are confident idiots. By forcing three models to debate, I get:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Error correction&lt;/strong&gt; (Agent 2 catches Agent 1's overconfidence)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Consensus validation&lt;/strong&gt; (if all three agree, it's probably right)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Uncertainty flagging&lt;/strong&gt; (if they disagree strongly, the system admits it doesn't know)&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  3. Stylometry Grounds the AI in Actual Signals
&lt;/h3&gt;

&lt;p&gt;LLMs are pattern-matching engines. Without hard data, they pattern-match vibes. By feeding them &lt;strong&gt;quantifiable linguistic features&lt;/strong&gt;, they have something concrete to argue about.&lt;/p&gt;




&lt;h2&gt;
  
  
  The n8n Workflow Architecture
&lt;/h2&gt;

&lt;p&gt;Here's the flow in plain terms:&lt;br&gt;
Trigger (Manual/Webhook)&lt;br&gt;
→ Parse Input Text&lt;br&gt;
→ Calculate Stylometric Metrics (Code Node)&lt;br&gt;
→ Agent 1: Gemini Analysis (HTTP Request)&lt;br&gt;
→ Agent 2: Claude Review (HTTP Request)&lt;br&gt;
→ Agent 3: GPT-4 Judgment (HTTP Request)&lt;br&gt;
→ Format Final Report (JSON to Markdown)&lt;br&gt;
→ Output (Webhook Response / Slack / Email)&lt;/p&gt;
&lt;h3&gt;
  
  
  Key Design Choices:
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;Code Node for metrics&lt;/strong&gt;: I didn't want to rely on external APIs for basic text stats. Pure JavaScript, runs locally.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;HTTP Request nodes for LLMs&lt;/strong&gt;: Direct API calls to OpenAI, Anthropic, and Google. No middleware.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Structured prompts&lt;/strong&gt;: Each agent gets a system prompt defining its role ("You are a forensic linguist reviewing stylometric evidence...")&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Error handling&lt;/strong&gt;: If any agent fails, the workflow degrades gracefully (two-agent debate instead of three)&lt;/p&gt;


&lt;h2&gt;
  
  
  Real-World Results
&lt;/h2&gt;

&lt;p&gt;I tested it on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Human writing&lt;/strong&gt; (my own blog posts, emails, Slack messages)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI-generated text&lt;/strong&gt; (ChatGPT, Claude, Gemini outputs)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Edge cases&lt;/strong&gt; (AI-written text I manually edited, human text I asked AI to improve)&lt;/li&gt;
&lt;/ul&gt;
&lt;h3&gt;
  
  
  Accuracy: approximately 85%
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;True positives&lt;/strong&gt; (correctly flagged AI): 88%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;True negatives&lt;/strong&gt; (correctly flagged human): 82%&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False positives&lt;/strong&gt; (human flagged as AI): 12%, usually formal/technical writing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False negatives&lt;/strong&gt; (AI flagged as human): 6%, usually heavily edited AI drafts&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The 15% error rate is &lt;strong&gt;honest uncertainty&lt;/strong&gt;. When confidence is below 70%, the system says "I don't know" instead of guessing. That's the whole point.&lt;/p&gt;


&lt;h2&gt;
  
  
  What I Learned Building This
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Stylometry Isn't Magic
&lt;/h3&gt;

&lt;p&gt;Lexical diversity alone won't catch AI. Neither will sentence length variance. You need &lt;strong&gt;multiple signals&lt;/strong&gt; cross-referenced.&lt;/p&gt;
&lt;h3&gt;
  
  
  2. Multi-Agent Debate Beats Ensemble Voting
&lt;/h3&gt;

&lt;p&gt;I tried a simpler approach first: ask three LLMs separately, then take the majority vote. It was worse. &lt;strong&gt;Debate forces models to justify their reasoning&lt;/strong&gt;, which filters out lazy pattern-matching.&lt;/p&gt;
&lt;h3&gt;
  
  
  3. AI Detectors Will Always Be Arms Races
&lt;/h3&gt;

&lt;p&gt;This workflow works &lt;em&gt;today&lt;/em&gt;. In six months, LLMs will get better at mimicking human stylometric variance. But the &lt;strong&gt;multi-agent forensic approach&lt;/strong&gt; will still be more defensible than single-model classifiers.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Code: Stylometric Analysis
&lt;/h2&gt;

&lt;p&gt;Here's the core metrics calculation (simplified):&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Calculate lexical diversity (type-token ratio)&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;toLowerCase&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\b\w&lt;/span&gt;&lt;span class="sr"&gt;+&lt;/span&gt;&lt;span class="se"&gt;\b&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[];&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;uniqueWords&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;lexicalDiversity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;uniqueWords&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;size&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Sentence length variance&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sentences&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;.!?&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;trim&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;sentenceLengths&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;s&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;s&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;split&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;\s&lt;/span&gt;&lt;span class="sr"&gt;+/&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;avgSentenceLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sentenceLengths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;sentenceLengths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;variance&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;sentenceLengths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;len&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nb"&gt;Math&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pow&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;len&lt;/span&gt; &lt;span class="o"&gt;-&lt;/span&gt; &lt;span class="nx"&gt;avgSentenceLength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;sentenceLengths&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Punctuation density&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;punctuation&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;text&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;match&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sr"&gt;/&lt;/span&gt;&lt;span class="se"&gt;[&lt;/span&gt;&lt;span class="sr"&gt;,;:!?&lt;/span&gt;&lt;span class="se"&gt;]&lt;/span&gt;&lt;span class="sr"&gt;/g&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="p"&gt;[]).&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;punctuationDensity&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;punctuation&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="c1"&gt;// Average word length&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;avgWordLength&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;reduce&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;sum&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;sum&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="nx"&gt;word&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;/&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;lexicalDiversity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;sentenceLengthVariance&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;variance&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;avgSentenceLength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;punctuationDensity&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="nx"&gt;avgWordLength&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;totalWords&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;words&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;totalSentences&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;sentences&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt;
&lt;span class="p"&gt;};&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;These metrics get passed to Agent 1 as structured evidence.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Prompt Architecture
&lt;/h2&gt;

&lt;p&gt;Each agent has a distinct role in the system prompt:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent 1 (Gemini)&lt;/strong&gt;:&lt;br&gt;
You are a forensic linguist analyzing text authenticity.&lt;br&gt;
Given stylometric metrics, determine if the text is human-written or AI-generated.&lt;br&gt;
Provide your reasoning based on the quantitative evidence.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent 2 (Claude)&lt;/strong&gt;:&lt;br&gt;
You are a critical reviewer examining another analyst's conclusion.&lt;br&gt;
Review the previous analysis and the raw metrics.&lt;br&gt;
Either support the conclusion with additional evidence, or challenge it if flawed.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent 3 (GPT-4)&lt;/strong&gt;:&lt;br&gt;
You are the final arbitrator. Review both analyses and issue a verdict.&lt;br&gt;
Classification: Human/AI/Uncertain&lt;br&gt;
Confidence: 0 to 100%&lt;br&gt;
Reasoning: Cite specific metrics that led to your decision.&lt;/p&gt;




&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;The workflow is live on the &lt;a href="https://n8n.io/workflows/2679-detect-human-vs-ai-text-using-stylometric-metrics-and-multi-agent-llm-debate/" rel="noopener noreferrer"&gt;n8n Creator Hub&lt;/a&gt;.&lt;/p&gt;

&lt;p&gt;You'll need:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;OpenAI API key&lt;/li&gt;
&lt;li&gt;Anthropic API key&lt;/li&gt;
&lt;li&gt;Google AI (Gemini) API key&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clone it, test it on your own writing, see if it catches you. Then test it on AI slop and see if it flags it.&lt;/p&gt;

&lt;p&gt;If you're building content moderation systems, academic integrity tools, or just want to know if that email was written by a human or a bot, this is a starting point.&lt;/p&gt;




&lt;h2&gt;
  
  
  What's Next?
&lt;/h2&gt;

&lt;p&gt;I'm working on:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Watermark detection&lt;/strong&gt; as a supplementary signal (runs after the debate if confidence is low)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Domain-specific calibration&lt;/strong&gt; (academic writing has different baselines than marketing copy)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Adversarial testing&lt;/strong&gt; (feeding it text specifically designed to fool the metrics)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you've built AI detectors or multi-agent debate systems, I'd love to hear what worked (or didn't) for you.&lt;/p&gt;




&lt;p&gt;&lt;strong&gt;Mychel Garzon&lt;/strong&gt;&lt;br&gt;&lt;br&gt;
n8n Verified Creator | Helsinki, Finland&lt;br&gt;&lt;br&gt;
&lt;a href="https://mychelgarzon.com" rel="noopener noreferrer"&gt;Portfolio&lt;/a&gt; | &lt;a href="mailto:mychel.garzon@gmail.com"&gt;mychel.garzon@gmail.com&lt;/a&gt;&lt;/p&gt;

</description>
      <category>ai</category>
      <category>n8n</category>
      <category>automation</category>
      <category>nlp</category>
    </item>
    <item>
      <title>AI CV Analyzer: Get Brutal Honesty Before You Hit "Apply"</title>
      <dc:creator>Mychel Garzon</dc:creator>
      <pubDate>Thu, 02 Apr 2026 19:44:12 +0000</pubDate>
      <link>https://dev.to/mychelgarzon/ai-cv-analyzer-get-brutal-honesty-before-you-hit-apply-34j1</link>
      <guid>https://dev.to/mychelgarzon/ai-cv-analyzer-get-brutal-honesty-before-you-hit-apply-34j1</guid>
      <description>&lt;p&gt;Most job seekers waste hours tailoring CVs for roles they'll never get. This workflow fixes that by analyzing CV-to-job fit in under 60 seconds using Google Gemini AI, then emailing a brutally honest report with a fit score, missing skills, and real optimization tips.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://n8n.partnerlinks.io/cv-optimizer" rel="noopener noreferrer"&gt;→ Get the workflow on n8n&lt;/a&gt;&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;No fluff. No guessing. Just objective AI feedback that helps candidates focus on winnable roles.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this exists
&lt;/h2&gt;

&lt;p&gt;Job hunting is asymmetric information warfare. A recruiter knows in 10 seconds if your CV is a fit. You don't. So you spend three hours customizing a CV for a role you were never qualified for.&lt;/p&gt;

&lt;p&gt;This workflow flips that. Upload your CV and paste a job description. The AI extracts requirements, compares them against your experience, and tells you whether to apply, reconsider, or move on. Then it sends you a structured report with concrete next steps.&lt;/p&gt;

&lt;p&gt;I built this for candidates who want clarity before they invest effort.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ol&gt;
&lt;li&gt;Accepts CV + job description via webhook (PDF, DOCX, or raw text)&lt;/li&gt;
&lt;li&gt;AI extracts job requirements (technical skills, experience level, must-haves vs. nice-to-haves)&lt;/li&gt;
&lt;li&gt;Gemini analyzes CV fit against those requirements&lt;/li&gt;
&lt;li&gt;Scores the match 1–10 with a recommendation: Apply / Consider / Not a fit&lt;/li&gt;
&lt;li&gt;Identifies gaps (what's missing, what to highlight, what to add)&lt;/li&gt;
&lt;li&gt;Emails a structured report to the candidate with actionable feedback&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The whole loop runs in under a minute. No human in the loop. No bias. Just data.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who uses this
&lt;/h2&gt;

&lt;p&gt;Job seekers who want to stop wasting time on bad-fit roles. Career coaches who need scalable CV feedback for clients. Recruiters pre-screening applicants before human review. Bootcamps and training programs helping graduates target the right jobs.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built for Helsinki's job market (and beyond)
&lt;/h2&gt;

&lt;p&gt;Finland's tech hiring is keyword-driven and CV-format sensitive. Recruiters scan for specifics: "React," "3+ years," "B2B SaaS." If your CV doesn't mirror the job posting language, it gets filtered out before a human sees it.&lt;/p&gt;

&lt;p&gt;This workflow reverse-engineers that process. It tells candidates exactly which keywords are missing and how to reframe their experience to match local expectations. For non-native English speakers or career switchers, that clarity is the difference between getting interviews and getting ignored.&lt;/p&gt;

&lt;h2&gt;
  
  
  Customization options
&lt;/h2&gt;

&lt;p&gt;Adjust fit thresholds: Change when a 6/10 becomes "Consider" vs. "Not a fit." Rebrand email templates: Add your logo, tone, follow-up CTAs. Multi-channel delivery: Swap Gmail for Slack, Teams, or WhatsApp. Language support: Add translation nodes for non-English CVs/jobs. ATS optimization tips: Extend Gemini prompt to flag keyword mismatches.&lt;/p&gt;

&lt;h2&gt;
  
  
  The shift
&lt;/h2&gt;

&lt;p&gt;I used to tell candidates "just apply to everything and see what sticks." That's exhausting and demoralizing.&lt;/p&gt;

&lt;p&gt;Now I tell them: run your CV through this first. If it's a 4/10, don't waste three hours customizing. If it's an 8/10, you know exactly what to tweak.&lt;/p&gt;

&lt;p&gt;Job hunting is hard enough. This workflow removes the guesswork.&lt;/p&gt;

&lt;p&gt;Built with n8n. Free to fork and customize.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>automation</category>
      <category>gemini</category>
      <category>helsinki</category>
    </item>
    <item>
      <title>Stop Being the Human Incident Router: AI-Powered Alert Triage with n8n</title>
      <dc:creator>Mychel Garzon</dc:creator>
      <pubDate>Thu, 26 Mar 2026 07:54:36 +0000</pubDate>
      <link>https://dev.to/mychelgarzon/stop-being-the-human-incident-router-ai-powered-alert-triage-with-n8n-3gfc</link>
      <guid>https://dev.to/mychelgarzon/stop-being-the-human-incident-router-ai-powered-alert-triage-with-n8n-3gfc</guid>
      <description>&lt;p&gt;Your monitoring sends 50 notifications a day.&lt;/p&gt;

&lt;p&gt;Maybe 3 are actually urgent. The rest? Noise.&lt;/p&gt;

&lt;p&gt;So you start ignoring them. Then something customer-facing breaks and you miss it because it was buried under 47 low-priority alerts.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Alert fatigue is killing your response time.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;I built an n8n workflow that fixes this. Two AI agents read your runbooks, validate severity, and enforce SLAs automatically.&lt;/p&gt;




&lt;h2&gt;
  
  
  How It Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Agent 1 (Analyzer)&lt;/strong&gt; validates every alert:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Checks against your runbook database&lt;/li&gt;
&lt;li&gt;Looks for customer impact signals&lt;/li&gt;
&lt;li&gt;Assigns confidence-scored severity&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Agent 2 (Response Planner)&lt;/strong&gt; builds the action plan:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;What to do first&lt;/li&gt;
&lt;li&gt;Who to notify&lt;/li&gt;
&lt;li&gt;When to escalate&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Then SLA enforcement runs autonomously. P1 gets 15 minutes. P2 gets 60. Nobody responds? Auto-escalates to management.&lt;/p&gt;

&lt;p&gt;No manual checking. No human bottleneck.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Primary LLM:&lt;/strong&gt; Gemini 2.0&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Fallback:&lt;/strong&gt; Groq (when Gemini fails)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Storage:&lt;/strong&gt; Google Sheets&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Alerts:&lt;/strong&gt; Slack + Gmail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;When Gemini goes down, the workflow automatically switches to Groq. Each agent gets 3 retry attempts with 5-second intervals.&lt;/p&gt;

&lt;p&gt;Basically always works.&lt;/p&gt;




&lt;h2&gt;
  
  
  Real Example
&lt;/h2&gt;

&lt;p&gt;Monitoring sends this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"DB Connection Pool Exhausted"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"description"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"user-service reporting 503 errors"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"P3"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Agent 1 reasoning:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Finds runbook entry: "Connection pool exhaustion = P2 if customer-facing"&lt;/li&gt;
&lt;li&gt;Detects "503 errors" = customer impact&lt;/li&gt;
&lt;li&gt;Service check: user-service is customer-facing&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Decision: Override P3 → P2&lt;/strong&gt; (confidence: 0.87)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Agent 2 action plan:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Check active DB connections&lt;/li&gt;
&lt;li&gt;Restart service if pool &amp;gt;90%&lt;/li&gt;
&lt;li&gt;Notify #incidents channel&lt;/li&gt;
&lt;li&gt;Start 60-minute SLA timer&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;What happens next (automatically):&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Slack alert posts to #incidents&lt;/li&gt;
&lt;li&gt;Timer starts&lt;/li&gt;
&lt;li&gt;Workflow waits, then checks Google Sheets&lt;/li&gt;
&lt;li&gt;Still empty after 60 min? Escalates to #engineering-leads with "SLA BREACH"&lt;/li&gt;
&lt;li&gt;Everything logged to audit trail&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  Why This Works
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Uses your runbooks, not generic templates&lt;/strong&gt;&lt;br&gt;
The workflow reads your Google Sheets runbook database. It knows your systems.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Stops false alarms&lt;/strong&gt;&lt;br&gt;
That "P1 URGENT" email from marketing? Gets downgraded automatically.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Multi-LLM fallback = reliability&lt;/strong&gt;&lt;br&gt;
Primary fails? Fallback takes over. No manual intervention.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;SLAs enforce themselves&lt;/strong&gt;&lt;br&gt;
Timers run autonomously. Management gets paged if nobody responds.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Complete audit trail&lt;/strong&gt;&lt;br&gt;
Every decision logged. Perfect for post-mortems.&lt;/p&gt;


&lt;h2&gt;
  
  
  The Fallback Pattern
&lt;/h2&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Try Gemini (primary)
2. Error? Wait 5 seconds
3. Retry Gemini (attempt 2)
4. Error? Wait 5 seconds
5. Retry Gemini (attempt 3)
6. Still failing? Switch to Groq
7. Groq gets same 3-retry pattern
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;6 total attempts across two providers = 99.9%+ uptime.&lt;/p&gt;


&lt;h2&gt;
  
  
  Two Agents vs One
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Why split the work?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;One agent doing everything (analyze + plan + format) = inconsistent outputs.&lt;/p&gt;

&lt;p&gt;Two specialized agents = better at their specific jobs.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Agent 1: Incident Analyzer&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an incident severity analyzer.
Given this alert and runbook, determine:
1. Is the reported severity accurate?
2. What signals indicate customer impact?
3. What's your confidence score?

Output JSON only.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Agent 2: Response Coordinator&lt;/strong&gt;&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;You are an incident response planner.
Given validated severity, determine:
1. What immediate actions to take?
2. Who to notify?
3. What's the SLA target?

Output JSON only.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Clean separation. One job each.&lt;/p&gt;




&lt;h2&gt;
  
  
  Google Sheets Setup
&lt;/h2&gt;

&lt;p&gt;The workflow needs three sheets:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Runbooks:&lt;/strong&gt;&lt;br&gt;
| Service | Known Issue | Severity | Impact | Contact |&lt;br&gt;
|---------|-------------|----------|--------|---------|&lt;br&gt;
| user-service | Connection pool exhausted | P2 | High | database-team |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Incidents:&lt;/strong&gt;&lt;br&gt;
| ID | Service | Severity | Acknowledged By | Status |&lt;br&gt;
|----|---------|----------|----------------|--------|&lt;br&gt;
| INC-001 | user-service | P2 | &lt;a href="mailto:john@example.com"&gt;john@example.com&lt;/a&gt; | Met |&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;AI_Audit_Log:&lt;/strong&gt;&lt;br&gt;
| Timestamp | ID | Agent | Decision | Confidence |&lt;br&gt;
|-----------|----|----|----------|-----------|&lt;br&gt;
| 2026-03-26 14:30 | INC-001 | Analyzer | P3→P2 | 0.87 |&lt;/p&gt;




&lt;h2&gt;
  
  
  Setup
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;What you need:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Gemini API (free tier works)&lt;/li&gt;
&lt;li&gt;Groq API (also free tier)&lt;/li&gt;
&lt;li&gt;Google Sheets&lt;/li&gt;
&lt;li&gt;Slack OAuth2&lt;/li&gt;
&lt;li&gt;Gmail&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;Time:&lt;/strong&gt; 30-45 minutes&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Steps:&lt;/strong&gt;&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Clone the n8n template&lt;/li&gt;
&lt;li&gt;Add API credentials&lt;/li&gt;
&lt;li&gt;Create Google Sheets structure&lt;/li&gt;
&lt;li&gt;Configure Slack channels&lt;/li&gt;
&lt;li&gt;Point webhook at your monitoring&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  SLA Enforcement Logic
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;1. Incident arrives → severity determined
2. SLA timer starts:
   P1: 15 min | P2: 60 min | P3: 4 hours
3. Workflow waits
4. Checks Google Sheets "Acknowledged By"
5. Empty? Escalate:
   P1 → page management + war room
   P2 → alert engineering leads
   P3 → remind team
6. Log SLA breach
7. Keep checking until acknowledged
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No cron jobs. No external schedulers. Workflow handles timing internally.&lt;/p&gt;




&lt;h2&gt;
  
  
  Get the Template
&lt;/h2&gt;

&lt;p&gt;Grab it here: &lt;a href="https://n8n.partnerlinks.io/incident-triage-linkedin" rel="noopener noreferrer"&gt;https://n8n.partnerlinks.io/incident-triage-linkedin&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Includes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Two-agent setup&lt;/li&gt;
&lt;li&gt;Multi-LLM fallback&lt;/li&gt;
&lt;li&gt;SLA automation&lt;/li&gt;
&lt;li&gt;Google Sheets logging&lt;/li&gt;
&lt;li&gt;Slack integration&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Deploy it. Point it at your runbooks. Stop being the human incident router.&lt;/p&gt;

</description>
      <category>automation</category>
      <category>ai</category>
      <category>devops</category>
      <category>n8n</category>
    </item>
    <item>
      <title>I Built a Real-Time Monitoring Dashboard for an AS/400 with n8n and React</title>
      <dc:creator>Mychel Garzon</dc:creator>
      <pubDate>Sun, 22 Mar 2026 12:47:39 +0000</pubDate>
      <link>https://dev.to/mychelgarzon/i-built-a-real-time-monitoring-dashboard-for-an-as400-with-n8n-and-react-20mc</link>
      <guid>https://dev.to/mychelgarzon/i-built-a-real-time-monitoring-dashboard-for-an-as400-with-n8n-and-react-20mc</guid>
      <description>&lt;p&gt;If you work with IBM i (AS/400), you know the pain. Your system runs critical business logic, but monitoring it feels like checking a car engine by listening to the sounds it makes.&lt;/p&gt;

&lt;p&gt;I wanted a real-time dashboard. Something visual. Something that tells me when jobs fail, when disk usage spikes, or when a scheduled task does not run on time. The problem? The AS/400 was not designed to talk to modern web apps.&lt;/p&gt;

&lt;p&gt;So I connected the dots with n8n in the middle and React on the front end. Here is how I built it.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Problem With AS/400 Monitoring
&lt;/h2&gt;

&lt;p&gt;Most IBM i shops rely on one of two things: WRKACTJOB on a green screen, or expensive enterprise monitoring tools that cost more than your car.&lt;/p&gt;

&lt;p&gt;Neither option works well for a small team that wants fast, visual feedback. You end up checking things manually or finding out about problems hours after they happen.&lt;/p&gt;

&lt;p&gt;I needed something that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Polls the AS/400 on a schedule&lt;/li&gt;
&lt;li&gt;Collects job status, disk usage, and message queue alerts&lt;/li&gt;
&lt;li&gt;Pushes that data to a frontend dashboard in near real-time&lt;/li&gt;
&lt;li&gt;Sends Slack or email alerts when something breaks&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌─────────────┐      ODBC/SQL       ┌─────────────┐
│   IBM i      │ ◄─────────────────► │    n8n       │
│   (AS/400)   │                     │  (Workflows) │
└─────────────┘                      └──────┬──────┘
                                            │
                                     REST API / WS
                                            │
                                     ┌──────▼──────┐
                                     │    React     │
                                     │  Dashboard   │
                                     └─────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three layers. The AS/400 holds the data. n8n pulls it out and transforms it. React displays it.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Querying the AS/400 From n8n
&lt;/h2&gt;

&lt;p&gt;The IBM i stores system information in a set of SQL views and table functions. These are gold. You can query them with standard SQL through an ODBC connection.&lt;/p&gt;

&lt;p&gt;In n8n, I used the &lt;strong&gt;MySQL node&lt;/strong&gt; configured with an ODBC bridge to the AS/400. Some teams use a middleware layer like ODBC Gateway or a lightweight API on the IBM i side. The key point is that n8n can talk SQL to the system.&lt;/p&gt;

&lt;p&gt;Here are the queries I used for the three main data points:&lt;/p&gt;

&lt;h3&gt;
  
  
  Active Jobs
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;JOB_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JOB_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;JOB_STATUS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;CPU_TIME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;TEMPORARY_STORAGE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QSYS2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ACTIVE_JOB_INFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;JOB_NAME_FILTER&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'*ALL'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;JOB_STATUS&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s1"&gt;'*ACTIVE'&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;CPU_TIME&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Disk Usage
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;ASP_NUMBER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SYSTEM_ASP_STORAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;TOTAL_AUXILIARY_STORAGE&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;CURRENT_AUXILIARY_STORAGE_PERCENTAGE&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="n"&gt;QSYS2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;ASP_INFO&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  Message Queue Alerts
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight sql"&gt;&lt;code&gt;&lt;span class="k"&gt;SELECT&lt;/span&gt; &lt;span class="n"&gt;MESSAGE_ID&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;MESSAGE_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;SEVERITY&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
       &lt;span class="n"&gt;MESSAGE_TIMESTAMP&lt;/span&gt;
&lt;span class="k"&gt;FROM&lt;/span&gt; &lt;span class="k"&gt;TABLE&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;QSYS2&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;MESSAGE_QUEUE_INFO&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="n"&gt;MESSAGE_QUEUE_NAME&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="s1"&gt;'QSYSOPR'&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
&lt;span class="k"&gt;WHERE&lt;/span&gt; &lt;span class="n"&gt;SEVERITY&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;=&lt;/span&gt; &lt;span class="mi"&gt;30&lt;/span&gt;
&lt;span class="k"&gt;ORDER&lt;/span&gt; &lt;span class="k"&gt;BY&lt;/span&gt; &lt;span class="n"&gt;MESSAGE_TIMESTAMP&lt;/span&gt; &lt;span class="k"&gt;DESC&lt;/span&gt;
&lt;span class="k"&gt;FETCH&lt;/span&gt; &lt;span class="k"&gt;FIRST&lt;/span&gt; &lt;span class="mi"&gt;50&lt;/span&gt; &lt;span class="k"&gt;ROWS&lt;/span&gt; &lt;span class="k"&gt;ONLY&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each of these runs on a separate n8n workflow with a &lt;strong&gt;Cron trigger&lt;/strong&gt;. Jobs and messages poll every 60 seconds. Disk usage polls every 5 minutes.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: The n8n Workflow Design
&lt;/h2&gt;

&lt;p&gt;Each workflow follows the same pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Cron Trigger&lt;/strong&gt; - fires on schedule&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SQL Query Node&lt;/strong&gt; - runs the query against the AS/400&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Function Node&lt;/strong&gt; - transforms the raw data into a clean JSON shape&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;HTTP Request Node&lt;/strong&gt; - pushes the data to a simple Express API (or writes to a shared database)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;IF Node&lt;/strong&gt; - checks for alert conditions&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Slack Node&lt;/strong&gt; - sends a notification if something is wrong&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Here is what the transform function looks like for active jobs:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JOB_NAME&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JOB_USER&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;status&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;JOB_STATUS&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;cpuTime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseFloat&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;CPU_TIME&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;tempStorage&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;parseInt&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;item&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;json&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;TEMPORARY_STORAGE&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
&lt;span class="p"&gt;}));&lt;/span&gt;

&lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="p"&gt;[{&lt;/span&gt; &lt;span class="na"&gt;json&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;polledAt&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="p"&gt;}];&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;For alerting, the IF node checks simple conditions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Disk usage over 85%? Alert.&lt;/li&gt;
&lt;li&gt;Severity 40+ message in QSYSOPR? Alert.&lt;/li&gt;
&lt;li&gt;A critical job that should be active is missing from the active jobs list? Alert.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is where n8n shines. You can build these rules visually and adjust thresholds without touching code.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: The Data Layer
&lt;/h2&gt;

&lt;p&gt;I kept this simple. The n8n workflows push JSON to a lightweight Express server that stores the latest state in memory and also writes to a PostgreSQL table for history.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Simplified Express endpoint&lt;/span&gt;
&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/api/metrics/:type&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;body&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Update current state&lt;/span&gt;
  &lt;span class="nx"&gt;currentState&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="c1"&gt;// Write to PostgreSQL for history&lt;/span&gt;
  &lt;span class="nx"&gt;db&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INSERT INTO metrics (type, data, created_at) VALUES ($1, $2, NOW())&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// Broadcast to WebSocket clients&lt;/span&gt;
  &lt;span class="nx"&gt;wss&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;clients&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;forEach&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;client&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;client&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;send&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;stringify&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;type&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;data&lt;/span&gt; &lt;span class="p"&gt;}));&lt;/span&gt;
  &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;ok&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kc"&gt;true&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The WebSocket broadcast is what makes the dashboard feel real-time. When n8n pushes new data, every open dashboard tab updates within a second.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 4: The React Dashboard
&lt;/h2&gt;

&lt;p&gt;The frontend is a single-page React app with three panels: Active Jobs, Disk Usage, and Message Queue.&lt;/p&gt;

&lt;p&gt;I used &lt;strong&gt;Recharts&lt;/strong&gt; for the disk usage chart and a simple table component for jobs and messages.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;useState&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;react&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nx"&gt;BarChart&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Bar&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;XAxis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;YAxis&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Tooltip&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Cell&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;recharts&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;DiskUsagePanel&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;useState&lt;/span&gt;&lt;span class="p"&gt;([]);&lt;/span&gt;

  &lt;span class="nf"&gt;useEffect&lt;/span&gt;&lt;span class="p"&gt;(()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;WebSocket&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;ws://localhost:3001&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
    &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;onmessage&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;msg&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;JSON&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;parse&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&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="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;type&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;disk&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
        &lt;span class="nf"&gt;setData&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;msg&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&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;return &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;ws&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;close&lt;/span&gt;&lt;span class="p"&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;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"panel"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Disk Usage (ASP)&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;BarChart&lt;/span&gt; &lt;span class="na"&gt;width&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;height&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="mi"&gt;250&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;data&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;XAxis&lt;/span&gt; &lt;span class="na"&gt;dataKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"aspNumber"&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;YAxis&lt;/span&gt; &lt;span class="na"&gt;domain&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Tooltip&lt;/span&gt; &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Bar&lt;/span&gt; &lt;span class="na"&gt;dataKey&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"usagePercent"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;data&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&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;&amp;lt;&lt;/span&gt;&lt;span class="nc"&gt;Cell&lt;/span&gt;
              &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
              &lt;span class="na"&gt;fill&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;entry&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;usagePercent&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;85&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ef4444&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#22c55e&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
            &lt;span class="p"&gt;/&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;Bar&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nc"&gt;BarChart&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The color logic is simple but effective. Green when everything is fine. Red when you should pay attention. No need to read numbers when the bar turns red.&lt;/p&gt;

&lt;p&gt;For the jobs table, I highlight rows where CPU time is unusually high or where a critical job has gone missing:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight jsx"&gt;&lt;code&gt;&lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;JobsPanel&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;criticalJobs&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;missingCritical&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;criticalJobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;filter&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nx"&gt;cj&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;find&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;j&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;j&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;includes&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;cj&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"panel"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Active Jobs&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;h2&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;missingCritical&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;length&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="s"&gt;"alert"&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          Missing critical jobs: &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;missingCritical&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;join&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;, &lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Job&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;User&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;CPU Time&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;Temp Storage&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;th&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;thead&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;jobs&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="p"&gt;)&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;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt; &lt;span class="na"&gt;key&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;i&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt; &lt;span class="na"&gt;className&lt;/span&gt;&lt;span class="p"&gt;=&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;
              &lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cpuTime&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="mi"&gt;10000&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;row-warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;''&lt;/span&gt;
            &lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;name&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;user&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;cpuTime&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;ms&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
              &lt;span class="p"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;&lt;span class="si"&gt;{&lt;/span&gt;&lt;span class="nx"&gt;job&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;tempStorage&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;MB&lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;td&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
            &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tr&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
          &lt;span class="p"&gt;))&lt;/span&gt;&lt;span class="si"&gt;}&lt;/span&gt;
        &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;tbody&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
      &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;table&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
    &lt;span class="p"&gt;&amp;lt;/&lt;/span&gt;&lt;span class="nt"&gt;div&lt;/span&gt;&lt;span class="p"&gt;&amp;gt;&lt;/span&gt;
  &lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What I Learned
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;The AS/400 is more accessible than people think.&lt;/strong&gt; The QSYS2 SQL services are powerful and well-documented. If your system is on IBM i 7.3 or later, you have access to hundreds of table functions that expose system information through standard SQL. You do not need to write CL programs or parse spool files.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;n8n is perfect for this middle layer.&lt;/strong&gt; It handles scheduling, data transformation, error handling, and alerting in one place. I could have written a Node.js cron script, but n8n gives me visibility into every execution. When a query fails at 3 AM, I can see exactly what happened in the n8n execution log the next morning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Start with three metrics, not thirty.&lt;/strong&gt; I started with a long list of things I wanted to monitor. That was a mistake. I scaled back to three: jobs, disk, and messages. Once those were solid, adding more was easy because the pattern was already proven.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;WebSockets make a huge difference.&lt;/strong&gt; Polling from the frontend would have worked, but the instant updates from WebSocket feel much better. When you fix something on the AS/400 and watch the dashboard turn green within a second, that feedback loop is satisfying.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try It Yourself
&lt;/h2&gt;

&lt;p&gt;If you have access to an IBM i system, you can start experimenting with the SQL queries using &lt;a href="https://pub400.com" rel="noopener noreferrer"&gt;pub400.com&lt;/a&gt;, a free public AS/400 system. The QSYS2 views work there too, with some restrictions.&lt;/p&gt;

&lt;p&gt;For the n8n side, the free self-hosted version handles everything I described here. You do not need the paid cloud version.&lt;/p&gt;

&lt;p&gt;The React dashboard is intentionally simple. No component library needed. Recharts, a WebSocket hook, and some CSS is all it takes.&lt;/p&gt;




&lt;p&gt;The AS/400 is not going anywhere. There are still thousands of these systems running critical workloads around the world. They deserve modern monitoring, and you do not need a six-figure IBM product to get it.&lt;/p&gt;

&lt;p&gt;If you are working with IBM i and want to talk about this approach, drop a comment or find me on &lt;a href="https://linkedin.com/in/mychelgarzon" rel="noopener noreferrer"&gt;LinkedIn&lt;/a&gt;. I am always happy to talk about making legacy systems play nice with modern tools.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Previously: &lt;a href="https://dev.to/mychelgarzon"&gt;The AS/400 Can't Send a Slack Message. I Made It.&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>automation</category>
      <category>n8n</category>
      <category>react</category>
      <category>as400</category>
    </item>
    <item>
      <title>Junior Me Built Workflows That Work. Senior Me Builds Workflows That Survive.</title>
      <dc:creator>Mychel Garzon</dc:creator>
      <pubDate>Wed, 18 Mar 2026 14:25:15 +0000</pubDate>
      <link>https://dev.to/mychelgarzon/junior-me-built-workflows-that-work-senior-me-builds-workflows-that-survive-1ac0</link>
      <guid>https://dev.to/mychelgarzon/junior-me-built-workflows-that-work-senior-me-builds-workflows-that-survive-1ac0</guid>
      <description>&lt;p&gt;I built an n8n workflow in 20 minutes. Then spent 3 hours figuring out why it stopped working a week later. Now I build differently. Here's what changed.&lt;/p&gt;




&lt;h2&gt;
  
  
  The problem with "it works" thinking
&lt;/h2&gt;

&lt;p&gt;When a workflow runs successfully in testing, it's tempting to move on. But in a real company, automations run hundreds of times a day without anyone watching. The question is not whether it works today. It's whether it still works next Tuesday at 3am when nobody is around.&lt;/p&gt;




&lt;h2&gt;
  
  
  1. Use the Error Trigger node. Seriously.
&lt;/h2&gt;

&lt;p&gt;n8n has a built-in Error Trigger node. Most people never touch it.&lt;/p&gt;

&lt;p&gt;Connect it to Slack or email on every critical workflow. When something fails it fires automatically. But don't just send "workflow failed." Send context: which workflow, which node, what the input was, what the error said.&lt;/p&gt;

&lt;p&gt;A useful alert fixes the problem in 5 minutes. A vague one wastes your entire morning.&lt;/p&gt;




&lt;h2&gt;
  
  
  2. Validate before you process
&lt;/h2&gt;

&lt;p&gt;Add an IF node early in your flow to check that required fields exist and look right. If something is off, stop immediately and send an alert.&lt;/p&gt;

&lt;p&gt;Catching bad data at the start is always cheaper than cleaning up after it.&lt;/p&gt;




&lt;h2&gt;
  
  
  3. Build for recovery
&lt;/h2&gt;

&lt;p&gt;If your workflow processes records, check if they've already been handled before acting on them. A simple Google Sheet works fine as a state tracker.&lt;/p&gt;

&lt;p&gt;This prevents duplicate emails, double charges, and very awkward client conversations.&lt;/p&gt;




&lt;h2&gt;
  
  
  4. Name things like a human
&lt;/h2&gt;

&lt;p&gt;"Check if user exists" is a node name. "IF1" is not.&lt;/p&gt;

&lt;p&gt;Use sticky notes to explain why you made certain decisions. A workflow a new person can read in 10 minutes is a workflow that will survive long term.&lt;/p&gt;




&lt;h2&gt;
  
  
  The shift
&lt;/h2&gt;

&lt;p&gt;I used to ship a workflow and move on. Now I spend an extra 30 minutes asking "what breaks this?" before I close the tab. That's the whole difference.&lt;/p&gt;

&lt;p&gt;That one shift changed how reliable my automations are. It also changed how much my clients trust them.&lt;/p&gt;

&lt;p&gt;Reliability is not something you add at the end. It's the way you build from the start.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;What's the worst silent failure you've had in automation? Drop it in the comments.&lt;/em&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>devops</category>
      <category>productivity</category>
    </item>
    <item>
      <title>The AS/400 Can't Send a Slack Message. I Made It.</title>
      <dc:creator>Mychel Garzon</dc:creator>
      <pubDate>Sun, 15 Mar 2026 19:29:05 +0000</pubDate>
      <link>https://dev.to/mychelgarzon/the-as400-cant-send-a-slack-message-i-made-it-goj</link>
      <guid>https://dev.to/mychelgarzon/the-as400-cant-send-a-slack-message-i-made-it-goj</guid>
      <description>&lt;p&gt;IBM i is remarkably reliable. That reliability is also the problem. Because nothing breaks often, nobody builds proper alerting. Then something does break, nobody notices for three hours, and you're explaining to a client why their invoices didn't go out.&lt;/p&gt;

&lt;p&gt;The first time I tried to connect to one of these systems from a modern Node.js service, the connection just hung. No error, no timeout, nothing. Turns out I had the wrong port and the IBM i firewall was silently dropping packets. That took longer to figure out than I'd like to admit.&lt;/p&gt;

&lt;p&gt;The system has no native webhook support. No push notifications. No modern alerting primitives. What it has is DB2, SFTP, message queues, and job logs. That's enough.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture
&lt;/h2&gt;

&lt;p&gt;The idea is simple: a small TypeScript service polls the IBM i system on a schedule, checks for failure conditions, and posts to an n8n webhook when it finds one. n8n handles the routing, the notification, and the audit log.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;TypeScript Poller (runs every 60s)
  → Connect to IBM i via DB2
  → Query job logs / message queues / custom alert table
  → If condition met → POST to n8n webhook
      → n8n routes by severity
          → [critical] → PagerDuty + Slack
          → [warning]  → Slack only
          → [info]     → Log to sheet
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The TypeScript Service
&lt;/h2&gt;

&lt;p&gt;Install the dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install &lt;/span&gt;node-jt400 node-cron axios dotenv
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-D&lt;/span&gt; typescript ts-node @types/node
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The core poller:&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="k"&gt;import&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="nx"&gt;dotenv&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;dotenv&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="nx"&gt;dotenv&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;config&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="c1"&gt;// eslint-disable-next-line @typescript-eslint/no-var-requires&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;jt400&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;node-jt400&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;jt400&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt;
  &lt;span class="na"&gt;host&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IBM_HOST&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;user&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IBM_USER&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="na"&gt;password&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;IBM_PASSWORD&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;AlertPayload&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="o"&gt;|&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;info&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;jobName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;string&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;checkJobQueue&lt;/span&gt;&lt;span class="p"&gt;():&lt;/span&gt; &lt;span class="nb"&gt;Promise&lt;/span&gt;&lt;span class="o"&gt;&amp;lt;&lt;/span&gt;&lt;span class="nx"&gt;AlertPayload&lt;/span&gt;&lt;span class="p"&gt;[]&lt;/span&gt;&lt;span class="o"&gt;&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;pool&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;query&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s2"&gt;`
    SELECT MESSAGE_TEXT, MESSAGE_TYPE, FROM_PROGRAM, MESSAGE_TIMESTAMP
    FROM QSYS2.MESSAGE_QUEUE_INFO
    WHERE MESSAGE_QUEUE_NAME = 'QSYSOPR'
      AND MESSAGE_TYPE IN ('INQUIRY', 'DIAGNOSTIC')
      AND MESSAGE_TIMESTAMP &amp;gt; CURRENT_TIMESTAMP - 1 MINUTES
  `&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;rows&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="na"&gt;row&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="kr"&gt;any&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;({&lt;/span&gt;
    &lt;span class="na"&gt;severity&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MESSAGE_TYPE&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;INQUIRY&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;?&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;critical&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;warning&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;jobName&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;FROM_PROGRAM&lt;/span&gt; &lt;span class="o"&gt;??&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;SYSTEM&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;message&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;row&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;MESSAGE_TEXT&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
    &lt;span class="na"&gt;timestamp&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="k"&gt;new&lt;/span&gt; &lt;span class="nc"&gt;Date&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;toISOString&lt;/span&gt;&lt;span class="p"&gt;(),&lt;/span&gt;
  &lt;span class="p"&gt;}));&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// seen is in-memory so it resets on restart — good enough for most cases,&lt;/span&gt;
&lt;span class="c1"&gt;// but write it to DB2 if you need persistence across deploys&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;seen&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;Set&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="o"&gt;&amp;gt;&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="k"&gt;async&lt;/span&gt; &lt;span class="kd"&gt;function&lt;/span&gt; &lt;span class="nf"&gt;sendAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;AlertPayload&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;id&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="s2"&gt;`&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;jobName&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;-&lt;/span&gt;&lt;span class="p"&gt;${&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;message&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="s2"&gt;`&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="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;has&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nx"&gt;seen&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;add&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;id&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nx"&gt;axios&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;post&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;process&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;env&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;N8N_WEBHOOK_URL&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nx"&gt;cron&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;schedule&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;* * * * *&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="k"&gt;async &lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;try&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alerts&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;checkJobQueue&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;
    &lt;span class="k"&gt;for &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;alert&lt;/span&gt; &lt;span class="k"&gt;of&lt;/span&gt; &lt;span class="nx"&gt;alerts&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="k"&gt;await&lt;/span&gt; &lt;span class="nf"&gt;sendAlert&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;alert&lt;/span&gt;&lt;span class="p"&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;catch &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;error&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Poller error:&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;err&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;IBM i alert poller running...&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Before you ship this
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;QSYS2.MESSAGE_QUEUE_INFO&lt;/code&gt; is your friend. It exposes the system operator message queue without needing to write RPGLE. You can filter by message type, timestamp, and severity right in SQL. Most IBM i shops already use &lt;code&gt;QSYSOPR&lt;/code&gt; for critical system messages so the signal-to-noise ratio is decent.&lt;/p&gt;

&lt;p&gt;Deduplication matters more than you'd think. A single job failure can generate a dozen messages in under a minute. The &lt;code&gt;seen&lt;/code&gt; Set in the code above handles this for most cases, but if you're running the poller across multiple instances or need history across restarts, write a seen-alerts table to DB2 instead.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;node-jt400&lt;/code&gt; uses JDBC under the hood via &lt;code&gt;java-bridge&lt;/code&gt;, which means you need a JDK installed on whatever machine is running the TypeScript service — not just a JRE. And &lt;code&gt;JAVA_HOME&lt;/code&gt; must be set at install time or the build fails with a confusing &lt;code&gt;jni.h not found&lt;/code&gt; error. On macOS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="si"&gt;$(&lt;/span&gt;/usr/libexec/java_home&lt;span class="si"&gt;)&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;node-jt400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;On Linux:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;export &lt;/span&gt;&lt;span class="nv"&gt;JAVA_HOME&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/usr/lib/jvm/java-11-openjdk-amd64
npm &lt;span class="nb"&gt;install &lt;/span&gt;node-jt400
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add it to your &lt;code&gt;.bashrc&lt;/code&gt; or systemd service env so it survives restarts. This is the most likely failure point on a fresh machine — put it in your setup docs before someone else tries to deploy it and spends 40 minutes confused.&lt;/p&gt;




&lt;h2&gt;
  
  
  The n8n side
&lt;/h2&gt;

&lt;p&gt;Create a webhook node as the trigger. Add a Switch node routing by &lt;code&gt;severity&lt;/code&gt;. Wire &lt;code&gt;critical&lt;/code&gt; to a Slack node and whatever paging tool you use. Wire &lt;code&gt;warning&lt;/code&gt; to Slack only. Wire everything to a Google Sheets or Airtable log node so you have a history.&lt;/p&gt;

&lt;p&gt;The whole n8n workflow takes about 15 minutes to build once the TypeScript service is posting clean JSON. The part that takes longer is deciding who actually gets paged at 2am. That's a people problem, not a code problem.&lt;/p&gt;




&lt;h2&gt;
  
  
  What this unlocks
&lt;/h2&gt;

&lt;p&gt;Once the poller is running you can extend the query to check anything DB2 can see: failed batch jobs, stuck SFTP transfers, records in an error table your CL programs write to, job queues that have been idle too long. The alerting logic lives in SQL, which means an IBM i developer can maintain it without touching TypeScript.&lt;/p&gt;




&lt;blockquote&gt;
&lt;p&gt;All code in this post was validated against a live IBM i system on &lt;a href="https://pub400.com" rel="noopener noreferrer"&gt;pub400.com&lt;/a&gt;. The SQL query, column names, and connection setup are confirmed working on Node.js v22 with &lt;code&gt;node-jt400&lt;/code&gt;.&lt;/p&gt;
&lt;/blockquote&gt;




</description>
      <category>n8n</category>
      <category>automation</category>
      <category>as400</category>
    </item>
    <item>
      <title>The Internal IT Automation Stack: How Growing SaaS Companies Use n8n to Scale Without Hiring</title>
      <dc:creator>Mychel Garzon</dc:creator>
      <pubDate>Sun, 08 Mar 2026 12:39:27 +0000</pubDate>
      <link>https://dev.to/mychelgarzon/the-internal-it-automation-stack-how-growing-saas-companies-use-n8n-to-scale-without-hiring-43aj</link>
      <guid>https://dev.to/mychelgarzon/the-internal-it-automation-stack-how-growing-saas-companies-use-n8n-to-scale-without-hiring-43aj</guid>
      <description>&lt;p&gt;There is a moment every growing SaaS company hits. You go from 50 employees &lt;br&gt;
to 150, and suddenly your IT team is drowning. Onboarding takes three days. &lt;br&gt;
Offboarding is a security risk. License renewals get missed. Finance is &lt;br&gt;
chasing invoice approvals over Slack.&lt;/p&gt;

&lt;p&gt;The answer is not hiring more IT staff. The answer is building an internal &lt;br&gt;
automation engine. And the tool that makes this possible is n8n.&lt;/p&gt;

&lt;p&gt;I'm Mychel Garzon — n8n Verified Creator and Junction 2025 n8n Tech &lt;br&gt;
Challenge Winner at Europe's largest hackathon. This is the architecture &lt;br&gt;
I use to eliminate manual IT work at scaling SaaS companies.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Four Pillars of Internal IT Automation
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Zero-Touch Onboarding
&lt;/h3&gt;

&lt;p&gt;Every new hire triggers a cascade of manual tasks: Google Workspace account, &lt;br&gt;
Slack invite, GitHub access, SaaS licenses, welcome message. Done manually &lt;br&gt;
this takes hours. Built in n8n it takes seconds.&lt;/p&gt;

&lt;p&gt;The architecture: a webhook receives the new hire payload from your HR system. &lt;br&gt;
A Switch node routes based on department — Engineering gets GitHub and Jira, &lt;br&gt;
Marketing gets HubSpot and Notion. Parallel branches provision each tool &lt;br&gt;
simultaneously.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The critical detail most teams miss:&lt;/strong&gt; every provisioning step needs &lt;br&gt;
independent error handling. If GitHub fails, Slack and Google Workspace &lt;br&gt;
should still complete. A sequential chain that breaks on step two is worse &lt;br&gt;
than no automation at all.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Secure Offboarding
&lt;/h3&gt;

&lt;p&gt;Offboarding is where manual IT processes become a genuine security risk. A &lt;br&gt;
departing employee with active Google Workspace and GitHub access for three &lt;br&gt;
days after their last day is not a hypothetical — it happens constantly.&lt;/p&gt;

&lt;p&gt;n8n offboarding pipelines solve this with a single webhook trigger from HR. &lt;br&gt;
Within seconds: Google Workspace suspended, GitHub org membership removed, &lt;br&gt;
Slack deactivated, SaaS licenses reclaimed. Each step runs independently &lt;br&gt;
with its own error branch. A compliance log writes to Google Sheets regardless &lt;br&gt;
of outcome.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Architecture insight:&lt;/strong&gt; use parallel execution not sequential chains. &lt;br&gt;
Sequential offboarding that stops at step two because a user never had a &lt;br&gt;
GitHub account is a security vulnerability dressed as a workflow.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. SaaS License Management
&lt;/h3&gt;

&lt;p&gt;The average 200-person SaaS company wastes between $40,000 and $80,000 per &lt;br&gt;
year on unused software licenses. The problem is not that companies do not &lt;br&gt;
care — it is that nobody has visibility.&lt;/p&gt;

&lt;p&gt;n8n solves this with a scheduled workflow that pulls active user counts from &lt;br&gt;
every major SaaS tool via API, compares them against your license count in &lt;br&gt;
Google Sheets, and emails a utilization report every Monday morning.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The deduplication logic is critical.&lt;/strong&gt; Without it the same renewal alert &lt;br&gt;
fires every day for 30 days. Store the last alert timestamp and only fire &lt;br&gt;
when the threshold has been crossed and no alert has been sent in the past &lt;br&gt;
7 days.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Internal Request Automation
&lt;/h3&gt;

&lt;p&gt;Password resets. Software access requests. Equipment orders. These consume &lt;br&gt;
IT helpdesk time without adding strategic value.&lt;/p&gt;

&lt;p&gt;n8n replaces the execution layer entirely. A Slack slash command creates a &lt;br&gt;
Jira ticket and routes it to the right approver based on request type and &lt;br&gt;
cost threshold. Under €500 auto-approves. Over €500 goes to the team lead. &lt;br&gt;
The approval is a Slack button click — not an email chain. The audit trail &lt;br&gt;
is automatic.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Architecture Principles That Make This Production-Grade
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Every external API call needs an error branch.&lt;/strong&gt; Silent failures in IT &lt;br&gt;
automation are security risks. A failed offboarding step nobody knows about &lt;br&gt;
is an active vulnerability.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Every workflow touching sensitive data needs an audit log.&lt;/strong&gt; Not the n8n &lt;br&gt;
execution log — a separate human-readable record in Google Sheets that &lt;br&gt;
compliance teams can access without opening n8n.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Deduplication is not optional.&lt;/strong&gt; Any workflow that polls data or sends &lt;br&gt;
notifications will spam your team without explicit deduplication logic. &lt;br&gt;
Always ask: what happens the second time this runs?&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Approval gates belong before actions, not after.&lt;/strong&gt; Updating your &lt;br&gt;
accounting system before an invoice is approved is not automation — &lt;br&gt;
it is a liability.&lt;/p&gt;




&lt;h2&gt;
  
  
  The Result
&lt;/h2&gt;

&lt;p&gt;Companies that build this internal automation engine do not need to hire &lt;br&gt;
a new IT staff member every time headcount doubles. The workflows handle &lt;br&gt;
the repeatable work. The IT team handles the exceptions and the strategy.&lt;/p&gt;

&lt;p&gt;n8n runs self-hosted inside your own infrastructure — meaning sensitive HR &lt;br&gt;
and Finance data never leaves your environment. For a SaaS company operating &lt;br&gt;
under GDPR this is not a nice-to-have. It is a requirement.&lt;/p&gt;

&lt;p&gt;If your IT team is still onboarding employees manually it is not a people &lt;br&gt;
problem. It is an architecture problem. And architecture problems have &lt;br&gt;
solutions.&lt;/p&gt;




&lt;p&gt;&lt;em&gt;Mychel Garzon is an n8n Verified Creator and Junction 2025 n8n Tech &lt;br&gt;
Challenge Winner based in Helsinki, Finland. Portfolio: &lt;br&gt;
&lt;a href="https://mychelgarzon.netlify.app" rel="noopener noreferrer"&gt;mychelgarzon.netlify.app&lt;/a&gt; | &lt;br&gt;
GitHub: &lt;a href="https://github.com/MychelGarzon" rel="noopener noreferrer"&gt;github.com/MychelGarzon&lt;/a&gt;&lt;/em&gt;&lt;/p&gt;

</description>
      <category>n8n</category>
      <category>automation</category>
      <category>devops</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
