Labels, Not Projects: Rethinking GTD

By Arshad Ansari

In June I deleted two Todoist projects I had been living out of for years.

Next and Someday-Later — the two lists at the heart of any GTD setup — stopped being projects and became labels: @next and @someday. It looks like a cosmetic change. It turned out to be the most consequential modelling decision in AEGIS's GTD layer, and the reason the system can reason about my project tree at all.

Some context if you're new here. AEGIS is my self-hosted personal AI orchestration platform — the origin story covers the shape of it: named agents running Temporal workflows over FastAPI and Postgres, on a small Docker Swarm at home. GTD sits at the centre. Todoist is the canonical task store; AEGIS mirrors it, and a GTD agent called Sebas works the mirror. Which means every modelling decision in my task system is also a schema decision in a database. Sloppy modelling stops being a private vice.

Two questions, one tree

Every task answers two independent questions. Where is it in the workflow — captured, actionable now, deferred, waiting on someone? And what is it about — which project, which ongoing effort, which corner of your life?

Most GTD setups, mine included for years, cram both answers into one tree. The tell is what happens when a task moves. Deciding "not this week" shouldn't change what a task is about — but in a tree that encodes both, deferring means physically relocating it, and either losing its project membership on the way or duplicating your entire project structure inside both Next and Someday. Anyone who has maintained parallel folder hierarchies knows how that ends.

The fix is old and unglamorous: containers encode workflow position; work-streams cross-cut. In Todoist terms, projects for where, labels for what. A work-stream is a project/<name> label. Delegation is @waiting plus a delegate/<person> label. Reference material is @reference. A task moves through the workflow without ever being moved.

The first version of this in AEGIS kept three managed projects as the workflow containers — Inbox, Next, Someday-Later — with everything else expressed as labels. It worked. It also wasted the best data structure Todoist has.

The day Next became a label

Todoist projects form a real tree — nestable, legible, one glance tells you the shape of your commitments. I was spending that entire structure on three flat buckets, while the actual structure of my work hid inside a naming convention on labels.

The eventual realization was that Next and Someday aren't places either. They're states. The only genuine place in GTD is the Inbox — the airlock where things wait for a decision. Everything after that decision is state, and state is a label.

So Next and Someday-Later collapsed into @next and @someday, and the project tree got its job back. This is what made "project-tree awareness" tractable. Before, the tree meant two things at once — sometimes a workflow stage, sometimes a category — and any code walking it had to know which was which. Now the tree means one thing, so AEGIS can walk it.

The flow that benefits most is ClarifyFlow, which works the Inbox. Every new capture gets classified into one of five outcomes: trash, reference, someday, a two-minute job, or a next action. With workflow state fully in labels, each outcome is a label operation, not a relocation — the task's identity and history stay put while its state advances.

There's a quieter channel through the same flow, and it's become one of my favourite features: label a task for an agent, leave a comment on it, and the comment routes through ClarifyFlow into a chat reply. Tasks as a slow, durable chat channel. Stranger than it sounds; more useful than it sounds, too.

The sync spine

None of this matters if the mirror is wrong. Todoist stays canonical — I capture from my phone, I live in its apps — so AEGIS has to be a well-behaved second reader and writer, never a competing source of truth.

The mirror is TodoistSyncFlow, a Temporal workflow on a five-minute cadence. Reads are incremental: each sync hands back a sync_token, and the next presents it to receive only what changed. Writes never go straight to the API — they queue through a todoist_outbox table and drain as batched commands.

That's the whole design. It fits in two sentences. It is also, comfortably, the hardest part of the system to get right.

Three ways a mirror lies

Self-loops. AEGIS applies a label. Five minutes later the incremental sync dutifully reports: this task changed. Nothing in the delta says who changed it — your own write comes back looking exactly like a human edit. Process it naively and the system re-clarifies its own decision, possibly writes again, and you've built a feedback loop with a five-minute period. Every write path has to assume its own echo is coming back around.

Poison-loops. The outbox retries failed commands, which is correct right up until it isn't. Todoist's Sync API returns a per-command status, and the envelope can succeed while individual commands inside it fail. A 5xx is transient — requeue it. A 4xx is a verdict: the command is malformed or points at something deleted, and it will fail identically forever. Requeue a 4xx and the outbox chews the same poisoned command every five minutes until the end of time. The rule is dull and absolute: retryable errors go back in the queue; permanent ones get logged and dropped.

Watermarks. Each task carries a clarify watermark — when it was last processed — so ClarifyFlow doesn't re-read the whole Inbox every tick. The subtle bug lives in the "done, but nothing changed" case. Clarify runs, reaches a conclusion, and applies nothing. Advance the watermark anyway and the task silently exits the pipeline forever: looked at once, never again. Never advance it and you re-clarify the same task every five minutes, paying an LLM for the privilege. The rule AEGIS landed on: only a real terminal state moves the watermark — a change actually applied, a write queued to the outbox, or a question raised to me. A bare "no change" deliberately leaves it unset, so the task re-enters the queue next tick. Get that predicate wrong in either direction and nothing crashes. It's just quietly wrong for weeks.

The honest case against labels

I should argue the other side, because I lost to it twice before winning.

Labels are flexible precisely because nothing polices them. They sprawl — near-duplicates, one-off tags, a vocabulary that decays into archaeology. Projects are rigid, but the tree forces a decision at capture time and stays legible a year later. Rigidity is a feature when the alternative is entropy.

What tipped it for me wasn't discovering a better argument. It was adding a machine. A labelling convention only holds if something enforces it, and now something does: the state vocabulary is a small closed set, work-streams follow the project/<name> pattern, and ClarifyFlow only knows five outcomes. Sebas polices what I never would have. Labels without an enforcer sprawl; labels with one turn out to be the more truthful model.

Where it's heading

Two directions, both unfinished.

Smarter clarify — better classification with fewer questions back to me, since every question the flow asks is an interruption it was supposed to prevent. And better project-tree detectors: today the pipeline is good at deciding that something is a next action; the harder question is a next action of what — which branch of the tree a raw capture actually belongs to. That's inference over messy human phrasing, and it's exactly the kind of problem that looks solved in a demo and isn't.

AEGIS lives at /aegis, and the code is at github.com/hikmahtech/aegis. The GTD layer is the part I'd read first — not because it's clever, but because it's where the system had to earn the right to touch data I actually live out of.

Building something data-heavy? Let's talk.