Skip to content

The open format

Obra Todo stores its entire database as one human-readable JSON document. The format is open and stable on purpose: agents, scripts, and other tools may read or rewrite the file directly. This page is the spec; the repository carries the same contract in FORMAT.md.

EnvironmentLocation
macOS app~/Library/Application Support/studio.obra.todo/todo.json
Browser dev modelocalStorage["obra-todo"]

The app re-reads the file every time its window regains focus, so an external edit shows up as soon as the user switches back. The app saves a few hundred milliseconds after any change, atomically (temp file + rename). Last writer wins — there is no merge.

{
"version": 1,
"projects": [ { "id": "", "name": "House", "indent": 0 } ],
"contexts": [ { "id": "", "name": "Work", "color": "#4f9cf9" } ],
"tasks": [ { "id": "", "title": "Call the plumber", "…": "" } ]
}

Array order is meaningful. Tasks render in array order (it’s an outline); projects render in array order in the sidebar. There are no separate order or rank fields.

{
"id": "5f3e0b1c-9a7d-4a2e-b8a1-2f6c1d9e4b7a",
"title": "Call the plumber about the boiler",
"note": "Ask about the warranty first.",
"projectId": null,
"contextId": "id-of-a-context-or-null",
"indent": 0,
"deferDate": "2026-08-01",
"dueDate": "2026-08-15",
"completedAt": null,
"createdAt": "2026-07-25T09:30:00.000Z"
}
FieldTypeMeaning
idstringUnique. The app uses UUIDs; any unique string works.
titlestringThe action itself.
notestringFree-form support material.
projectIdstring | nullnull = the task is in the Inbox.
contextIdstring | nullThe GTD context. null = none.
indentinteger ≥ 0Outline depth relative to surrounding rows of the same list. At most one level deeper than the row above; the app clamps at 4.
deferDate"YYYY-MM-DD" | nullHidden-until date. Deferred tasks dim; the day it arrives the task appears in Today.
dueDate"YYYY-MM-DD" | nullThe deadline. Due/overdue → Today; future → Upcoming.
completedAtISO 8601 | nullnull = open. Set = done.
createdAtISO 8601When the task was captured.

Calendar-day fields (deferDate, dueDate) are local days with no time or timezone — a deadline is a day, not a moment.

FieldTypeMeaning
idstringUnique.
namestringDisplay name.
indentinteger ≥ 0Sidebar nesting depth; same one-deeper rule, clamped at 3.

Deleting a project in the app moves its tasks back to the Inbox — it never deletes tasks.

FieldTypeMeaning
idstringUnique.
namestringDisplay name (e.g. “Work”, “Private”).
colorCSS colorThe context’s dot, shown everywhere.
  1. Write the whole document atomically — write a temp file, then rename it over todo.json. The app does the same.
  2. Preserve unknown fields when round-tripping; the app does.
  3. Omitted optional fields are fine — the app fills in defaults on load (missing ids become fresh UUIDs, missing indent becomes 0, missing nullable fields become null).
  4. Invalid JSON is ignored on reload and refused on save, so a botched write can’t destroy data — but validate before writing anyway.
  5. To complete a task, set completedAt; to reopen, set it back to null. Don’t delete completed tasks unless asked — they’re the user’s log.
  6. Never touch the backups/ directory next to the file.
  • InboxprojectId == null, array order, as an outline.
  • Today — open tasks where dueDate <= today, plus tasks whose deferDate == today.
  • Upcoming — open tasks with dueDate > today, grouped by day.
  • Completed — tasks with completedAt, newest first.
  • A project — its tasks in array order (completed stay, grayed).
  • A context — open tasks with that contextId, across all projects.