DEV Community

Mike Clarke
Mike Clarke

Posted on

Your AI generated hundreds of pieces of content. None of it ever shipped.

Your AI generated hundreds of pieces of content. None of it ever shipped.

Key Takeaways

  • A queue in a ready state is not done. Done means consumed and delivered.
  • Generation and publication are two separate stages. An automated bridge between them is not optional.
  • Any queue without a consumer and a depth alert is a silent failure waiting to accumulate.
  • "The work looks done" is the most dangerous state in an autonomous system.

In ARIA — the autonomous content operations system we run at Elevare Digital — we watched two queues quietly fill up over time. Content was being generated. It was landing in the database in a ready state. Every upstream metric looked fine.

None of it ever reached the destination. Not a single item.

The root cause was not a bug. It was a design assumption that never got questioned: publication was a manual step, and nobody ran it.


What the system looked like

ARIA's content pipeline has two distinct stages. Stage one: generation. The agents do their work, write the content, and mark the row ready. Stage two: publication. Something takes those ready rows and pushes them out.

Stage one was fully automated. Stage two was manual by design — a decision that made sense early on when we wanted a human in the loop before anything shipped publicly.

The problem is that "manual by design" eventually became "never runs." There was no consumer process watching the queue. There was no alert watching the queue depth. The count just grew.

-- What we were looking at in Supabase:
-- content_queue table, simplified

CREATE TABLE content_queue (
  id uuid PRIMARY KEY DEFAULT gen_random_uuid(),
  content_id uuid REFERENCES content(id),
  status text NOT NULL DEFAULT 'pending',
  -- statuses: pending | generating | ready | published | failed
  created_at timestamptz DEFAULT now(),
  updated_at timestamptz DEFAULT now()
);

-- The generation side was automated and ran correctly.
-- Items moved from 'pending' -> 'generating' -> 'ready'.
-- Nothing moved them from 'ready' -> 'published'.
-- The query below returned a number that kept growing:

SELECT COUNT(*) FROM content_queue WHERE status = 'ready';
Enter fullscreen mode Exit fullscreen mode

That count was not a success metric. It was a backlog. We were reading it as one and ignoring the other.


Why this is easy to miss

When you look at a pipeline dashboard and see items flowing into ready, it feels like progress. The agents are working. The generation numbers are up. The logs are clean.

The gap between ready and published is invisible unless you specifically instrument it. We hadn't. There was no metric on queue depth over time, no alert threshold, nothing that would fire if ready stopped draining.

This is the specific failure mode:

// Pseudocode for what ARIA's generation side was doing — correctly:
async function generateContent(jobId: string) {
  await updateStatus(jobId, 'generating');
  const content = await runGenerationAgents(jobId);
  await saveContent(content);
  await updateStatus(jobId, 'ready'); // <-- work stops here
  // Nothing downstream is listening. This is a dead end.
}

// What the publication side needed but didn't have:
async function publishConsumer() {
  // This function did not exist as an automated process.
  // It existed as a manual script someone had to remember to run.
  const readyItems = await getItemsByStatus('ready');
  for (const item of readyItems) {
    await publish(item);
    await updateStatus(item.id, 'published');
  }
}
Enter fullscreen mode Exit fullscreen mode

The generation function had no idea there was no consumer on the other end. It just kept doing its job.


The fix: every queue gets a consumer and a depth alert

The rule we applied after this: no queue exists without two things attached to it.

  1. An automated consumer that drains it.
  2. A depth alert that fires if items in a terminal-waiting state (like ready) exceed a threshold for too long.

The consumer can be a Deno edge function on a schedule, a Supabase pg_cron job, a webhook trigger — the mechanism matters less than the guarantee that something is watching and pulling.

-- pg_cron job that checks for stranded 'ready' items
-- and alerts if the queue hasn't drained

-- First, a view that makes the staleness visible:
CREATE VIEW stranded_content AS
SELECT
  id,
  content_id,
  status,
  updated_at,
  now() - updated_at AS time_in_state
FROM content_queue
WHERE
  status = 'ready'
  AND updated_at < now() - INTERVAL '2 hours';

-- Then a function that pages someone if this view has rows:
CREATE OR REPLACE FUNCTION alert_on_stranded_queue()
RETURNS void AS $$
DECLARE
  stranded_count integer;
BEGIN
  SELECT COUNT(*) INTO stranded_count FROM stranded_content;

  IF stranded_count > 0 THEN
    -- Call your alerting mechanism here.
    -- We use a Supabase edge function that posts to our ops channel.
    PERFORM net.http_post(
      url := current_setting('app.alert_webhook_url'),
      body := json_build_object(
        'message', format('%s items stranded in ready state', stranded_count),
        'severity', 'warning'
      )::text,
      headers := '{"Content-Type": "application/json"}'
    );
  END IF;
END;
$$ LANGUAGE plpgsql;

-- Schedule it:
SELECT cron.schedule(
  'check-stranded-queue',
  '*/30 * * * *', -- every 30 minutes
  'SELECT alert_on_stranded_queue()'
);
Enter fullscreen mode Exit fullscreen mode

The threshold and interval depend on your expected throughput. The point is that a human gets paged before the count becomes embarrassing.


The lesson isn't about the bug

This wasn't a bug in the traditional sense. The code did exactly what it was written to do. Generation worked. The ready status was accurate. The manual publication step was documented.

The failure was architectural: we treated "generated" as a proxy for "shipped" without building anything to enforce the difference.

In an autonomous pipeline, every state transition needs an owner. If the transition from ready to published requires a human action, then the system needs to demand that action — not wait quietly while the queue fills.

A full queue is not progress. It's generated work that went nowhere. The two look identical from the outside until you add the one metric that matters: how long has this item been waiting, and who knows about it?


— Mike Clarke, founder of Elevare Digital.

Top comments (0)