todofiles: Local Plaintext Files for Jira Tickets
TLDR;
todofiles is a file syntax (basically markdown), parser, and tool for maintaining Jira tickets in local plaintext files and syncing them to/from Jira (maybe other ticket backends later).
I don’t like navigating ticketing system Web UIs; todofiles allow me to keep my todo lists in plaintext files while syncing changes to Jira:
---
# File-level defaults applied to all tickets
labels: ["my_team", "my_project"]
board: "BackendTeamBoard"
item_type: "task"
status_map:
x: "Done"
in_prog: "In Progress"
review: "In Review"
---
[ ] add json output format to products API
[in_prog] refactor frontend to use v2 API
id: abc123
jira: PROJ-42
[in_prog] add slack integration
id: ghi789
jira: PROJ-45
description: |
Add integration with our Slack bot using OAuth workflows.
subtasks:
[x] create oauth callback endpoint
id: jkl012
jira: PROJ-46
[ ] register new Slack App
id: mno345
Lines like this that don't start with [...] are free-form comments.
They are preserved in the file but never synced to any backend.
$ todofiles push ./my-proj.todo
CREATE (5)
+ [ ] add json output format to products API
+ [in_prog] refactor frontend to use v2 API
+ [in_prog] add slack integration
+ [x] create oauth callback endpoint
+ [ ] register new Slack App
Background
I’ve always found navigating/using project management software like Jira, ClickUp, Trello, etc… to be tedious.
Creating new tickets or finding/updating existing tickets invovles menu-diving through various layers of a Web UI that attempts to load dozens of other tickets at each click.
Once I finally get to create a ticket in what seems to be in the correct backlog/board/project, the ticket disappears because I didn’t assign the correct labels/status/project/board/… - if I’m not quick enough to catch the warning popup, my newly documented ticket disappears into the ether.
When I’m in a meeting and need to scribble down a task quickly I don’t often find myself reaching for my web browser to wait for a UI to load where I can enter my ticket, nor do I want a heavy web app consuming what precious/limited memory my corporate-issue Thinkpad can spare.
Or when I’m reviewing meeting notes/next-steps, I’ve usually already got a plain-text list of TODOs - the burden of copying those into a ticketing system feels silly (and thus rarely happens…).
As a result, my Jira board is almost never reflective of what I’m actually working on and I frequently find myself apologizing to PMs for my negligence.
Plaintext .md Files for Tracking Notes/TODOs
Like many developers, I prefer to spend most of my time in my text editor, editing text files.
I’ve defaulted to keeping most of my TODOs and project-related notes in plaintext files - specifically markdown files (not that I actually ever render the markdown to html/pdf or any other, I just find the syntax highlighting nice for combining things like headers, bulleted lists, code snippets, amongst notes).
This has been nice for a few reasons:
- I can very easily
grepaccross my various notes/todos (much faster than trying to search through old tickets, Confluence pages, etc…) - finding files is also much easier/faster (same keyboard shortcut I use to open files in development)
- It’s super fast to pull up a new file (
ctrl + shift + t,nvim ~/notes,:e TODO.md) - as a neovim user, I get to keep my hands on the keyboard (as opposed to in a browser/web UI requiring clicks) …etc…
So I started keeping TODOs/tasks as checkboxes in my markdown notes files:
[x] - add unit tests for new /product endpoint
[ ] - implement /product endpoints
Jira/ClickUp inevitably became very stale from my local plaintext TODO files.
todofiles
todofiles is a textfile syntax and synchronization tool that allows you to write your todos/tickets in a plaintext format roughly resembling markdown, parse that file, and sync those with tickets in Jira (maybe other ticketing systems/backends in the future…).
todofile Syntax
The syntax of a .todo file is basically markdown with a header describing the scope or default fields for all tickets the file will create in the ticketing system backend (currently just Jira):
Any line starting with [<status>] Ticket title description... will be parsed as a separate ticket (where <status> can be mapped to statuses in Jira).
---
# yaml header w/ some optional settings
# Jira board - all tickets under this file will be associated with this board
board: MYPROJ
# Any tickets created/managed by this todofile will be assigned to this user
assignee: AB32B2B2383B2B1B23412341234
# Any newly created tickets will automatically be added to this sprint
sprint: "current"
# Mapping of statuses we can place in our `[<status]` blocks to what they're called
# in our ticketing backend.
status_map:
todo: "To Do"
in_prog: "In Progress"
x: "Done"
---
Any text in this document that doesn't match the header matter or ticket parser is ignored (so we can keep misc notes here too).
Once a ticket is synced w/ Jira, it receives 2 identifiers the `jira` ID for the ticket, and an internal uuid (todofiles cli will write these back to the .todo file automatically for each ticket).
[in_prog] refactor auth middleware
id: a3f9c1b2
jira: PROJ-84
[x] fix the flaky test
id: 7d2e4a91
jira: PROJ-71
This ticket isn't yet sync'd (doesn't have an id or jira id) - that's fine, we just haven't run `todofiles push ./my-todo.todo` yet.
[todo] wire up the new endpoint
Architecture
The system is a layered pipeline from todofile to the ticketin backend, with a local AST and sqlite db inbetween:
.todo file → Parser → Internal AST → SQLite (via Alembic) → Service Mapper → Jira API
Parser (todo_files/parser.py)
Reads a .todo file and produces a ParsedFile. Responsibilities:
- Parses the YAML frontmatter block into a
FileConfigobject - Parses the body into an ordered list of
Ticketobjects and free-form string blocks - Preserves everything else in the file as is
- Handles nested subtasks (parsed recursively at a deeper indent level)
- Handles multiline field values using YAML block-scalar (
|) syntax
Internal AST (todo_files/models.py)
The shared data model imported by every other layer:
| Class | Purpose |
|---|---|
Ticket |
One ticket: title, status, id, remote_key, labels, description, subtasks, extra fields |
FileConfig |
File-level defaults: board, item_type, labels, status_map, assignee, sprint |
ParsedFile |
The full file: path + config + ordered list of Ticket | str items |
Storage (todo_files/storage/)
SQLite + Alembic. Tracks every known ticket and its sync state so that push can diff local state against the last-pushed snapshot without making an API call.
Sync engine (todo_files/sync.py)
Bridges the parser and the storage/Jira layers. Responsibilities:
assign_ids- walks the parsed ticket tree and assigns 8-char hex UUIDs to any ticket that lacks one; returnsTrueif the file needs to be written backticket_hash- stable SHA-256 of a ticket’s content fields (excludesid/remote_key), used to detect local changesbuild_plan- diffs aParsedFileagainst the DB and returns aSyncPlan(lists of tickets to create, update, delete, untrack, or leave clean); does not modify anythingexecute_plan- applies the plan to SQLite; Jira calls are the CLI’s responsibilitymark_synced- setssync_status=cleanfor tickets that were successfully pushed
Service mapper (todo_files/mappers/)
The only Jira-specific code. JiraMapper implements BaseMapper and translates between the internal AST and Jira REST API v3.
Status updates require a Jira transition (not a direct field update): the mapper fetches available transitions, matches by name, and POSTs the transition.
Adding a new backend (e.g. Linear, ClickUp) requires a new mapper class - parser, AST, and storage should remain unchanged.
CLI
todofiles push <file> # parse → SQLite → push to Jira
todofiles push --dry-run <file> # show what would change (no API call)
todofiles pull <file> # fetch from Jira → update SQLite + file
todofiles pull --dry-run <file> # show what would change without writing
todofiles import <file> <key> # fetch an existing Jira ticket and append it to file
todofiles config set <key=val> # set a config value
todofiles config show # print current config (api_token redacted)
todofiles config whoami # print your Jira account info
todofiles config status-map # print a status_map template from your Jira project
push flow
- Parse file → assign missing IDs → write IDs back if any were assigned
- Build sync plan (DB diff, no API call)
- Print plan; prompt for confirmation on each deletion (
delete/untrack/abort) - For each CREATE:
POST /rest/api/3/issue→ writejira: KEYback to file and DB - For each UPDATE:
PUT /rest/api/3/issue/{key}+ transition if status changed - For each DELETE:
DELETE /rest/api/3/issue/{key} - Update
sync_status → cleanin DB
pull flow
- Parse file → ensure all tickets have IDs
- For each ticket with a
remote_key, fetch from Jira and update the in-memory AST (remote wins) - Write the updated file; update DB; mark pulled tickets clean
Available at github.com/jamiebeverley/todo_files.
