Straight metallic shapes with a blue gradient
6 min read

AI Workflows Need Durable State, Not Longer Prompts

Mehdi Rezaei
Mehdi
Author
ai
nextjs
Engineering

Most AI features look reliable in a demo because the happy path is short. One prompt comes in, one answer comes out, and nobody asks what happens when the tool call times out, the user refreshes halfway through, or the model produces something that should trigger a human review instead of another API request.

That is why so many teams end up blaming model quality for failures that are really workflow design failures. The model is only one moving part. The brittle piece is usually everything around it: no durable state, no resumability, no idempotency, no audit trail, and no clean boundary between fast request work and slow background work.

If you are building AI features in Next.js right now, the upgrade I would make is not a longer system prompt. It is a durable workflow layer backed by Postgres.

The real failure mode is workflow amnesia

The failure mode in production is rarely that the model cannot answer at all. The common failure mode is that the surrounding application has no memory of what was attempted, what succeeded, what partially failed, and what should happen next.

Teams often start with a single request handler that calls a model, maybe one tool, maybe two, and then writes a final result. That is fine for low-value drafting or throwaway assistants. It breaks down the moment the workflow has money, user trust, or side effects attached to it.

The second you send an email, write to a CRM, modify a ticket, generate a report, or kick off a multi-step enrichment flow, you are no longer building chat. You are building an operational system. Operational systems need state you can inspect and recover.

What durable actually means

Durable does not mean complicated for the sake of it. It means every meaningful run has an identity, a status, a history of steps, and a place to resume from. If the process dies halfway through, you should be able to look at one row or one trace and know what happened without reconstructing the story from logs and guesswork.

For most JavaScript teams, Postgres is already the most boring place to put that state, and boring is exactly what you want here. You do not need a mysterious orchestration layer before you need a table that tells you which job is waiting, running, blocked, failed, or complete.

The shape I like is simple: a workflow run record, a step record, and a queue worker that advances the state machine. The web request creates the run and returns quickly. The worker performs the slow or failure-prone work. The UI reads progress from the database instead of pretending the original request can safely own the whole lifecycle.

A shape worth shipping

A minimal design usually includes a workflow_runs table with a business key, current status, input payload, output payload, retry count, timestamps, and an optional resume token. Then add workflow_steps for retrieval, model invocation, validation, human approval, and external side effects.

type WorkflowStatus = 'queued' | 'running' | 'waiting_human' | 'retrying' | 'failed' | 'completed'


interface WorkflowRun {

id: string

workflow: 'lead_enrichment' | 'content_review' | 'support_triage'

status: WorkflowStatus

input: Record<string, unknown>

output?: Record<string, unknown>

error?: { code: string; message: string }

retryCount: number

resumeAfter?: string

createdAt: string

updatedAt: string

}


interface WorkflowStep {

runId: string

step: 'retrieve' | 'call_model' | 'validate' | 'side_effect' | 'await_review'

status: 'pending' | 'running' | 'failed' | 'completed'

startedAt?: string

finishedAt?: string

payload?: Record<string, unknown>

}


That split gives you something most AI products are missing: a visible contract. You can answer basic questions immediately. Which runs are stuck? Which step fails most often? Which failures are safe to retry? Which ones need a person? Without that structure, every incident turns into archaeology.

Keep the request path boring

The request path in Next.js should stay boring. An action or route handler validates input, creates a run row, enqueues work, and returns a run id. The client can poll, subscribe, or refresh server-rendered status using that id. None of this requires the model call to live inside the original user request.

That separation improves more than reliability. It also fixes latency. A user does not need to sit inside a 35 second request while your app retrieves context, calls multiple tools, validates structured output, and writes downstream changes. Fast acknowledgement plus durable progress is a better product than fake synchronous magic.

It also forces a healthier boundary between planning and execution. The model can propose the next action, but the application decides whether that action is allowed, whether prerequisites are satisfied, and whether a side effect should happen now, later, or only after review.

Where teams get this wrong

The common mistake is letting the model own too much control flow because it feels elegant at first. In production, that elegance becomes ambiguity. If the model is effectively deciding retries, side effects, and completion criteria by implication, you do not have a robust workflow. You have a persuasive string generator driving your operations.

Use Postgres for workflow state, not for every token. Store inputs, normalized outputs, structured tool results, step metadata, and decision checkpoints. Do not dump every transient fragment into the relational core unless it has debugging or compliance value. Durable does not mean hoarding.

For streaming UX, you can still stream partial model output when it is useful. Just do not confuse streamed text with durable completion. A run is complete when your workflow state says it is complete, not when the first visible answer appears on screen.

The same rule applies to agents. An agent loop can be useful, but it needs a leash. Put explicit limits around tool budgets, step counts, allowed side effects, and escalation points. If your system cannot explain why it stopped, retried, or asked for human input, it is not ready for work that matters.

When the extra engineering is worth it

This pays off quickly in workflows with delayed value: research pipelines, support triage, onboarding flows, CRM enrichment, content review, batch generation, and internal copilots that do more than autocomplete. All of them benefit from resumability and auditability more than they benefit from one extra point of model cleverness.

There is a tradeoff, of course. A durable workflow layer is more engineering than a single route handler. You need schemas, status transitions, retry policy, backoff, and observability. But that cost buys you something real: incidents become diagnosable, side effects become safer, and product behavior stops depending on whether one long request happened to survive the network.

If your AI feature is a low-stakes drafting box, do not overbuild it. But if it touches customer data, expensive compute, business actions, or multi-step logic, the durable version is usually the cheaper version once you account for operational drag.

The takeaway

Good production AI systems do not feel magical from the inside. They feel explicit. You can inspect them, replay them, pause them, and reason about them under pressure. That is what makes them trustworthy.

So if you are looking for the next meaningful upgrade in an AI-heavy full-stack app, stop polishing prompts for a day and design the run lifecycle instead. The prompt still matters. The workflow matters more.

Share this article