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.
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 | |
|---|---|
| Writers | Claude Code sessions (through a CLI and a skill), the web board, any script with a key |
| Front end | a REST API, CLI and single-file web UI, deployed as a Cloudflare Worker on its own domain; it stores no credentials |
| WREN data | private JSON collections org-projects, org-tasks, org-worklog, each with a JSON Schema and the natural key slug; a materialized query open-tasks |
| WREN site | public tree organizer with two files: a redirect page and the setup bundle |
| Size on 2026-10-05 | 10 projects, 45 tasks, 280 worklog entries, about 600 versions in all |
| Who wrote it | 40 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.
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:
| Collection | Key | Fields |
|---|---|---|
org-projects | slug | name, status, client, color, description, tags, board buckets, source |
org-tasks | slug | title, project, status, priority, due, notes, tags, bucket, rank, dependsOn[], comments[], completion stamps, source |
org-worklog | slug (YYYY-MM-DD-xxxxxx) | date, summary, details, project, task, duration in minutes, source |
- The
org-prefix. The first attempt used the bare namesprojects,tasksandworklog. Within three minutes they were renamed, because "bare names can collide with Wren's own reserved routes (projectsis its public project directory)". The bare-name documents from those minutes are still in the test org, and atasksmaterialized query in both orgs. - Dates twice. Every date also has a numeric
YYYYMMDDtwin (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.
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
SessionStarthook 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.mdcarries the same rules for other tools, and the API serves its own/llms.txtso "a fresh agent with no repo access" can drive it. - Who wrote it. Every write carries a
source(ORG_SOURCEin the CLI,X-Organizer-Sourceover 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 differentclaude:…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.
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 deploypublishes 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 arealand atestprofile, and amockprofile 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
/projectsand 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 holdsorganizer.zip, the setup bundle a new machine downloads. Republishing the bundle is a multipartPUTto its document id, "so the URL never changes".
Timeline and scale
| When | What happened (from the work log and the data) |
|---|---|
| 2026-08-14 | API, 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-15 | Public tree reworked (see the warts below); task dependencies added. |
| August | 30 tasks and 79 worklog entries created. |
| September | 13 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-01 | Last 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.txtmeant 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 happened | Workaround | Root cause | Status |
|---|---|---|---|
Bare collection names (projects) collided with WREN routes | org- prefix on every collection | /api/v1/projects is WREN's public directory | Still applies: avoid projects as a collection name |
| Date strings couldn't be range-filtered | Numeric YYYYMMDD twins | >, >=, <, <= compare as numbers | Fixed 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 path | Hard-coded /api/v1, with a comment that the docs showed unprefixed paths | Docs described unversioned routes | Fixed: docs start with a URL map; everything authenticated is under /api/v1 |
| Email sign-in failed in production | Send both cookie names | HTTPS uses the __Secure- cookie prefix | For agents, MCP browser sign-in replaces the proxy (Part B) |
A per-tree public rule (tree:site) seemed not to be saved or enforced | Public rule on tree:*; then a tree literally named * so the directory had something to link | Unclear. On a retry the next day, the rule tree:organizer was saved and enforced, and the workaround trees were removed | Per-tree rules work (the org's public rule today is tree:organizer) |
The projects directory showed a tree named * with no link | See above | The directory resolves trees by the rule's literal name | Gone with the per-tree rule |
| A republished bundle took hours to reach everyone | Documented: "Cloudflare caches these for 4 hours" | Long cache lifetime on public files | Fixed: public responses are max-age=60; errors are no-store |
| All agents shared one key | Self-reported source field | One full-access key for the account | Narrow 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 yet | The list endpoint's maximum is 200 | Page 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 itself | None | Set up, never wired in | Read 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.
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.
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:
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:
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.
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:
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.
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
| Concern | As built | Today |
|---|---|---|
| Agent access | CLI or REST through the Organizer API | Same for writes; /orgs/{slug}/mcp for reads and history |
| Real vs test | CLI profiles, "if it's ambiguous, ask" | One org-bound endpoint per org; other orgs' keys get 403 |
| Credentials | One full-access key; password proxied for people | A narrowed key per agent; browser sign-in for MCP |
| Who wrote it | Self-reported source | source plus a key per agent, optional write audit |
| Board freshness | On page load | Live events, passed through by the Worker |
| Notifications | None | Webhooks on org-tasks |
| Sharing | One person | Groups and invites |
| Date ranges | numeric YYYYMMDD twins | ISO dates in where |
| Task history | top-level diffs; retries add versions | deep=true diffs; unchanged writes add none |
| Public files | curl PUT by id | wren deploy and promote, or PUT …/by-key/organizer.zip |
| Undo | Re-edit by hand | rollback/{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
sourcefield. - 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 on412re-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
- Give agents a narrow, documented door. One API, one conventions file and an
/llms.txtgot many sessions on several machines writing consistent data. - Make agents say who they are. A
sourceon every write made the history readable. Add a key per agent so the server can enforce what the label only describes. - Natural keys first. Idempotent upserts are what make an agent's retries harmless.
- 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.
- 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.