Case study

A task tracker that AI agents write to

The Organizer is one person's task tracker and work log: projects, tasks, a kanban board and a record of what was done each day. Almost everything in it was written by AI coding agents, during the sessions in which they did the work. This is how it's built on WREN, what we had to work around, and how we'd build it now.

Written October 2026 from the project's code (the setup bundle published in its WREN tree), its data in a WREN backup taken on 2026-10-05, and the work log it kept about itself. Task titles and personal details are left out.

The project

The Organizer keeps projects → tasks → worklog. A task has a status (inbox, todo, in-progress, blocked, done, dropped), a priority, a due date, comments and dependencies on other tasks. A worklog entry says what was done on a given day and why. A web board shows the open tasks in lanes, and a report lists today's or this week's work.

It has one human user. The other writers are Claude Code sessions on that person's machines. On machines where logging is switched on, every coding session is reminded at its start to offer to record its work. When the person says yes, the agent files tasks and writes worklog entries as it goes. Each write names the session that made it, for example claude:tkd-scores.

At a glance
WritersClaude Code sessions (through a CLI and a skill), the web board, any script with a key
Front enda REST API, CLI and single-file web UI, deployed as a Cloudflare Worker on its own domain; it stores no credentials
WREN dataprivate JSON collections org-projects, org-tasks, org-worklog, each with a JSON Schema and the natural key slug; a materialized query open-tasks
WREN sitepublic tree organizer with two files: a redirect page and the setup bundle
Size on 2026-10-0510 projects, 45 tasks, 280 worklog entries, about 600 versions in all
Who wrote it40 of 45 tasks and all 280 worklog entries carry a claude:… source on their current version; 5 tasks came from the web board

Part A

As built

The Organizer was built and deployed on 2026-08-14 and 2026-08-15 by Claude Code, and its own work log records each step. It has been in daily use since.

Architecture as built Claude Code sessions, the CLI and the web board call the Organizer API, a stateless Cloudflare Worker, with the caller's own WREN key. The Worker passes the key through to WREN's private collections. A public WREN tree holds a redirect page and the setup bundle, which makes the project show up in WREN's projects directory. Claude Code sessions skill + start-of-session hook ORG_SOURCE=claude:topic CLI bin/org profiles: real, test, mock Web board key in localStorage Organizer API Cloudflare Worker slugs, status rules, boards, dependencies, reports no secrets: passes caller's key WREN · private org-projects org-tasks org-worklog schema + naturalKey "slug" materialized open-tasks WREN · public tree organizer /index.html → redirect /organizer.zip setup bundle curl PUT by document id republish the bundle
Figure 1. As built: every writer goes through the Organizer API, which holds the rules and no credentials. WREN stores the data and hosts two public files.

Data model

Three JSON collections, each with a JSON Schema and slug declared as the natural key, so every write is an upsert by key:

CollectionKeyFields
org-projectsslugname, status, client, color, description, tags, board buckets, source
org-tasksslugtitle, project, status, priority, due, notes, tags, bucket, rank, dependsOn[], comments[], completion stamps, source
org-worklogslug (YYYY-MM-DD-xxxxxx)date, summary, details, project, task, duration in minutes, source
  • The org- prefix. The first attempt used the bare names projects, tasks and worklog. Within three minutes they were renamed, because "bare names can collide with Wren's own reserved routes (projects is its public project directory)". The bare-name documents from those minutes are still in the test org, and a tasks materialized query in both orgs.
  • Dates twice. Every date also has a numeric YYYYMMDD twin (dateNum, completedOnNum), because "Wren's SQL range operators (>=, <=) cast to numeric, so date strings can't be range-filtered". Reports filter on the twins. (WREN 0.9 compares ISO dates directly; see Part B.)
  • Dependencies stored once. A task stores only what it waits for. What it blocks is worked out on read, "so the two halves can never disagree". Cycles and unknown slugs are refused, and finishing a task with open dependencies returns 409 unless the caller insists.
  • History is the audit trail. The task view interleaves comments with WREN version diffs, so "who changed what, when" needs no extra table.
js · server/wren.mjs (the whole write path)
upsertByKey(collection, key, data) {
  return this.#req("PUT", `/${collection}/by-key/${encodeURIComponent(key)}`, data);
}

How work gets in

The project's rule for agents is short: "Never call the Wren API directly for task/project/worklog data." Agents use the CLI or the REST API, so slug generation, status rules, completion stamps, board order and referential integrity live in one place.

  • A start-of-session hook. An installer adds a SessionStart hook to Claude Code on each machine that opts in. When logging is on, the hook tells the session to ask once, in its first reply, whether this session's work should be recorded. It is per machine and off by default, so a private computer stays untouched.
  • A skill and a conventions file. The skill tells the agent how to log: one entry per meaningful unit of work, "summaries for a future [owner] skimming his week", and a duration "only when defensible". AGENTS.md carries the same rules for other tools, and the API serves its own /llms.txt so "a fresh agent with no repo access" can drive it.
  • Who wrote it. Every write carries a source (ORG_SOURCE in the CLI, X-Organizer-Source over REST). WREN's version history records which account wrote a version, and here every agent writes with the same key, so the source field is what tells sessions apart. 66 different claude:… sources appear in the data.
  • The board. The web UI asks for a WREN key or an email and password once and keeps the credential in the browser. Its writes carry source: web-ui.
bash · what an agent runs
export ORG_SOURCE="claude:tkd-scores"
org --env real task add "Title" -p tkd-scores
org --env real task update <slug> -s in-progress
org --env real log add "What was actually done" -p tkd-scores -t <slug>
org --env real task done <slug>

The agents followed the duration rule: only 4 of 280 worklog entries carry a duration.

Deploy and public page

  • The API is a Worker with no secrets. npx wrangler deploy publishes it on its own domain. Every request carries the caller's own WREN credential, which the Worker passes on, so "Wren scopes all data to that key's org" and any WREN account can use the tool. Real and test data are simply two accounts: the CLI keeps a real and a test profile, and a mock profile with an in-memory backend for development.
  • Sign-in by proxy. For people, the API forwards email and password to WREN's sign-in endpoint and hands back the session token. It had to send both cookie names, because "https deployments expect the __Secure- prefix".
  • The WREN tree exists for the projects directory. WREN lists an org's public trees at /projects and links a tree that has an /index.html. To appear there, the tree holds a small redirect page as /index.html, which forwards visitors to the real project page on the Organizer's domain. It also holds organizer.zip, the setup bundle a new machine downloads. Republishing the bundle is a multipart PUT to its document id, "so the URL never changes".

Timeline and scale

WhenWhat happened (from the work log and the data)
2026-08-14API, CLI and test org built; deployed as a Worker; web UI; email sign-in; kanban board with buckets. The four steps with a recorded duration took 170 minutes in all. The start-of-session hook, Windows installer, AGENTS.md and /llms.txt followed the same day.
2026-08-15Public tree reworked (see the warts below); task dependencies added.
August30 tasks and 79 worklog entries created.
September13 tasks and 192 worklog entries created. One topic alone, the sessions that built a shop's stock tracker, wrote 137 worklog entries between 2026-09-08 and 2026-10-01.
2026-10-01Last write in the backup; the key was last used that evening.

On 2026-10-05 the 45 tasks were 27 done, 1 dropped, 2 in the inbox, 13 to do and 2 in progress. The busiest task has 9 versions. The open-tasks materialized query has been refreshed 143 times and holds 17 rows.

What worked well

  • Natural keys make agent retries safe. "Upserts are idempotent (Wren natural keys), so agent retries are safe." An agent that repeats a call after a timeout changes nothing twice.
  • Schemas catch bad writes. Status and priority are enums, slugs and dates have patterns. A typo from an agent is a 422, not a stray value on the board.
  • Versions as task history. The task view shows every change as a diff, taken straight from WREN. Nobody built an audit table.
  • No secrets in the service. Because the Worker only passes the caller's credential through, there was nothing to configure in the cloud and nothing to leak from it.
  • Built for agents from day one. A skill, a hook, a conventions file and an /llms.txt meant that sessions on other machines and other tools could use it without being shown how.

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
Bare collection names (projects) collided with WREN routesorg- prefix on every collection/api/v1/projects is WREN's public directoryStill applies: avoid projects as a collection name
Date strings couldn't be range-filteredNumeric YYYYMMDD twins>, >=, <, <= compare as numbersFixed in 0.9.0: range filters compare non-numbers as text, so ISO dates work (Part B)
The client had to find the real base pathHard-coded /api/v1, with a comment that the docs showed unprefixed pathsDocs described unversioned routesFixed: docs start with a URL map; everything authenticated is under /api/v1
Email sign-in failed in productionSend both cookie namesHTTPS uses the __Secure- cookie prefixFor agents, MCP browser sign-in replaces the proxy (Part B)
A per-tree public rule (tree:site) seemed not to be saved or enforcedPublic rule on tree:*; then a tree literally named * so the directory had something to linkUnclear. On a retry the next day, the rule tree:organizer was saved and enforced, and the workaround trees were removedPer-tree rules work (the org's public rule today is tree:organizer)
The projects directory showed a tree named * with no linkSee aboveThe directory resolves trees by the rule's literal nameGone with the per-tree rule
A republished bundle took hours to reach everyoneDocumented: "Cloudflare caches these for 4 hours"Long cache lifetime on public filesFixed: public responses are max-age=60; errors are no-store
All agents shared one keySelf-reported source fieldOne full-access key for the accountNarrow a key per agent (Part B); keep source
Reads stop at 200 documents and don't page; the unfiltered worklog list already returns 200 of 280 (with the total)None yetThe list endpoint's maximum is 200Page with offset, or _query with a cursor
open-tasks is refreshed on every task write, but nothing reads it; the report runs the same query itselfNoneSet up, never wired inRead it, or drop it

Part B

As we'd build it today

The data model stays: schemas, natural keys and versions already do most of the work. What changes is how agents connect, what each one may touch, and how the board learns about their writes. The Organizer's README already named the next step: "a thin MCP server wrapping these endpoints … Not built yet." WREN now has one built in.

Architecture, built today Each agent connects to the org-bound MCP endpoint with its own key, narrowed by key rules to the task collections. Rules that span documents stay in the Organizer API. WREN's live event stream tells the board to reload when an agent writes, and a webhook can notify other systems. Agent sessions one narrowed key each or browser sign-in Web board reloads on live events invited members, groups /orgs/{slug}/mcp only this org's keys writes are versions Organizer API cross-document rules: dependencies, boards, reports WREN · private org-tasks org-worklog org-projects schemas do field checks key rules per agent /api/v1/_events webhooks
Figure 2. Built today: agents reach WREN over MCP with keys that can only touch the task collections, the API keeps the rules that span documents, and the board hears about every write (dashed).

1 · Agents over MCP, bound to one org

WREN's MCP endpoint gives an agent document tools (query_documents, get_document, write_document, list_versions, diff_versions) that call the REST API in-process, "so permissions, label filters and org isolation apply unchanged". The org-bound endpoint /orgs/{slug}/mcp accepts only keys of that org; a key from another org gets 403. That replaces the "real or test?" question with a configuration: one endpoint per org.

bash · Claude Code, once per machine
claude mcp add --transport http organizer \
  https://wren.aemwip.com/orgs/<real-slug>/mcp \
  --header "Authorization: Bearer $ORGANIZER_AGENT_KEY"

For a person at a desk, /mcp/login signs in through the browser instead: WREN shows a consent page where they pick the org, the token works for MCP only, and the connection can be revoked under Settings → Connected apps. No password passes through the Organizer.

Two things to know. write_document labels each version preview unless you pass label: ""; for task data, pass it. And MCP writes go straight to WREN, so they skip the Organizer's rules. Schemas still check every field, but dependencies, completion stamps and board order span documents. Either keep agents on the Organizer API for writes and use MCP for reading and history, or move those rules into the schema where they fit and accept the rest as conventions.

2 · One key per agent, narrowed by key rules

Today every session writes with the same full-access key, and the only record of which session wrote is the source it reports. A WREN key acts as its creator unless it has key:<id> rules of its own, which then narrow it. Give each machine or tool its own key that can write tasks and the worklog, read projects, and nothing else:

http · admin, once per agent key
POST /api/v1/keys         {"name": "agent-laptop"}            → id, key (shown once)
POST /api/v1/permissions  {"principal": "key:<id>", "resource": "collection:org-tasks",    "access": "write"}
POST /api/v1/permissions  {"principal": "key:<id>", "resource": "collection:org-worklog",  "access": "write"}
POST /api/v1/permissions  {"principal": "key:<id>", "resource": "collection:org-projects", "access": "read"}

A key that leaks from one machine can then only write task data, and revoking it stops one agent without touching the others. Keep the source field; it names the session, which a key can't. Rules can also set auditWrites to log matching requests.

3 · A board that updates when an agent writes

The board loads when you open it. Since 0.7.0, WREN streams committed changes as server-sent events: GET /api/v1/_events?collections=org-tasks,org-projects with the caller's credential. Events carry ids, versions and natural keys, never data, so the board re-reads what changed. The Worker already passes the caller's credential through, so it can pass this stream through as well:

text · one event
data: {"type":"document.updated","collection":"org-tasks","id":"…","version":4,"key":"fix-login-bug","at":"…"}

Webhooks get the same payloads, batched, signed with HMAC and retried. A webhook on document.updated for org-tasks could post to a chat when a task moves to blocked, without any poller.

4 · Sharing a project

The org has one member. Since 0.6.0, WREN has groups and invites: create a group, give it read (or write) on the task collections, and invite someone straight into it. Invite links only work for the invited email address. The pass-through design means the Organizer itself needs no change; a new member signs in and sees what their group allows.

http · admin
POST /api/v1/groups       {"name": "Helpers"}                        → id
POST /api/v1/permissions  {"principal": "group:<id>", "resource": "collection:org-tasks", "access": "write"}
POST /api/v1/invites      {"email": "…", "role": "member", "groupIds": ["<id>"]}

Granularity is the collection, so a group sees all tasks, not one project's. Showing one project only would need a data filter on a read rule (filterLang and filterExpr, for example project == 'website' in JMESPath).

5 · Dates, history and undo

The numeric twins can go. Since 0.9, range operators compare numbers numerically and anything else as text, so ISO dates filter correctly on the fields the Organizer already stores:

http
GET  /api/v1/org-worklog?where=date>=2026-09-28 AND date<=2026-10-04&limit=200
POST /api/v1/org-tasks/_query  {"where": "status:done AND completedOn>=2026-09-21", "cursor": …}

The task view diffs every pair of versions to show its history. A task keeps its comments and dependencies in arrays, so a top-level diff reports "/comments replaced" for one new comment. With deep=true the diff names the element ({"op": "add", "path": "/comments/3"}), and v1/v2 also accept label names.

Agents retry, and a retried write used to add a version with the same data. Since 0.9, a write equal to the current version returns "unchanged": true and creates none, so a task's history shows only real changes. To undo an agent's bad edit, POST /api/v1/org-tasks/{id}/rollback/{n} writes a new version with the old data and keeps the history; a task deleted by mistake comes back with POST /api/v1/org-tasks/{id}/undelete. Before letting an agent reorganize a whole board, label the tasks (before-cleanup), and if the result is wrong, POST /api/v1/org-tasks/_restore {"label": "before-cleanup"} puts every task back in one transaction.

6 · The public page and the bundle

Replace the hand-run curl PUT with wren deploy. It compares SHA-256 hashes, and since 0.8.0 identical bytes are stored once: re-uploading an unchanged file returns unchanged: true and creates no version.

bash
export WREN_URL=https://wren.aemwip.com WREN_API_KEY=wren_…
wren deploy ./public --tree organizer --label preview --public
wren promote organizer --from preview

For the bundle alone, files by name (0.9) replace the lookup by document id: declare {"collectionType": "binary", "naturalKey": "filename"} on org-site once, then republish with PUT /api/v1/org-site/by-key/organizer.zip (multipart). It replaces the same document, so the tree path and its public URL stay as they are. At about 600 versions, the org needs no retention policy; if it ever does, exempt the task collections with an empty policy ({}), because their history is the audit trail.

Side by side

ConcernAs builtToday
Agent accessCLI or REST through the Organizer APISame for writes; /orgs/{slug}/mcp for reads and history
Real vs testCLI profiles, "if it's ambiguous, ask"One org-bound endpoint per org; other orgs' keys get 403
CredentialsOne full-access key; password proxied for peopleA narrowed key per agent; browser sign-in for MCP
Who wrote itSelf-reported sourcesource plus a key per agent, optional write audit
Board freshnessOn page loadLive events, passed through by the Worker
NotificationsNoneWebhooks on org-tasks
SharingOne personGroups and invites
Date rangesnumeric YYYYMMDD twinsISO dates in where
Task historytop-level diffs; retries add versionsdeep=true diffs; unchanged writes add none
Public filescurl PUT by idwren deploy and promote, or PUT …/by-key/organizer.zip
UndoRe-edit by handrollback/{n}, undelete, _restore to a label

Still open

  • Rules across documents. Schemas validate one document. "A task may only be done when its dependencies are" needs code outside WREN, which is why the Organizer API stays.
  • Which key wrote a version. Version history records the account that wrote it. Telling agents apart by key means turning on write audit for their rules, or keeping the source field.
  • Two writers on one task. Adding a comment reads the task and writes it back whole. If an agent and the board do that at the same moment, the later write wins and a comment is lost. Since 0.10 the fix is a conditional write: send the version read as If-Match, and on 412 re-read the task and add the comment again. The tracker doesn't do this yet.
  • 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.

Lessons

  1. Give agents a narrow, documented door. One API, one conventions file and an /llms.txt got many sessions on several machines writing consistent data.
  2. Make agents say who they are. A source on every write made the history readable. Add a key per agent so the server can enforce what the label only describes.
  3. Natural keys first. Idempotent upserts are what make an agent's retries harmless.
  4. Ask before logging. The hook asks once per session and stays silent on machines that never opted in. That kept the log useful and the person in charge of it.
  5. Read what you build. The materialized query was refreshed 143 times and never read. Check that each piece of WREN you set up is actually used.

New to these ideas? Start with Concepts, then the MCP guide.