A working proof of the agentic-workflow pattern proposed for the Empire AI lab. Three real competitor pages, eleven Cloudflare services, full audit trail, gated behind Application Security. Click through the tabs below to see how it works, what it costs, and what scaling it would require.
Application Security gates the hostname. Email-allowlisted users only. No customer PII in scope. Source policy: public pages, robots respected.
Workflows for durable orchestration. R2 with zero egress. Workers AI for inference. The same pattern runs at 3 pages or 300.
Every model call routes through AI Gateway. Every run, signal, cost, and latency is persisted to D1. Workers Logs covers the rest.
Sandbox-deployed under a separate Worker, D1, R2, and AI Gateway. Nothing wired to Empire's internal systems.
| Service | Role in this app |
|---|---|
| Workers | Compute. Hosts the cron handler, the API, the orchestrator, and serves the dashboard from a single deployment. |
| Workflows | Durable orchestration. Each step retries independently — one failed competitor fetch doesn't kill the run. |
| Cron Triggers | Fires the morning workflow at 10:00 UTC (06:00 ET) every day. |
| Browser Rendering | Real headless Chrome instances fetch competitor pages, including SPA-rendered content. |
| Workers AI | Llama 3.3 70B for structured signal extraction and (when selected) synthesis. |
| AI Gateway | Unified observability. Every model call — Workers AI and Claude — logged + costed in one dashboard. Unified Billing for Anthropic. |
| R2 | Object storage. Raw competitor HTML, normalizer outputs, and final brief JSON. Zero egress fees, ever. |
| D1 | SQL audit log. Three tables: runs, signals, model_calls. Queryable record of everything the agent did. |
| KV | Configuration store for competitor URL list and prompt versions. |
| Workers Logs | Real-time stack traces and console output. Powers the wrangler tail debugging loop. |
| Application Security | Cloudflare Access policy on the hostname. Email-allowlisted users only; everyone else is blocked at the edge before the Worker runs. |
runs/{runId}/raw/{competitor}.html — raw page snapshot. runs/{runId}/normalize/{competitor}.json — normalizer raw output. runs/{runId}/brief.json — final synthesized brief.runs (workflow runs), signals (per-competitor extracted signals), model_calls (every Workers AI / AI Gateway call with cost and latency).This log exists so anyone reviewing the demo can see what was attempted, what broke, and what was fixed. Governance is the record, not the intention.
Picked the Community Competitive Intelligence use case from the eight in the Empire AI framework. Anchor community: Empire Avalon, Caledonia. Competitors: Losani, Mattamy, Branthaven — three Hamilton-area builders overlapping the catchment.
Every primitive runs on Cloudflare: Workers, Workflows, Browser Rendering, Workers AI, AI Gateway, R2, D1, KV, Cron, Logs, Application Security. Workflows specifically chosen so the orchestrator is durable and steps retry independently.
Project structure, wrangler config with 11 bindings, D1 migration applied to remote DB in 1.08 ms. Three CLI commands provisioned D1, R2, and KV.
23 KB worker, all bindings wired, custom Workflow class registered, cron schedule active.
First end-to-end run failed. Used the REST API URL pattern via the binding's fetch. The binding is meant for puppeteer/playwright; the REST API is a separate auth surface.
@cloudflare/puppeteerRewrote the extractor: puppeteer.launch(env.BROWSER) → newPage() → goto() → content(). Extractions succeeded on the next deploy.
"Please configure AI Gateway in the Cloudflare dashboard." First-time gateway names must be created before env.AI.run({gateway:{id}}) will route to them.
Created empire-avalon-demo gateway. Authorized the Cloudflare API MCP so subsequent gateway management — provider config, Unified Billing, rate limits — runs programmatically.
All four model calls visible in AI Gateway. Workflow status: complete. Brief in R2. Audit row in D1. Cost: $0.0012. Wall time: 40 seconds.
Pipeline running, signals empty. Inspected R2-archived HTML directly: Losani's homepage is a SPA that hadn't hydrated by domcontentloaded; Mattamy's URL was a 404 page; the 8B normalizer was returning {"signals":[]} regardless of input.
Three API calls: zone lookup, custom-domain attachment with TLS, Access app + email-allowlist policy. Two minutes including cert provisioning. Whole hostname behind Application Security.
Switched waitUntil from domcontentloaded to networkidle2. Added a 4-second hydration buffer. Reverted Mattamy to a URL path that returns content.
The 8B fast model over-constrained under JSON mode and short-circuited. The 70B follows the schema from prompt instructions alone. Cost per page rose from $0.0002 to $0.001 — still trivially cheap. First 70B run extracted 10 real signals with prices, evidence, and confidence scores.
Inspected the synthesizer's raw output in R2: the Workers AI binding wraps structured responses as {response: {sections}, tool_calls, usage}. The parser was looking for sections at the wrong nesting level.
11 signals, 3 brief sections, real prices ($499K Losani Benchmark Towns, $700–$800K Branthaven), recommendations grounded in Empire Avalon Caledonia. 30-second runtime. $0.0041 total cost.
Wired the AI Gateway authorization secret. Clicked the Claude toggle in the dashboard. Same workflow re-ran with synthesis routed to Claude Sonnet 4.6 via Unified Billing — no separate Anthropic key, Cloudflare bills the upstream provider spend. 13 signals, $0.0144. Brief quality jumped — Claude caught a "Losani launches HST Relief incentive" headline the Workers AI run had missed.
Adopted the wine, navy, orange, and cream palette from empirehomes.com. Playfair Display + Inter for editorial feel.
Cloudflare services moved to a dedicated Stack tab so the Daily brief view runs full-width. External source links added to every brief section. Tooltips on the provider toggle and Run workflow button. Hash-routed tabs so each view is its own URL.
Clicking any row in Run history opens a slide-in drawer with the full run detail: brief sections, every signal extracted, every model call with cost and latency. Backdrop and Escape close it.
Added a /api/stats endpoint that returns average cost per run by provider, sourced from D1. The About tab pillar cards now show real averages — "Avg over N runs" — instead of static placeholders.
| Surfaced | Lesson | Fix time |
|---|---|---|
| Wrong Browser Rendering pattern | The env.BROWSER binding is for puppeteer/playwright; the REST API is a separate auth surface. | 2 min |
| AI Gateway didn't auto-create | First-time gateway names must exist before env.AI.run({gateway:{id}}) routes. | 90 sec |
XSS via raw innerHTML | Security review caught LLM output being interpolated raw. Added an escape helper and a delta allowlist. | 5 min |
| SPA returned 404 before hydration | domcontentloaded fires before client-rendered listings; networkidle2 + 4s wait fixed it. | 5 min |
| Llama 3.1 8B fast degenerated under JSON mode | Over-constrained inference short-circuits; 70B follows the schema from prompt alone. | 8 min |
| Workers AI envelope wrapping | The binding nests structured output under .response; the parser was at the wrong level. | 4 min |
This demo proves the architecture. Standing up the Empire AI lab properly requires the operating model around it.
Open the live dashboard. Click Run workflow to fire a fresh competitive brief in real time. Toggle the provider between Workers AI and Claude to compare the selected-model estimate and output quality. Click any row in Run history to see the full per-run detail — brief, signals, model calls, cost.