Case study

Websites maintained by AI agents

Four websites on WREN are written and kept up to date by AI coding agents: the agents write the pages, the build and deploy scripts, and much of the content. A person asks for changes and approves releases. This is how those agents deploy, check and release the sites today, what they had to work around, and how WREN's current features fit that kind of work.

Written October 2026 from the sites' code and deploy scripts, their data in a WREN backup taken on 2026-10-05, and the Claude Code sessions in which they were built. Athlete names, keys and personal details are left out. We only describe what the sessions show was actually done.

The sites

Each site lives in its project's own WREN org, with that org's API key. All four were built in Claude Code sessions, and the agents run the deploys themselves.

SiteWRENWhat the agents doSize and time
Tournament data at data.tkd-scores.com: World Taekwondo entry lists with ranking seedstree site (13 files) and tree events (92 documents)Wrote the single-page app, help, Olympic and LLM pages, and the scraping pipeline that refreshes the data on a scheduleApp page: 43 versions, 2026-04-09 to 2026-09-17. Data: 4,094 versions on 84 days
Fight analysis: videos and scoreboard-based write-ups of Swiss athletes' fightstree combats, collections fights and athletesBuilt the video and OCR pipeline, wrote reviews, build and publish the page; a daily job republishes it378 fight documents with 3,769 versions; site pages 127 versions, 2026-09-09 to 2026-10-05
Club Hub UI: a style guide and component library for a club management apptree clubhub-ui, served on its own domain by a WorkerWrote the components, guides and deploy script; deployed with labels149 files, 278 file versions, all on 2026-10-02
bootOS: the docs and marketing site of an experimental operating systemtree bootos (9 pages)Wrote the site generator and every page; deploy after each change134 file versions, 2026-09-23 to 2026-09-27

Other WREN projects work the same way and have their own case studies: the live results site in the tournament tracker, the data and pipeline behind data.tkd-scores.com, the MAE shop's stock tracker, and the task tracker those agents log their work to. Other trees in these orgs, such as earlier club-site drafts and design mockups, are left out: we have no record of how they were made.


Part A

As built

How an agent ships a change

The four sites share a pattern. A person asks for a change in plain words. The agent edits the source in a local repository, runs its checks, and runs a deploy script it wrote itself. Every script talks to WREN's REST API with a key read from an environment variable or a .wren file, uploads changed files to a binary collection, assigns tree paths, and makes sure a public read rule exists. Then the agent fetches the public URL without credentials to see what visitors see, and reports back with the link.

How the agents ship, as built A person asks for a change. A Claude Code session edits the repository, runs tests and a browser harness, and runs its own deploy script, which uploads files with an API key, assigns tree paths and checks the public rule. Scheduled jobs run the same scripts. The agent then reads the public URL without credentials, through a Cloudflare Worker or directly, and reports. Person asks, approves "go live" Claude Code session edit, test, screenshot run own deploy script key from env or .wren Scheduled jobs same scripts, daily WREN · one org per site binary collection + tree public rule on the tree mostly no label filter: each upload is live at once Cloudflare custom domain Worker or the WREN URL directly Agent checks public URL, no key bytes, overflow, console
Figure 1. As built: the agent deploys with its own script and checks the result the way a visitor would. In three of the four sites, an upload is visible the moment it lands.

In the sessions we have, no agent used WREN's MCP endpoint, and no MCP sign-in was ever granted. The agents read WREN's llms.txt, llms-full.txt and /openapi.json, and wrote small REST clients. Where the docs and the service disagreed, the agents wrote down that "the service wins" and coded against what it did.

A data site with a staged redesign

data.tkd-scores.com is a single-page app plus help, Olympic-ranking and LLM-usage pages, stored as files in the site tree of the project's own org and served on the custom domain through a Cloudflare Worker. Its data lives in the events tree, which a scraping pipeline refreshes on a schedule (daily, and fully every three days); the data side has its own case study. The app and the pipeline are maintained by Claude Code sessions: first on a Mac, and since 2026-09-16 on a Windows machine, where all 18 commits of the project's repository carry a Claude co-author line.

The September redesign shows how the agent released without a release feature:

  • Staging by copy. Offered options, the owner picked "Stage at data.tkd-scores.com/preview/". The agent uploaded new copies of the pages and assigned them under /preview/ in the same live tree, so the owner could check them on a phone.
  • Release by overwrite. After "ok, lets go live with this", the agent uploaded the new app over the live document. The app page went from version 41 to version 43 that day.
  • Cleanup by hand. It then deleted the 12 /preview/ tree paths and the 11 staging documents, noting that "Wren's DELETE endpoints work".
  • Rollback as a note. The rollback plan is written in the agent's memory: "git tag spa-pre-redesign / Wren v41".
  • Checks. A Puppeteer harness loads 7 routes at phone, tablet and desktop width in light and dark mode, and fails on horizontal overflow or console errors. A hook reminds the agent to update the help page, llms.html and llms.txt whenever the app or the data shape changes.

The site also publishes its own /llms.txt, which tells other agents what the data means, how to query it, and that the payload sits at .document.data.

A video analysis site that republishes itself

The fight-analysis site shows videos of Swiss athletes' fights at several 2026 tournaments, with a write-up for each, built from the scoreboard overlay with OCR. An agent built the whole pipeline: locating fights in court streams, cutting clips, reading the scoreboard, writing up each fight, and publishing. Detailed frame-by-frame reviews were also written in the session; the agent's notes put the cost at "roughly 20-40 min and 150-250k tokens per fight".

  • Data by natural key. The publish script upserts every fight into the fights collection with PUT …/by-key/{id}, retrying on 5xx. It reads the existing documents first and skips unchanged fights, "so unchanged fights are left alone (no version churn)".
  • The page as one file. index.html loads the fights from the public collection. The script uploads it as a new version of the same document, keeps the tree path assigned, and sets a published label. The public rules have no label filter, so the label doesn't decide what visitors see.
  • A daily job. A scheduled batch at 09:30 uploads new clips to YouTube, regenerates write-ups and rankings, rebuilds the pages and republishes. The data shows it: from 2026-09-10 on, every day has a page version between 07:30 and 07:45 UTC.
  • A rename. After a second tournament was added, the owner asked for a new URL. The agent created a second tree, combats; the old tree still serves its last page.
  • Public versus private. Rankings and detailed reviews weren't meant to be public. The agent's reasoning: "Wren has no private pages. Anything published there is readable by anyone. It is also listed in Wren's public data summary for your organisation, including the path to ranking.html." So the public page carries only videos and basic scoreboard analysis, and ranking.html holds the rest encrypted with a passphrase.

A style guide released with labels

The Club Hub UI style guide was built in one session on 2026-10-02: design tokens, about 35 CSS components, JavaScript behaviors, icons, a design guide and a UX guide. It is the only one of the four sites released with labels. The agent's deploy script "mirrors wren deploy":

js · tools/deploy-wren.mjs (simplified)
// upload new or changed files (SHA-256 in a local state file), assign tree paths
// labels: everything current → preview; with --promote also → published
for (const label of PROMOTE ? ['preview', 'published'] : ['preview'])
  for (const id of docIds) await api('POST', `/${COLLECTION}/${id}/labels`, { label });
// public read, published versions only
await api('POST', '/permissions', { principal: '*', resource: `tree:${TREE}`,
                                    access: 'read', labelFilter: 'published' });
  • The agent deployed a small Worker that serves the tree on its own domain, read-only, and only what the public rule shows, so only published versions are visible.
  • Before each deploy it ran a CSS lint and the test suite (478 tests at one point), and checked pages in a browser at desktop and phone width.
  • Every deploy command in the session used --promote, so preview and published always moved together. On 2026-10-05 both labels point at the current version of all 149 files.
  • The style guide publishes its own llms.txt and a components.json with each component's data contract, for agents that build screens from it.

A docs site that checks its own bytes

The bootOS site is generated from source by a script the agent wrote. The same build goes to two places: the operating system's own file system, and a WREN tree "where wren itself serves it to the public web". The deploy step:

  • declares filename as the collection's natural key "so re-deploying replaces rather than piles up";
  • finds existing files by listing the collection, because by-key "answers 404 for every one of them" for uploaded files, and soft-deletes leftover duplicates (8 of the collection's 17 documents are deleted);
  • reads the site's public address from WREN's /api/v1/projects instead of guessing the org slug.

After each deploy the agent fetched every changed page from the public URL without credentials and compared it with the build. Once, style.css didn't match. The agent checked the headers, found the Cloudflare cache still serving the old file (max-age=14400 at the time), and confirmed a byte-for-byte match a few seconds later. The site was deployed after each change; of its 134 file versions, 66 are identical to the version before.

What worked well

  • Every agent write is a version. Nothing an agent uploaded was ever overwritten in place. The data site's rollback plan is simply "v41", and it exists because WREN kept it.
  • A plain HTTP API is enough for an agent. The agents wrote their deploy clients from WREN's docs, with no SDK: 95 lines for the fights site, 123 for the style guide.
  • Agents check what visitors see. Public URLs need no key, so an agent can verify a release exactly as a visitor gets it.
  • Natural keys make republishing safe. The daily job and the agents call the same upsert, and repeating it changes nothing twice.
  • Label filters work as a gate. Where they were used, the public saw only published versions, with a Worker that needs no logic of its own.
  • One org per site. Each site's key reaches only its own org, which limited what any one agent could touch.

Warts and workarounds

The status column refers to WREN 0.10.0, the release that publishes this page.

The workarounds from all case studies, with the better way for each, are collected in I did this. Is there a better way?

What happenedWorkaroundRoot causeStatus
The redesign was staged as copies under /preview/ in the live tree, then 12 paths and 11 documents deleted by handCopy, overwrite, deleteThe public rule had no label filter, so anything in the tree was publicUse labels: deploy with preview, release with promote (Part B)
Rollback was "Wren doc v41" for one file, plus a git tagNotes in the agent's memoryNo release name across the treeName a release with promote --label; roll back with promote --from
Republishing unchanged files created new versions: 68 of the fights site's 127 page versions and 66 of bootOS's 134 file versions repeat the one beforeThe fights script reads every fight first and skips unchanged data documents, not pagesEvery upload and every JSON write made a versionFixed: identical file uploads (0.8.0) and identical JSON writes (0.9.0) return unchanged: true and add no version
by-key returned 404 for uploaded files, so deploys made duplicatesIndex by listing; soft-delete duplicatesNatural keys didn't apply to binary uploadsFixed in 0.9.0: files by name (naturalKey: "filename", PUT …/by-key/{name})
The docs said /api/permissions; the service wanted /api/v1/permissionsOne place in each client "that can be corrected in one edit"Docs described unversioned routesFixed: the docs start with a URL map
A deployed file kept its old version at the edge for a while; on the style guide's domain a 404 stayed cached for up to 60 secondsWaiting; the Worker stopped caching errorsLong cache lifetimes, cacheable errorsFixed: public responses are max-age=60, errors no-store
The public fights page showed only 200 fightsPaging with offset and totalThe list endpoint returns at most 200By design; or _query with a cursor
The fights site sets a published label that changes nothingNone neededIts public rules have no label filterAdd labelFilter if releases should be gated
The style guide's preview was never looked at on its ownTests and browser checks before each deployEvery deploy used --promotePractice: promote after checking ?label=preview
Renaming the fights site meant a second tree; the old one serves a stale pageKeep bothA tree name is part of its URLA public alias on the rule adds a short name without a copy
A key quoted in a prompt was copied into every summary of a long sessionThe key was rotated (2026-10-04) and kept in env and an ignored fileSecrets in conversation textPractice; narrowed keys limit the damage (Part B)
WREN was unreachable while its tunnel was downA script that waits for a 200, then finishes the deployWREN ran on one workstation behind a tunnelInfrastructure, not WREN

Part B

As we'd build it today

The agents did their job well. What they lacked was a way to separate "written" from "released", a name for each release, and deploys that don't add noise. Those are now WREN features, and they map onto how these sites are actually run: the agent writes, the person says "go live".

Release flow, built today The agent deploys with the preview label using a key narrowed to its site. The person or the agent checks the preview on the private route. Promote moves the published label on every file in one transaction and can name the release. The public rule shows only published versions, and llms.txt lists only released pages. Rolling back promotes an older release label. Agent wren deploy --label preview or MCP write_file Person checks ?label=preview says "go live" WREN · site tree v7 ← preview v6 ← published, release-12 v5 ← release-11 unchanged upload: no version promote --from preview every file, one transaction promote --from release-11 rollback, same way Visitors rule: labelFilter "published" Other agents /orgs/{slug}/llms.txt released pages only
Figure 2. Built today: the agent stages, a person releases with one call, and both visitors and other agents see only released versions.

1 · Let agents stage, let people release

Instead of copying the redesign under /preview/ and deleting it afterwards, deploy it as the preview label of the same files. A public rule with labelFilter: "published" keeps it invisible. The first deploy with --public creates that rule:

bash · the agent
export WREN_URL=https://wren.aemwip.com WREN_API_KEY=wren_…    # this site's key
wren deploy ./events --tree site --label preview --public
curl -H "Authorization: Bearer $WREN_API_KEY" \
  "$WREN_URL/api/v1/tree/site/index.html?label=preview"        # check it like the owner will

The harness that checks the live site can check the preview the same way, with the key. When the owner says "go live", one call releases every file at once:

bash · after "ok, lets go live with this"
wren promote site --from preview --label release-2026-09-17   # name the release
wren promote site --from preview                               # every page goes live in one transaction

Promote moves the label on every document in the tree "in a single transaction, so readers never see a half-promoted site". There is nothing to clean up: the staged versions are simply older versions. Data that should go live as soon as it's written, like the events tree or the fights collection, keeps a rule without a label filter.

2 · Name releases, roll back in one call

"Wren doc v41" covers one file. A release label covers the whole tree, including the help pages and screenshots that changed with it:

bash
wren promote site --from release-2026-09-16    # point "published" back at the old release

A site without a label filter, like the fights site, has no published pointer to move back. Since 0.9 it can still go back to a named state in one call: label the files at each release, and POST /api/v1/tree/combats/_restore {"label": "release-…"} writes the labeled content as new versions of every file in the tree, in one transaction. For JSON data, GET …/{id}/diff?v1=published&v2=preview&deep=true shows field by field what a release would change, so the person saying "go live" can see it. For a single document, POST /api/v1/{collection}/{id}/rollback/{n} writes a new version with the old data and keeps the history. Under a label-filtered public rule, version history and diffs return 403, so visitors can't read drafts through the history either.

3 · Deploys that change nothing add nothing

Since 0.8.0, file bytes are stored once per SHA-256, and replacing a file with identical bytes, name and type returns "unchanged": true without a new version. The fights site's daily republish and bootOS's deploy-everything script would have added 134 fewer versions (68 and 66). Since 0.9.0 the same holds for JSON: an upsert whose data equals the current version creates none, so the fights script can send every fight and drop its read-everything-first step. wren deploy also compares hashes before uploading.

bootOS's deploy script wanted exactly what 0.9 added: files addressed by name. Declaring filename as the natural key now registers the names of the files already there (or refuses with 409 and lists duplicate names, which is how the leftover copies would have shown up), and the deploy becomes one call per file:

http
PUT /api/v1/bootos-site/_schema           {"collectionType": "binary", "naturalKey": "filename"}
PUT /api/v1/bootos-site/by-key/style.css  multipart, field "file"   → 201 created / 200 replaced / 200 unchanged
GET /api/v1/bootos-site/by-key/style.css  → metadata incl. sha256, to compare with the build

No listing, no duplicate pruning, and a POST of a name that already exists is refused with 409 instead of making a second copy. A duplicate deleted by mistake comes back with POST /api/v1/bootos-site/{id}/undelete.

4 · MCP for agents, one narrowed key each

WREN's MCP tools match what these agents did by hand, with the same release step built in. write_file finds the file in the tree, uploads it and labels it preview; promote_tree moves preview to published in one transaction and carries MCP's destructive hint, which clients can use to ask before calling it. list_tree, read_file and diff_versions let the agent see what's there and what changed.

bash · Claude Code, per site
claude mcp add --transport http bootos-site \
  https://wren.aemwip.com/orgs/<slug>/mcp \
  --header "Authorization: Bearer $BOOTOS_SITE_KEY"

The org-bound endpoint accepts only that org's keys. Today each site's key is a full-access key for its org. A key with key:<id> rules uses only those, so a site key can be narrowed to its tree and asset collection:

http · admin, once per agent key
POST /api/v1/permissions  {"principal": "key:<id>", "resource": "tree:bootos",              "access": "write"}
POST /api/v1/permissions  {"principal": "key:<id>", "resource": "collection:bootos-site",   "access": "write"}

That key can't change permissions or reach the org's other collections. Had the leaked key been narrowed like this, rotating it would still have been right, but it could only have touched the site. For a person working with an agent, browser sign-in (/mcp/login) avoids keys in prompts entirely: the token works only for MCP, and it is revoked under Settings → Connected apps.

Without a key, /orgs/{slug}/mcp?tree=site gives any agent read-only tools over just the released site, which is a cleaner way for other agents to consume the data site than scraping it.

5 · Tell agents what's there

Each public org serves /orgs/{slug}/llms.txt, listing its public collections and tree paths. That listing is what told the fights agent that a "private" page in a public tree wasn't private. Since 0.7.0, for a tree whose public rule shows only published, it lists only released pages, so a staged preview doesn't leak through it. Keep writing a site's own llms.txt as the data site and the style guide do: WREN's file says where things are, the site's file says what they mean.

6 · Live events, webhooks and retention

  • Reload on release. GET /orgs/{slug}/_events?trees=site is public and, under a published filter, sends only moves of the published label. An open page or a monitoring agent can reload when a release lands, instead of polling.
  • React to releases. A webhook on label.set gets one batched call per promote, signed with HMAC. Use it to purge a custom-domain cache or tell the person the release is live.
  • Keep history bounded. The fights collection gains versions every day. A retention policy such as {"maxVersions": 20} keeps the newest versions, and labeled versions are always kept, so release labels survive. Run _preview before saving it.

Side by side

ConcernAs builtToday
StagingCopies under /preview/, deleted after releasepreview label on the same files
ReleaseOverwrite the live files one by onewren promote, one transaction
Rollback"Wren v41" for one file, plus a git tagpromote --from release-… for the whole tree
RedeploysNew version for every uploadUnchanged files and JSON add no version
Finding a fileList the collection, prune duplicatesby-key/{filename}
Rollback without a label filterOne document at a timetree _restore to a release label
Agent accessHand-written REST clientsMCP: write_file, promote_tree, diff_versions
KeysOne full-access key per orgNarrowed per agent; browser sign-in for MCP
What other agents seeEverything public, listed in llms.txtReleased pages only
Release signalsAgent polls the public URLLive events and webhooks on label.set
History growthUnboundedRetention policy; labeled versions kept

Still open

  • Pages for signed-in viewers. Public routes serve only what a * rule allows, at the whole-tree level, and private routes need a key or a WREN session. A members-only page on a public site still needs another layer, like the passphrase the fights site uses.
  • Public files by name. Since 0.10, a public file collection keyed by filename is also served by name: /orgs/{slug}/{collection}/by-key/{name}/raw, under the same public rule and label filter.
  • One person approving. Nothing in WREN records who approved a release. Write the approval into the release label's name, or into the commit.
  • Unproven here. None of these sites has used MCP, narrowed keys, live events, retention, restore or files by name yet. Part B is how we'd use them, not a report of using them.

Lessons

  1. Separate writing from releasing. Agents write often and fast. A label filter on the public rule lets them do that without anyone seeing half-done work, and promote turns "go live" into one call.
  2. Make the agent check what visitors see. The best habit in these sessions was fetching the public URL without a key after every deploy.
  3. Public means listed. Anything a * rule allows is in the org's llms.txt. Decide what's public before an agent publishes it.
  4. One org and one narrow key per site. It keeps each agent inside its own site, and a leaked key costs one site, not all of them.
  5. Keep secrets out of prompts. A key in a message is copied wherever the conversation goes.
  6. Let the agent keep the docs. Hooks that remind it to update help pages and llms.txt kept these sites' docs in step with their code.

New to these ideas? Start with Concepts, then Publish a site and the MCP guide.