DEV Community

Oğuzhan Tüsen
Oğuzhan Tüsen

Posted on

6 real problems building an AI agent on Cloudflare Workers (and the fixes)

These are the technical problems we hit building Glotier, an agent that runs on Cloudflare Workers. Each one is documented from git history, commits, and code: the problem, the root cause, the fix, and the lesson. Nothing invented.

  1. Cloudflare Workers have no persistent filesystem, so a Python framework that stores state in local SQLite cannot run there

Context. Cloudflare does support Python on Workers. Python code runs on Pyodide, which is CPython compiled to WebAssembly and interpreted inside the V8 isolate. What Workers do not provide is a persistent local filesystem. A Python agent framework that keeps its state in local SQLite files cannot run there, because those files have nowhere to live. Pyodide also only supports pure-Python packages and packages with WebAssembly builds, which rules out anything depending on arbitrary C extensions.

What we did. Built the agent Cloudflare-native. A Worker acts as the orchestrator and calls the model providers directly over HTTP, with Cloudflare D1 for persistence and focused single-purpose workers that return structured JSON.

Lesson. "Does this language run on Workers?" is the wrong question, because Python does. The right questions are: does the framework need a persistent filesystem (Workers has none, so state belongs in D1, KV, or R2), and are its dependencies available as pure-Python or WebAssembly builds? A framework that stores state in local files will not port, regardless of language.

  1. The dev.to (Forem) API returns 403 to a Cloudflare Worker without a User-Agent

Problem. Saving a draft to the dev.to (Forem) API returned 403 Forbidden from our Cloudflare Worker, with a valid API key.

Root cause. dev.to rejects requests that arrive without a User-Agent header, and Cloudflare Workers' fetch does not send a default one, so the failing request went out with no User-Agent at all. Isolating the header confirmed it: an empty User-Agent returned 403, while any non-empty value returned 200.

Fix. Set an explicit User-Agent and the Forem Accept header on every call:

jsconst DEVTO_UA = "Glotier/1.0 (+https://glotier.com)";
// on every fetch to the Forem API:
headers: { "api-key": apiKey, "User-Agent": DEVTO_UA, Accept: "application/vnd.forem.api-v1+json" }

Lesson. Cloudflare Workers' fetch is minimal and sends no default headers. When a third-party API returns 403 from a Worker but works from other clients, a missing User-Agent is a prime suspect. Set the headers the API expects explicitly.

  1. The model kept inventing facts, and switching to a stronger model did not fix it

Problem. The content agent wrote confident, wrong details. For a build-in-public post about a game, it stated the game was built in Unity (it is Godot) and that it had no server (it has a Supabase leaderboard).

First fix that did NOT work. Moving generation to a stronger paid model. It still invented features, code, and stack details. The problem was not model capability; we were asking it to write about specifics it had never been given.

Fix that worked.

Made grounding notes required. With no real notes, the worker returns no draft instead of a plausible fiction:

jsconst notes = (brief.ownerNotes ?? "").trim();
if (!notes) return { draft: null, honest: true };

Then tightened the prompt to forbid unstated specifics (tools, libraries, languages, frameworks, techniques the notes did not mention), and added a separate fact-check pass: an independent step that sees only the source notes and the claims from the draft, and flags any claim the source does not support.

Lesson. Hallucination is a grounding problem, not a model-tier problem. A bigger model fabricates just as confidently about things it was never told. Feed it real source material, forbid unstated specifics, and verify the output against the source. If there is no source, produce nothing.

  1. Our GEO audit gave sites credit for files that did not exist

Problem. The audit checks whether a site has an llms.txt, a /.well-known/ai.txt, and /ai/summary.json. Single-page apps passed all of them without having any.

Root cause. Many SPAs answer every path, including ones that do not exist, with a 200 and the app's HTML shell (a client-side 404). Our fetch saw 200 OK with a body and counted the file as present. It was HTML, not the expected text or JSON.

Fix. Do not trust the status alone; check that the body is the shape you asked for. A file counts only if it does not look like an HTML fallback:

jsconst looksLikeHtml = (t) => /^\s*(<!doctype html|]|])/i.test(t);
const looksLikeJson = (t) => /^\s*[[{]/.test(t.trim());
// llms.txt / ai.txt counts only if present AND !looksLikeHtml(body)
// summary.json counts only if present AND looksLikeJson(body)

Lesson. 200 OK does not mean "this resource exists." Any audit or crawler that probes for specific files has to content-sniff, or SPAs will hand it a passing grade it did not earn.

  1. Our "is this brand named by AI" check always said yes

Problem. We ask a grounded model whether AI names a product in its space. It reported "mentioned" for nearly everything, including a product with no real AI visibility. A false "you are visible" is the worst error for a visibility tool.

Root cause. The query seeded the product's own name: I'm looking for apps like "". What are the best options and alternatives?. Answering naturally, the model repeats the name in its reply, and our check then found the name and marked it mentioned. The question guaranteed the answer.

Fix. Ask by category, never by name, then check whether the product surfaces on its own: What are the best apps right now?, with the product name nowhere in the prompt. If it appears, it is genuinely named. If not, it is honestly "not named yet," and the competitors it named instead are useful too.

Lesson. If you use an LLM to measure whether something is known, make sure your prompt is not what puts the answer there. Any term in the question echoes in the response. Test for presence with a question that does not contain the thing you are testing for.

  1. Adding SoftwareApplication schema broke Google's rich-results validation

Problem. We added SoftwareApplication JSON-LD to describe the product. Google's rich-results validation then reported an error on every page carrying it.

Root cause. SoftwareApplication requires aggregateRating or review to be eligible as a rich result. We had no real ratings, and we will not invent star ratings to satisfy a validator.

Fix. Removed SoftwareApplication and described the entity with types valid without ratings: Organization plus WebSite, and a FAQPage from real questions and answers. Honest structured data that still improves how well AI engines parse the site. Re-add SoftwareApplication only once there are real ratings to attach.

Lesson. Some schema.org types have required properties for a rich result (SoftwareApplication wants a rating). Do not bolt on a type you cannot honestly fill: a faked aggregateRating risks a manual action, and a validation error on every page costs more than the missing type is worth. Pick the richest type you can populate truthfully.

Top comments (0)