TechFabric engineers work with our clients to put AI to work and bring real, measurable value to their business. We're at the forefront of applied AI, where new tactics, strategies, models and agents go into production every day and their impact is measured straight away. We work with companies in manufacturing, retail, supply chain, distribution, insurance and more.
Recently we started working with companies building go-to-market products, helping them use AI to produce content that is real, fact-checked and true to their brand voice. That work needs a suite of AI agents with reliable, inexpensive real-time search and reading. Our first agents searched through DataForSEO's search results and read pages with a reader we wrote ourselves. Within a week we had moved search to Cloudflare's Web Search API. When Cloudflare shipped web_fetch on 11 October, we had it measured against our own reader and running in production the next day.
The measurement is what convinced us. Our blog writer has a fact-check that reads every claim in a draft against the pages the writer used, and cuts any number that appears in no source. Beside web_fetch, our own reader turned a Databricks docs page's SQL into system . billing . usage and a research PDF into 338,000 characters of binary. The fact-check had been reading that.
This is what we measured, what we changed in our agents, and the two traps we hit on the way.
What our agents were doing before
Our writer is a TypeScript agent that runs in GitHub Actions on our own harness, with Claude Opus 5 behind Cloudflare's AI Gateway. Search already went through Cloudflare, because the writer calls the Web Search REST endpoint directly. Reading was ours, a plain fetch and a tag stripper that turned HTML into text. It was fast and it had no dependencies, and we'd never looked closely at what it produced.
The comparison
We ran both readers on six sources on 12 October. The character counts are what the agent actually receives.
| Source | Our reader | web_fetch | What changed |
|---|---|---|---|
| Databricks docs (AI Functions) | 15,055 | 15,280 | Ours flattened SQL and lost headings and lists. web_fetch kept the code blocks and the structure. It also took 1.9 s against our 0.16 s. |
| Databricks blog post | 11,156 | 12,421 | The same body. web_fetch added the title, description, publish date and authors. |
| A research PDF (the CIDR Lakehouse paper) | 338,275 of binary | 51,239 | Ours returned raw PDF bytes. web_fetch returned the paper. |
| A JavaScript-heavy pricing calculator | 192 | 392 | Both got an empty shell. Neither runs JavaScript. |
| A vendor site that serves Markdown to agents | 24,936 | 20,915 | web_fetch asked for the site's Markdown and used it directly. |
| A short announcement page | 2,906 | 3,104 | About the same, with headings kept. |
The PDF row is the one that mattered most to us. A good share of what our engineers cite (papers, vendor whitepapers, compliance documents) arrives as PDF, and our writer had never once read one. The SQL row mattered to the fact-check: a claim about system.billing.usage can't be checked against text that says system . billing . usage.
The JavaScript row is the honest limit. If a page renders its content in the browser, web_fetch sees what we saw, and that is nothing. We'd still reach for a headless browser there.
How we wired it in
web_fetch ships as a tool for Cloudflare's own agent runtimes, but the SDK also exports the function underneath it, fetchWeb(), which needs no Durable Object. That's what we use, because our agents run in GitHub Actions, not in a Worker.
fetchWeb() expects a Workers AI binding for its Markdown conversion. Outside a Worker there's no binding, so we pass an object with the same shape that calls the REST endpoint instead:
export function markdownBinding(account: string, token: string) {
return {
async toMarkdown(file: { name: string; blob: Blob }) {
const form = new FormData();
form.append("files", file.blob, file.name);
const res = await fetch(`https://api.cloudflare.com/client/v4/accounts/${account}/ai/tomarkdown`, {
method: "POST",
headers: { authorization: `Bearer ${token}` },
body: form,
});
const data = await res.json();
if (!res.ok || !data.success) throw new Error(`tomarkdown answered ${res.status}`);
return data.result[0];
},
};
}
The token is the one our agents already use for AI Gateway. Conversion only needs Workers AI Read, so nothing new had to be issued. Converting HTML, PDF and Office documents is free. Images would use paid models, and fetchWeb() never sends them.
We kept our old reader as the fallback. A conversion failure, a timeout or a Cloudflare outage drops to the plain fetch, with the same refusals and the same 5 MB cap, so an outage on Cloudflare's side never blocks a draft. What fetchWeb() refuses on purpose (private hosts, credentials in a URL, an https page redirecting to http, an oversized body) stays refused. The fallback would only do those things worse.
Two traps
Digits inside URLs counted as sources. Our fact-check treats a number as sourced if it appears in the pages the writer read. web_fetch keeps links, which is right for a reader, but every link target is full of digits: dates, IDs, version paths. A figure the writer invented could "match" a number in someone's URL. We now strip every URL from what the model reads before the check runs, code blocks included.
The SDK pulled our model library forward. Adding the agents package would have upgraded pi-ai under our harness from 0.83 to 1.x. We'd seen that upgrade break our AI Gateway connection once before, quietly, with every model call failing. A small .npmrc in each agent's folder keeps the existing version, and the lockfile gained 43 packages without changing any we already had. We'd check the lockfile diff on any SDK added to an agent that runs in production.
What we left alone
Search. Cloudflare's search client sends exactly the request our writer already sends to the Web Search API, so swapping it in would change nothing. Search is billed through AI Gateway credits at the provider's list price, $7 per 1,000 queries on Exa, the provider we use, and results aren't cached, so a repeated search is billed again. That's worth knowing before an agent searches in a loop.
We also haven't moved our harness itself onto the new tools yet. The harness integration exists and passes its tests, but using it means leaving the harness version the AI Gateway trap above pins us to. We'll do that move on its own, carefully, and not as a side effect of a feature.
What it changed
The fact-check now reads the sources the way an engineer would, with the code intact and the PDFs readable. That's a quieter change than a new model, and it's the kind we'd make first. A model is only as careful as the text in front of it, and since we started, our writer had been checking claims against pages it couldn't fully see.