# Why AI Applications Need Background Workflows

This post walks through why stuffing long-running AI work into a normal web request eventually breaks down, what background workflows are, and how a tool like Inngest handles events, retries, and checkpointing so you don't burn tokens re-doing work every time a server hiccups. We'll look at webhook-triggered AI pipelines, agents running inside workflows, and where this pattern shows up in real production systems.

## TLDR

Long-running AI tasks (code review, document processing, multi-step agents) don't belong inside a single HTTP request. Background workflows decouple the trigger from the execution, and tools like Inngest add checkpointing — so a crash mid-workflow doesn't mean re-running expensive, token-costly steps from scratch. Reliability, not raw capability, is what keeps people using your software.

## Introduction

If you've built anything with LLMs beyond a simple chat box, you've probably hit this wall: a request comes in, you call a model, maybe you call it three or four more times in a loop, you touch a database, maybe a webhook fires partway through — and the whole thing is expected to happen before an HTTP connection times out.

That works fine for a single, fast completion. It falls apart the moment "AI task" starts meaning "multi-step process that might take 30 seconds, might take 10 minutes, and might fail somewhere in the middle."

Before going further, it helps to have a mental model of a normal request-response cycle and where it breaks under AI workloads — that's exactly the gap background workflows are built to fill, and it's what the rest of this post is about.

* * *

## Problem 1: Everything Is Jammed Into a Web Request

### Problem Statement

A typical API route does its work synchronously: request comes in, code runs, response goes out. This assumes the work is short. AI tasks routinely aren't. A document processing pipeline might need to extract text, chunk it, embed it, run it through a model for summarization, then write results back — each step taking real time, and each one able to fail independently.

Keep the request open that whole time and you're fighting HTTP timeouts, tying up server resources, and giving the user a spinning loader with no visibility into what's actually happening.

### Solution

Split the *trigger* from the *execution*. The web request's only job becomes: accept the input, kick off a background job, return immediately. The actual AI work runs asynchronously, outside the request lifecycle, and reports back (via polling, websockets, or a callback) when it's done.

### Step-by-Step Instructions

1.  Take a task that clearly takes too long for a synchronous request — for example, "summarize this 40-page PDF and email the result."
    
2.  In the synchronous version, trace what happens: request → extract text → chunk → call the model several times → write to DB → return. Notice how much of that has nothing to do with what the client actually needs immediately.
    
3.  In the background version, the request only enqueues a job (`processDocument(fileId)`) and returns a job ID right away.
    
4.  The heavy lifting — extraction, chunking, model calls — happens in a worker process that isn't blocked by an open HTTP connection.
    
5.  Compare the two side by side: same work, but one ties up a request thread for minutes and the other frees it up in milliseconds.
    

**Synchronous request vs. background workflow:**

![](https://cdn.hashnode.com/uploads/covers/65a8dbbd615555c5bcb87c93/1da87e87-1401-4b02-9e41-289afefa93a8.png align="center")

In the synchronous path, the client's connection stays open for the entire chain. In the background path, the client gets an immediate acknowledgment while the actual work happens off to the side.

**Pseudocode — synchronous route:**

```javascript
function handleRequest(fileId):
    text = extractText(fileId)    // blocking, slow
    chunks = chunkText(text)      // blocking
    summary = callModel(chunks)   // blocking, slow, costs tokens
    saveResult(fileId, summary)   // blocking
    return response(summary)      // client waited for ALL of this
```

**Pseudocode — background workflow route:**

```javascript
function handleRequest(fileId):
    jobId = enqueue("processDocument", { fileId })
    return response({ jobId })       // returns almost instantly

// runs separately, outside the HTTP request lifecycle
function processDocument(fileId):
    text = extractText(fileId)
    chunks = chunkText(text)
    summary = callModel(chunks)
    saveResult(fileId, summary)
    notifyClient(fileId, summary)
```

* * *

## Problem 2: Crashes Mean Starting Over — and That's Expensive With AI

### Problem Statement

This is the part that matters most in practice. System reliability is the main thing that decides whether people keep using your software as their default choice. It doesn't matter how state-of-the-art the underlying model is — if the system isn't reliable, people leave.

The traditional fix for unreliable systems is retries with backoff: if a step fails, try again. But retries alone assume you're re-running something cheap. If the *server itself* crashes mid-process, there's usually no checkpoint of where you were. You don't retry the failed step — you recalculate the entire pipeline from scratch.

For a database write, that's annoying. For an AI pipeline, that's expensive. Every model call you re-run is tokens you're paying for again, on top of the latency you already lost.

### Solution

This is exactly where Inngest earns its place. Instead of treating a workflow as one opaque function that either fully succeeds or fully fails, Inngest lets you define it as a series of steps, each of which is checkpointed as it completes. If the process crashes after step 3 of 5, Inngest doesn't rerun steps 1 through 3 — it resumes from where it left off.

For AI workflows specifically, that means a completed model call is *saved*, not repeated. You retry the step that actually failed, not the entire chain of expensive completions before it.

### Step-by-Step Instructions

1.  Break your AI pipeline into discrete `step.run()` calls instead of one long function — e.g., `extractText`, `generateEmbeddings`, `summarizeWithModel`, `saveResult`.
    
2.  Each step's result is automatically checkpointed by Inngest once it completes successfully.
    
3.  If a later step throws (network blip, rate limit, server restart), Inngest retries just that step — the earlier, already-paid-for model calls aren't repeated.
    
4.  Configure retry behavior (attempt counts, backoff) per function so transient failures don't need manual intervention.
    
5.  On a genuine failure after retries are exhausted, Inngest surfaces the failure with full context on which step failed and what the prior steps already produced — instead of a wall of "unhandled exception" and no trail.
    

**Workflow with checkpointing and retry:**

![](https://cdn.hashnode.com/uploads/covers/65a8dbbd615555c5bcb87c93/daf24dad-5d0c-48dc-910a-e32b17f5be85.png align="center")

Notice steps 1 and 2 are never re-run after the crash — only the step that was actually in flight when things went wrong gets retried.

**Pseudocode — no checkpointing (recalculates everything on crash):**

```javascript
function processDocument(fileId):
    text = extractText(fileId)
    embeddings = generateEmbeddings(text)   // costs tokens
    summary = summarizeWithModel(embeddings) // costs tokens, CRASHES here
    saveResult(fileId, summary)

// on restart, the entire function runs again from scratch —
// extractText and generateEmbeddings (and their token cost) repeat
```

**Pseudocode — with step-based checkpointing (Inngest-style):**

```javascript
function processDocument(fileId):
    text = step.run("extractText", () => extractText(fileId))
    embeddings = step.run("generateEmbeddings", () => generateEmbeddings(text))
    summary = step.run("summarizeWithModel", () => summarizeWithModel(embeddings))
    step.run("saveResult", () => saveResult(fileId, summary))

// each step.run() result is checkpointed automatically
// on crash + restart: extractText and generateEmbeddings are read from
// the checkpoint, NOT re-executed — only summarizeWithModel retries
```

* * *

## Problem 3: Getting AI Workflows to Actually Start

### Problem Statement

A background workflow is only useful if something reliably triggers it. Real systems don't wait around for a user to click a button — a GitHub PR gets opened, a form gets submitted, a file lands in storage. You need a dependable way to turn those external events into workflow executions.

### Solution

Webhooks are the connective tissue. An external service posts an event to your endpoint, that endpoint hands the event to Inngest, and Inngest runs the matching workflow — decoupled from however long the AI part takes.

### Step-by-Step Instructions

1.  Define an event your workflow cares about, e.g. `github/pull_request.opened`.
    
2.  Set up a webhook endpoint that receives the GitHub payload and sends it to Inngest as an event (`inngest.send(...)`).
    
3.  Write an Inngest function that listens for that event and runs your AI logic — for example, an agent that reviews the diff and posts comments back to the PR.
    
4.  Because the webhook handler just forwards the event and returns, GitHub's webhook timeout is never at risk, no matter how long the actual review takes.
    
5.  The flow end-to-end looks like: **GitHub PR → Webhook → Inngest → AI Agent → Code Review** posted back to the PR.
    

**Event-driven trigger flow:**

![](https://cdn.hashnode.com/uploads/covers/65a8dbbd615555c5bcb87c93/4590f9ed-ccec-4837-b809-5dac5732616b.png align="center")

**Pseudocode — webhook receiver:**

```javascript
// this endpoint must respond fast — no AI work happens here
function onGithubWebhook(payload):
    if payload.action == "opened":
        inngest.send({
            name: "github/pull_request.opened",
            data: { 
                prId: payload.pull_request.id, 
                diffUrl: payload.pull_request.diff_url 
            }
        })
    return response(200)   // GitHub is happy, timeout risk is zero
```

**Pseudocode — Inngest function triggered by that event:**

```javascript
inngest.createFunction(
    { id: "review-pull-request" },
    { event: "github/pull_request.opened" },
    function(event):
        diff = step.run(
            "fetchDiff", 
            () => fetchDiff(event.data.diffUrl)
        )

        review = step.run(
            "runReviewAgent", 
            () => reviewAgent.run(diff)
        )

        step.run(
            "postComment", 
            () => postGithubComment(event.data.prId, review)
        )
)
```

* * *

## Agents Inside Background Workflows

Agents make this pattern more important, not less. An agent that plans, calls tools, checks its own output, and loops until it's satisfied is essentially a background workflow already — it just doesn't know it yet. Running an agent loop inside a framework like Inngest means each tool call and each reasoning step can be checkpointed the same way a simple pipeline step would be. If the agent is 8 steps into a 12-step task when something fails, you resume the agent, you don't restart its train of thought — and you don't re-pay for the tokens it already spent getting there.

**Agent loop as checkpointed steps:**

![](https://cdn.hashnode.com/uploads/covers/65a8dbbd615555c5bcb87c93/8b1e4150-87f8-48e6-b2a6-c8b3985bc843.png align="center")

**Pseudocode — agent loop with per-step checkpointing:**

```javascript
function runAgent(task):
    state = initState(task)
    while not state.done:
        stepId = "agent-step-" + state.iteration
        action = step.run(
            stepId + "-plan", () => planNextAction(state)
        )

        result = step.run(
            stepId + "-tool", () => callTool(action)
        )

        state = step.run(
            stepId + "-update", () => updateState(state, result)
        )

    return state.finalResult

// if this crashes at iteration 8 of 12, resuming replays iterations 1-7
// from checkpoints instead of re-calling the model 7 more times
```

## Real-World Examples

*   **Code review bots** triggered by PR webhooks, running an agent that reads the diff, checks it against project conventions, and comments.
    
*   **Document processing pipelines** that extract, chunk, embed, and summarize large files without holding an HTTP connection open.
    
*   **Customer support triage** where an incoming ticket event triggers a workflow that classifies, drafts a response, and only pings a human when the agent's confidence is low.
    
*   **Data enrichment jobs** where an event (new signup, new record) kicks off a multi-step AI pipeline to enrich and score the record asynchronously.
    

* * *

## Developer's Quick Reference

*   Long-running AI tasks (multi-step calls, document processing, agent loops) don't belong in a synchronous request — decouple trigger from execution.
    
*   Retries alone aren't enough if a crash loses your place entirely; you need **checkpointing**, not just retry-with-backoff.
    
*   Inngest checkpoints each step, so a crash mid-workflow resumes from the last completed step instead of recalculating everything — which directly saves token spend on AI calls.
    
*   Webhooks (e.g., GitHub PR events) are a clean way to trigger these workflows without exposing your AI logic to webhook timeout limits.
    
*   Agents fit naturally into this model — each reasoning/tool-call step can be checkpointed just like any other workflow step.
    

## Footer: The Behind-the-Scenes

The thing that kept nagging at me while thinking through this was reliability. It's easy to get excited about model quality — better reasoning, better context windows, better tool use — but none of that matters if the system around the model isn't dependable. If it's not reliable, even state-of-the-art AI won't keep people around; they'll just leave for something that works.

The old habit was to write retries with backoff and call it a day. That's fine when the failure is "the API call bounced." It's not fine when the *server itself* crashes, because then there's no checkpoint of where things were — you're not retrying a step, you're recalculating the whole thing. With regular software that's wasteful. With AI, it's genuinely expensive, because every model call you redo is tokens you're paying for a second time.

That's the piece that made Inngest click for me: it's not just a job queue, it's a way to save intermediate progress. Each step gets checkpointed, so on a crash you resume instead of restart. It's a small shift in mental model — from "the workflow either finishes or it didn't" to "the workflow remembers exactly how far it got" — but it changes the economics of running AI pipelines in production.
