DEV Community

Cover image for I Deleted My Last n8n Workflow. Convex Ran the 30-Minute Job in 80 Lines of TypeScript.
Phil Rentier Digital
Phil Rentier Digital

Posted on • Originally published at rentierdigital.xyz

I Deleted My Last n8n Workflow. Convex Ran the 30-Minute Job in 80 Lines of TypeScript.

I deleted my last n8n workflow this week. It had been running for 8 months without complaining. I deleted it because one evening, watching my stack run, I noticed a stupid thing: Convex, Infisical, NetBird, Traefik, all going through the same VPN mesh, all versioned in the same monorepo, all code-reviewable.

TLDR: Visual editors had one real killer feature, "no code needed." Claude Code just killed that feature. The question now is whether your job actually needs to leave the visual layer, or if it lives there just fine.

Everything except one service. n8n. Its source of truth was a local SQLite plus 3 Google Sheets I no longer dared close.

I have nothing against n8n. I loved it for 2 years and bled on its esoteric JS expressions (anyone who has tried {{$node["Webhook"].json["body"]["data"][0]["nested"]}} knows). But this is just rationality. Time spent, service rendered, deliverables sold.

I Couldn't git diff My Own Workflow

I just noticed it was the only service in my stack I couldn't git diff. Not testable. Not easily replayable. The only thing that escaped me.

And the worst part is that this specific workflow weighed 2,633 lines of JSON when exported. 2,633 lines that no one, me included, could code-review seriously. I tried once, after a junior teammate asked me what a specific node did and I realized I had forgotten. I opened the export in VS Code, scrolled for 4 minutes, gave up, and went back to the visual editor to find the node by clicking around. That's the moment I knew. A piece of infrastructure you can only audit by clicking on it is not infrastructure. It's a Tamagotchi.

n8n is not poorly designed. It's designed for another job. The mismatch is mine, not n8n's, and I had carried it for 8 months without naming it.

The 2,633-Line JSON I Couldn't Even Code-Review

TITLE "The 2,633-Line Tax" + subtitle "what versioning costs when your workflow lives in JSON". Metaphor: a giant scroll of JSON unrolling from a printer, with a tiny human at the bottom trying to read it through a magnifying glass, while a green git terminal on the left shows clean diffs and a red JSON terminal on the right shows a wall of unreadable characters. Style: Franco-Belgian ligne claire comic, thick black outlines, flat shading, expressive faces. Palette: paper cream #FFF8E7, ink black #111111, hot red #E63946, terminal green #4CAF50, sky blue #4FC3F7. Content: left panel labeled "CODE" with a clean git diff (+12 -3), right panel labeled "JSON EXPORT" with a chaotic wall of curly braces and quotation marks, the human in the middle holds a sign reading "I am supposed to review this". Highlight: the JSON wall has a glowing red 2633 number in the corner, the git diff has a small green checkmark. Legend: bottom-left sticky note, "left = reviewable / right = vibes-based". Footer: copyright rentierdigital.xyz. NOT flat corporate vector, NOT minimalist tech aesthetic.


The Hidden Cost of JSON-Based Development Workflows

30+ nodes. Google Sheets used as a database, with empty columns acting as a state machine, because that's what you do when your tool doesn't give you a real state machine. A polling loop against an external LLM API that takes anywhere from 90 seconds to 30 minutes per call, and that loses its place the moment n8n restarts. Webhook receivers that fire twice when n8n is under load. A queue mode setup I spent a weekend tuning.

I am not picking on n8n alone here. Make is the same problem with worse pricing. Pay-per-operation, no version control, friction the moment you need anything serious. Zapier is the same thing again, billed per task, every IF/ELSE costs you. n8n at least lets you self-host. That's the only architectural win it has over the other two, and even that win comes with a queue mode setup that takes a weekend to tune.

When I say "I can't code-review 2,633 lines of JSON", I am being charitable. The n8n staff themselves admit it. On their own official forum, in a thread titled N8n performance and scalability, they write: "Scaling n8n is currently not that easy" and "n8n does not scale properly yet." And an operator on a community forum reported his self-hosted n8n reliably crashing under load spikes of 2,000 requests in a few minutes. Reliably. As in, you can plan for it.

That's not a hater on Reddit. That's n8n on n8n.

The 80-Line TypeScript Pattern That Replaced All Of It

Here is the pattern that replaced the 30+ nodes. Self-rescheduling polling with Convex's scheduler. State persisted on every step. If the worker crashes between two polls, the scheduler picks it back up exactly where it left off. No queue mode. No SQLite to back up.

import { internalAction, internalMutation } from "./_generated/server"
import { internal } from "./_generated/api"
import { v } from "convex/values"

// Kick off a long-running external job
export const startJob = internalAction({
  args: { jobId: v.string(), payload: v.any() },
  handler: async (ctx, { jobId, payload }) => {
    const remoteId = await callExternalLLM(payload)
    await ctx.runMutation(internal.jobs.recordStart, { jobId, remoteId })
    // Schedule the first poll in 30 seconds
    await ctx.scheduler.runAfter(
      30_000,
      internal.jobs.pollStatus,
      { jobId, remoteId, attempt: 1 }
    )
  },
})

export const pollStatus = internalAction({
  args: {
    jobId: v.string(),
    remoteId: v.string(),
    attempt: v.number(),
  },
  handler: async (ctx, { jobId, remoteId, attempt }) => {
    // 30 min timeout (60 polls of 30s)
    if (attempt > 60) {
      await ctx.runMutation(internal.jobs.markTimeout, { jobId })
      await ctx.runAction(internal.jobs.notifyIncident, { jobId })
      return
    }

    const status = await fetchRemoteStatus(remoteId)

    if (status.state === "done") {
      await ctx.runMutation(internal.jobs.recordResult, {
        jobId,
        result: status.result,
      })
      return
    }

    if (status.state === "failed") {
      await ctx.runMutation(internal.jobs.recordFailure, {
        jobId,
        error: status.error,
      })
      return
    }

    // Still running, reschedule
    await ctx.scheduler.runAfter(
      30_000,
      internal.jobs.pollStatus,
      { jobId, remoteId, attempt: attempt + 1 }
    )
  },
})
Enter fullscreen mode Exit fullscreen mode

That's it. Roughly 80 lines once you add the mutations that write to the table. It does what 30 n8n nodes did, plus a few things n8n could never do cleanly. Versioned with the app, tested with the app, deployed in the same CI pass, state living in the same Convex table the frontend already reads from, so the UI sees the job status update in real time without me doing anything extra.

The reason this rivals Inngest's step.sleep() pattern is that every ctx.scheduler.runAfter() call durably persists the next invocation in Convex's own store. If the deployment crashes mid-job, when it comes back up the scheduler resumes from the last persisted step. I am not faking durability with a cron loop and praying.

If you are not on Convex, this exact pattern lives natively in Convex + Claude Code: The Ultimate Duo for Shipping SaaS at 3 AM. And if you are on Postgres, you have Inngest, Trigger.dev, Temporal. They all solve the same actual problem: a long-running job, with state, that must survive crashes. That's the right problem to solve. It's not n8n's.

Most automation tutorials get lost here. They show you how to chain 12 nodes that call an LLM and write to a Sheet, but they never show you what happens when the LLM call takes 14 minutes and your n8n container OOMs at minute 9. The answer is, you redo the job from scratch and you cry. The pattern above redoes the last 30 seconds and moves on.

The 4-Question Architecture Map: How I Actually Choose

TITLE "The 4-Question Map" + subtitle "duration on Y, code coupling on X". Metaphor: a 2x2 architecture map drawn like an old subway map, 4 stations connected by colored lines, each station representing a tool family. Style: engineer blueprint, white lines and text on a deep navy background, tech-pen feel, drafting compass marks in the corners. Palette: blueprint navy #0B2545, chalk white #F4F4F4, neon yellow #FFD60A, hot pink #FF3E7F, mint #4ECDC4. Content: 4 quadrants labeled "SHORT + LOOSE (visual stuff: Zapier/Make/n8n)", "LONG + TIGHT (Convex/Inngest/Trigger/Temporal)", "REACTIVE CRUD (Convex direct)", "HEAVY DATA (Supabase + pg_cron / Airflow)". X axis labeled "code coupling: loose to tight", Y axis labeled "duration: seconds to hours". Highlight: the "LONG + TIGHT" quadrant glows in neon yellow with a small lightning bolt, indicating where the article focuses. Legend: bottom-right corner, "yellow station = where the article lives". Footer: copyright rentierdigital.xyz. NOT flat corporate vector, NOT minimalist startup aesthetic.


The 4-Question Architecture Map for Tool Selection

I needed a decision rule that survives the next 4 framework launches. Here are the 4 questions I ask now.

Question 1. Job under 30 seconds, glue work between SaaS tools, no business logic?
Use the visual stuff. Zapier, Make, n8n, all defendable. The cost of the visual editor is real but the upside (you onboard a non-dev colleague in 1 afternoon) is also real. Picking code here is overengineering.

Question 2. Job between 5 and 30 minutes, durable state, retries, resume after crash?
Code or you die. Convex scheduler if you're already on Convex. Inngest, Trigger.dev or Temporal otherwise. No visual editor, ever. A Trigger.dev customer testimonial published on their landing page says it bluntly about Zapier and n8n: "they become complex, really slow, expensive and time-consuming to manage for large automations." That matches my own experience exactly.

Question 3. CRUD plus real-time plus reactive frontend?
Convex direct. Don't add Inngest on top. Doubling the orchestration layer is doubling the state, doubling the failure modes, doubling the auth dance. Convex's scheduler is enough for 90% of background jobs a SaaS app needs. I think this is the line where I am the least sure, honestly. If your real-time needs grow into multi-region with strict ordering guarantees, Convex might not be the answer in 18 months and you'll want a proper queue. For now it holds.

Question 4. Heavy data pipeline, complex SQL, DAGs?
Supabase plus pg_cron, or Airflow. Convex does not do ad-hoc analytical SQL. Don't try to make it. This is where Convex stops being the right tool and Postgres-shaped infrastructure starts winning.

That's the entire decision tree. The 2 axes (duration and coupling) sit underneath, but you don't need to think about them every time. You just ask how long the job lives and how tightly it touches the rest of your code, and the tool falls out of the answer.

The reason I trust this framework more than tool-by-tool comparisons is that it has survived 3 stack migrations in 2 years. I moved from Supabase to Convex, from Vercel cron to Convex scheduler, from n8n to TypeScript, and the 4 questions did not change once across any of those moves. Only the names of the tools did. The real test of any architecture decision rule, in my experience, is whether it still makes sense 18 months later, when half the products on the slide have a new pricing page, a new logo, or a new acquirer. If it still makes sense, keep it. If it doesn't, it was tool-shilling dressed up as a framework, and you can throw it away with the slide deck.

What I'd Still Use n8n For

I am not on a crusade. There are jobs where n8n still wins, and Convex would obviously be the wrong answer.

Sync a Google Sheet to a Slack channel when a cell changes. Email notification when a new PostgreSQL row appears. Stripe webhook forwarded to Discord. Any stable, repetitive, no-business-logic job under 30 seconds. For all of these, n8n beats writing TypeScript. Not because n8n is great. Because the job is small enough that the visual editor's overhead is smaller than the code overhead. The break-even point is real and n8n sits comfortably on the right side of it for this class of work.

I wrote a whole piece on why one open-source repo turned Claude Code into an n8n architect a few weeks ago. That article and this one are not contradictory. Claude Code makes n8n much better at the things n8n is already good at. This article is about the moment a job stops being one of those things. The moment your workflow needs durable state, retries, replay, code review, the moment a junior teammate looks at the JSON export and asks "is this normal?" That's when you leave.

(My pool guy came by yesterday morning while I was thinking about all this, asked me what I was building, and I said "a thing that watches another thing for 30 minutes." He nodded slowly and said "ok" the way you say "ok" to someone who has clearly not slept enough. He's not wrong. The kids came back from school an hour later, the dog stole a tortilla off the counter, and I forgot about Convex for the rest of the afternoon.) 🐕

Choose Orchestration Like Infrastructure, Not Like a Tool

Picking an orchestration layer is not a tool choice. It is an architecture choice. Tools move every 18 months. Convex will get a competitor. Inngest will get a new pricing tier. n8n will ship a v3. The framework underneath (how long does the job live and how tightly does it touch the rest of your code) does not move. It was true before n8n. It will be true after.

And if you are reading this thinking your current n8n workflow should move to code, maybe. Maybe not. Run the 4 questions. If the answer is "glue work between SaaS, under 30 seconds, no business logic", stay on n8n. It's literally built for that.

Delete one workflow this week. The smallest one you have. See how it feels.

Sources

This post may contain affiliate links. If you click them, I might earn a small commission, costs you nothing, and helps me keep shipping quality articles every day for your reading pleasure.

Top comments (0)