Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Introduction

taskdb is a zero-config task tracker that lives entirely in plain Markdown files. No database, no server, no account required — just a .tasks/ folder sitting right inside your project.

It’s designed to be fast to pick up, impossible to vendor-lock, and friendly to both humans and LLMs.


Why taskdb?

  • Human-readable files: Every task is a .md file with YAML frontmatter. Open it in any editor, read it in any diff tool, commit it to git.
  • Statuses as directories: A task’s status is determined by which directory holds its symlink — no magic fields, no migrations.
  • LLM-native: Agents can create, update, comment on, and complete tasks using a dead-simple CLI. Great for planning and tracking work mid-session.
  • Truly zero-config: Run taskdb init once and you’re done. No config file needed, though there are plenty of override capabilities built-in w/ environment variables.

Quickstart

1. Install

Option A: release binary install (recommended)

curl -fsSL https://raw.githubusercontent.com/toastdriven/taskdb/main/scripts/install.sh | bash

Option B: global package install

bun add -g taskdb

See the Installation page for full details.

2. Initialise a project

Run this once inside your project directory:

$ taskdb init
Initialised project at: .tasks

This creates a .tasks/ directory with the standard status folders (ready, in-progress, done, complete).

3. Create your first task

$ taskdb create "Write the README"
#1: Write the README - (Ready)

4. Move it along

$ taskdb update 1 --status=in-progress
#1: Write the README - (In-progress)

$ taskdb comment 1 "Drafted the installation section"
$ taskdb complete 1
#1: Write the README - (Complete)

5. See what’s going on

# list everything
$ taskdb list

# filter by status
$ taskdb list --status=in-progress

# full-text search
$ taskdb search "README"

That’s the core loop. Head over to the Guides for more realistic walkthroughs, or jump straight to the CLI Reference for every flag and option.

Installation

The goal is to install once, then run taskdb ... directly from your shell.

curl -fsSL https://raw.githubusercontent.com/toastdriven/taskdb/main/scripts/install.sh | bash

This installs taskdb to /usr/local/bin by default (or $INSTALL_DIR if set).

Option B: global install with Bun

Requires Bun:

# install Bun (if needed)
curl -fsSL https://bun.sh/install | bash

# install taskdb globally
bun add -g taskdb

Verify installation

taskdb --help
# or
taskdb --version

Initialize a project

Run once inside the project you want to track:

taskdb init

This creates the .tasks/ directory structure.

Tip: override the project path with --project=<path> or TASKDB_PROJECT_PATH.

Guides

Use these guides when you want taskdb patterns, not just command syntax.

In this section

Suggested reading order

  1. Start with the TODO tracker guide.
  2. Read the LLM/agent workflow if you automate development tasks.
  3. Use the GitHub Issues replacement guide for team process setup.

Guide: Use taskdb as a simple TODO tracker

If you just want a clean personal TODO workflow, taskdb works well with a tiny status flow:

  • ready
  • in-progress
  • done
  • complete

Typical daily loop

# See what's ready
taskdb list --status=ready

# Create a task
taskdb create "Refactor auth middleware" --labels='["chore"]'

# Pick it up
taskdb update 3 --status=in-progress

# Add progress notes
taskdb comment 3 "Extracted shared validator"

# Finish
taskdb update 3 --status=done
taskdb complete 3

Tips

  • Keep titles short and action-oriented.
  • Put details in --description.
  • Use labels (feat, bug, docs, chore) for quick filtering.
  • Run taskdb search "keyword" when you remember text, not IDs.

Guide: LLM/agent planning workflow with taskdb

taskdb is a good fit for agentic workflows because each task is a file and every status change is explicit.

Example feature request

“I’d like to add a React-based contact form to my website.”

An agent can break this into tasks:

  • Define fields + validation rules
  • Create React form component
  • Add server endpoint
  • Add spam protection and tests
  • Document usage

Then track each step via CLI updates/comments.

Suggested AGENTS.md snippet

## Task Tracking

Before starting multi-step work, create tasks in `taskdb`.

- Use `taskdb create` for each subtask.
- Move active work to `in-progress`.
- Add comments as progress notes and decision logs.
- Mark completed work with `taskdb complete <task-identifier>`.
- Prefer small, reviewable tasks over large umbrella tasks.

Minimal command sequence

taskdb create "Define contact form schema" --labels='["feat","frontend"]'
taskdb create "Implement React contact form UI" --labels='["feat","frontend"]'
taskdb create "Add POST /contact handler" --labels='["feat","backend"]'

taskdb update 12 --status=in-progress
taskdb comment 12 "Schema agreed: name/email/message"
taskdb complete 12

Guide: taskdb as a lightweight GitHub Issues replacement

For small teams/projects, taskdb can replace Issues with local, versioned task files.

Why this works

  • Tasks are plain Markdown + YAML frontmatter.
  • You can review task changes in pull requests.
  • Status is visible via symlink directories (ready/, in-progress/, done/, complete/).
  • No external service required.

Team workflow

  1. Keep .tasks/ committed to git.
  2. Create tasks for bugs/features.
  3. Reference task IDs in commits/PRs.
  4. Update status/comments during implementation.
  5. Complete tasks when merged.

Conventions to adopt

  • Labels: bug, feat, docs, chore, priority:high
  • Comments should include decisions and blockers.
  • Use taskdb list --labels='["bug"]' for triage views.

Tradeoffs

  • No hosted UI out of the box.
  • No built-in assignees/milestones.
  • Best for low/medium scale projects where git is already the source of truth.

CLI Reference

Usage

All commands support shared global options:

  • --project <path>: override project root path (default: .tasks, or TASKDB_PROJECT_PATH)
  • --help: show usage and options

Many commands also support --format <output-format>.

Valid formats:

  • quiet: suppress non-error output
  • plain: human-readable output (terse one-line summaries for most commands; full multi-line details for view)
  • json: machine-readable JSON output

Task identifiers

Commands that accept <task-identifier> support:

  • integer id (1, 00001)
  • basename (00001-my-task)
  • path fragment (00000/00001-my-task.md), with or without project/status prefix

Commands

init

Initialise a project directory structure.

taskdb init [--format quiet|plain|json]

Options:

  • --format <quiet|plain|json>: output style

Example:

$ taskdb init
Initialised project at: .tasks

create <title>

Create a new task.

taskdb create "Title" [--description <text>] [--status <status>] [--labels <json-array>] [--format quiet|plain|json]

Options:

  • --description <text>: initial description body
  • --status <status>: initial status directory (default: ready)
  • --labels <json-array>: labels as JSON array string (example: '["feat","p1"]')
  • --format <quiet|plain|json>: output style

Example:

$ taskdb create "Write README" --status in-progress --labels='["docs","p1"]'
#1: Write README - (In-progress) - [docs, p1]

update <task-identifier>

Update one or more task fields.

taskdb update <task-identifier> [--title <text>] [--description <text>] [--status <status>] [--labels <json-array>] [--format quiet|plain|json]

Options:

  • --title <text>: replace title
  • --description <text>: replace description body
  • --status <status>: transition to another status
  • --labels <json-array>: replace labels with JSON array string
  • --format <quiet|plain|json>: output style

Example:

$ taskdb update 1 --status done --labels='["docs"]'
#1: Write README - (Done) - [docs]

view <task-identifier>

View a task in plain, raw markdown, or JSON format.

taskdb view <task-identifier> [--format plain|raw|json]

Options:

  • --format <plain|raw|json>:
    • plain: full multi-line task details
    • raw: markdown file content (frontmatter + body)
    • json: serialized task object

Example (plain):

$ taskdb view 1
Id: #1
Title: Write README
Slug: write-readme
Status: done
Labels:  docs
Created: 2026-05-16T10:00:00.000-05:00
Updated: 2026-05-16T10:05:00.000-05:00
Full Path: .tasks/all/00000/00001-write-readme.md

complete <task-identifier>

Transition a task to complete status and append a status-change comment.

taskdb complete <task-identifier> [--format quiet|plain|json]

Options:

  • --format <quiet|plain|json>: output style

Example:

$ taskdb complete 1
#1: Write README - (Complete) - [docs]

delete <task-identifier>

Permanently delete task file and status symlinks.

taskdb delete <task-identifier> [--format quiet|plain|json]

Options:

  • --format <quiet|plain|json>: output style

Example:

$ taskdb delete 1
Deleted task [00001] Write README

comment <task-identifier> <comment>

Append a task comment.

taskdb comment <task-identifier> "comment text" [--format quiet|plain|json]

Options:

  • <comment>: comment text to append
  • --format <quiet|plain|json>: output style

Example:

$ taskdb comment 1 "Added installation notes"
#1: Write README - (In-progress) - [docs]

list

List tasks, optionally filtered.

taskdb list [--status <status>] [--labels <json-array>] [--updated-before <date>] [--updated-after <date>] [--format plain|json]

Options:

  • --status <status>: only include tasks in this status
  • --labels <json-array>: include tasks containing all listed labels
  • --updated-before <date>: include tasks updated strictly before this date
  • --updated-after <date>: include tasks updated strictly after this date
  • --format <plain|json>: output style

Example:

$ taskdb list --status in-progress
#2: Implement auth - (In-progress) - [feat, backend]
#4: Write README - (In-progress) - [docs]

search <query>

Full-text search tasks (rg preferred, grep fallback).

taskdb search "query" [--format plain|json]

Options:

  • <query>: text to search for in task files
  • --format <plain|json>: output style

Example:

$ taskdb search "README"
#4: Write README - (In-progress) - [docs]

Reference

This section documents taskdb internals and implementation-facing behavior.

Use this when you need architecture details, data contracts, or code-level orientation.

In this section

API Overview

This page summarizes internal module boundaries. Detailed API docs are split per source file.

Entry points

  • taskdb.ts: executable entrypoint (#!/usr/bin/env bun), calls run(process.argv.slice(2)).
  • src/cli.ts: creates Commander program and runs argument parsing.

CLI layer

  • createProgram(output?): builds configured Commander instance and registers commands.
  • run(args, output?): runs CLI and returns process-like exit code.

Output formatting behavior

Command output rendering is centralized in src/commands/helpers.ts:

  • formatTask(task, output): terse single-line output (#<id>: <title> - (<Status>) - [labels]).
  • formatFullTask(task, output): multi-line task details (used by view --format plain).
  • formatTaskJSON(task, output): single task JSON output.
  • formatTaskList(tasks, output): terse list output.
  • formatTaskListJSON(tasks, output): list JSON output.

Command registration

  • src/commands/index.ts: registerAllCommands(program, output)
  • Commands: init, create, update, view, complete, delete, comment, list, search.

Detailed module API docs

src/constants.ts

Shared constants used across taskdb, with optional environment-variable overrides.

Exports

DEFAULT_PROJECT_PATH: string

  • Default: .tasks
  • Env override: TASKDB_DEFAULT_PROJECT_PATH
  • Used when --project is not provided.

ALL_TASKS_DIR: string

  • Default: all
  • Env override: TASKDB_ALL_TASKS_DIR
  • Canonical task file directory.

COMPLETE_TASKS_DIR: string

  • Default: complete
  • Env override: TASKDB_DONE_TASKS_DIR
  • Conventional completed-task status directory.

DEFAULT_STATUSES: string[]

  • Default: ["ready", "in-progress", "done"]
  • Env override: TASKDB_DEFAULT_STATUSES (CSV)
  • Parsed via parseCsvEnv.

NON_STATUS_DIRS: Set<string>

  • Default contents: ALL_TASKS_DIR
  • Env override: TASKDB_NON_STATUS_DIRS (CSV)
  • Directories excluded from status detection.

DESCRIPTION_HEADER: string

  • Default: ## Description
  • Env override: TASKDB_DESCRIPTION_HEADER
  • Markdown heading used when serializing/parsing description sections.

COMMENTS_HEADER: string

  • Default: ## Task Comments
  • Env override: TASKDB_COMMENTS_HEADER
  • Markdown heading used when serializing/parsing comments sections.

MAX_FILES_PER_DIR: number

  • Value: 32768
  • Used for grouped task directory computation (Task.groupDir).

src/models/task.ts (Task)

Represents one canonical task markdown file.

Constructor

new Task(params)

Creates an in-memory task instance.

params fields:

  • id: number
  • slug: string
  • title: string
  • labels: string[]
  • created: string
  • updated: string
  • description: string
  • comments: TaskComment[]
  • projectPath: string
  • status?: string

Static helpers

Task.idString(id: number): string

Returns 10-digit id string used in frontmatter (example: 1 -> 0000000001).

Task.idPad(id: number): string

Returns 5-digit id string used in filenames/group paths (example: 1 -> 00001).

Task.groupDir(id: number): string

Returns group directory name by bucket (Math.floor((id - 1) / MAX_FILES_PER_DIR), zero-padded to 5).

Task.buildCommentsSeparator(): string

Returns computed markdown separator between description/comments:

\n---\n\n${COMMENTS_HEADER}\n\n

Task.parseBody(content: string): { description: string; comments: TaskComment[] }

Parses markdown body (post-frontmatter) into normalized description + comments.

Task.parseComments(tableText: string): TaskComment[]

Parses comments markdown table rows into TaskComment[].

Task.read(filePath: string, projectPath: string, status?: string): Promise<Task>

Reads markdown file, parses frontmatter/body, and returns hydrated Task.

Instance properties/getters

filename: string (getter)

<idPad>-<slug>.md

filePath: string (getter)

Absolute path: <projectPath>/<ALL_TASKS_DIR>/<groupDir>/<filename>.

groupDirPath: string (getter)

Absolute group directory path for this task.

Instance methods

buildBody(): string

Builds markdown body (Description section + comments table).

buildFileContent(): string

Builds full file text via gray-matter.stringify (frontmatter + body).

write(): Promise<void>

Ensures group dir exists and writes file content.

create(): Promise<void>

First-write lifecycle:

  1. slug from title (toSlug)
  2. set created and updated (makeRfc3339)
  3. write file

addComment(comment: string): Promise<void>

Appends comment row, bumps updated, writes.

updateTitle(title: string): Promise<void>

Updates title, bumps updated, writes.

updateDescription(description: string): Promise<void>

Updates description, bumps updated, writes.

updateLabels(labels: string[]): Promise<void>

Replaces labels, bumps updated, writes.

deleteFile(): Promise<void>

Deletes canonical task file only (does not remove status symlinks).

toJSON(): object

Returns plain object for CLI JSON output.

src/models/project.ts (Project)

Represents a taskdb project root (.tasks) and manages filesystem/status operations.

Types

ListFilter

  • status?: string
  • labels?: string[]
  • updatedBefore?: Date
  • updatedAfter?: Date

Used by listTasks.

Constructor

new Project({ path })

  • path: string absolute project path.

Directory/status lifecycle

scaffold(): Promise<void>

Creates grouped root directories for:

  • ALL_TASKS_DIR
  • COMPLETE_TASKS_DIR
  • each entry in DEFAULT_STATUSES

isInitialized(): Promise<boolean>

Returns true if canonical tasks directory exists.

getStatusDirs(): Promise<string[]>

Lists project subdirs treated as status directories (directories not in NON_STATUS_DIRS).

ensureStatusDir(status: string, groupDir: string): Promise<boolean>

Ensures <status>/<groupDir> exists. Returns true when the status directory is new.

maxTaskId(): Promise<number>

Scans canonical task files and returns max numeric task id, or 0 if none.

createStatusSymlink(status: string, id: number, filename: string): Promise<void>

Creates status symlink pointing to canonical task file.

removeStatusSymlink(status: string, id: number, filename: string): Promise<void>

Removes status symlink; ignores missing link.

getTaskStatus(id: number, filename: string): Promise<string | null>

Finds which status directory currently contains the symlink.

transitionStatus(task: Task, newStatus: string, warn?: OutputFn): Promise<void>

Moves task symlink to newStatus, optionally warning when status dir is newly introduced. Also updates task.status in memory.

Task resolution and listing

resolveTask(identifier: string): Promise<Task | null>

Supports:

  • numeric id (1, 00001)
  • basename (00001-my-task)
  • path fragment (00000/00001-my-task.md)

findTaskById(id: number): Promise<Task | null>

Finds task by numeric id in canonical storage and hydrates Task.

listTasks(filters?: ListFilter): Promise<Task[]>

Returns tasks sorted by id asc.

  • If filters.status is set, scans that status directory.
  • Always reads canonical files from ALL_TASKS_DIR.
  • Applies label + updated-before/after filters.

_isRipgrepAvailable(): Promise<boolean>

Checks if rg is available on PATH.

searchTasks(query: string): Promise<Task[]>

Full-text search over task files:

  • rg -l when available
  • fallback: grep -rl

Returns hydrated tasks sorted by id asc.

High-level mutations

createTask(title: string, description = "", status = "ready", labels: string[] = [], warn?: OutputFn): Promise<Task>

Creates task with next id, writes canonical file, and creates status symlink.

deleteTask(task: Task): Promise<void>

Removes all status symlinks and deletes canonical task file.

src/utils/datetime.ts

makeRfc3339(date?: Date): string

Formats a date as RFC 3339 with:

  • millisecond precision
  • local timezone offset (±HH:MM)

Parameters

  • date (optional): Date to format. Defaults to new Date().

Returns

  • RFC 3339 datetime string.

Example

makeRfc3339();
// "2026-05-14T18:26:13.246-05:00"

src/utils/env.ts

parseCsvEnv(value: string | undefined, fallback: string[]): string[]

Parses a comma-separated env var string into a trimmed string array.

Behavior:

  • undefined/empty -> returns fallback
  • trims whitespace around each entry
  • drops empty entries
  • if parsed list becomes empty, returns fallback

Parameters

  • value: raw env var value
  • fallback: fallback array

Returns

  • parsed list or fallback

Examples

parseCsvEnv("a, b, c", ["x"]); // ["a", "b", "c"]
parseCsvEnv(" , ", ["x"]);      // ["x"]

src/utils/slug.ts

toSlug(name: string): string

Converts human-readable text into a filesystem-safe slug.

Rules (in order):

  1. trim
  2. lowercase
  3. remove non [a-z0-9\s-]
  4. replace whitespace runs with -
  5. collapse repeated -
  6. strip leading/trailing -

Parameters

  • name: input string

Returns

  • normalized slug

Examples

toSlug("My Cool Project!"); // "my-cool-project"
toSlug("  Hello   World  "); // "hello-world"

src/utils/strings.ts

capitalizeFirst(value: string): string

Capitalizes the first character in a string using locale-aware uppercasing.

Behavior:

  • Empty string returns unchanged.
  • First character uses toLocaleUpperCase().
  • Remaining string is unchanged.

Parameters

  • value: input string

Returns

  • input with first character capitalized

Example

capitalizeFirst("in-progress"); // "In-progress"

Specifications

This page defines the behavioral/data contract for taskdb.

Filestructure Layout

The tasks for the project are stored by convention under .tasks/ in the current working directory. This is overridable using either the --project=... or TASKDB_PROJECT_PATH environment variable to specify a different path.

Within this directory are two mandatory subdirectories: all & complete.

The all directory holds all of the task files within set of zero-padded subdirectories. Each task file gets an auto-incrementing/unique integer, which is zero-padded to retain ordering under normal lexicographical conditions (e.g. 0001 -> 0009, then 0010).

Note: This structure of nested subdirectories was chosen due to common filesystem limitations, preventing more than ~32,768 files in a single directory. By computing the integer, splitting up the numbering across multiple subdirectories (<NNNNN>/<NNNNN>-<slug>.md, e.g. 00000/00001-initial-setup.md), this allows for 32768 * 32768, or over 1 BILLION tasks.

If you need more than 1B tasks for a single project, you may want to investigate a different solution…

The complete directory holds symlinks to all of the completed task files. It mirrors the all layout/filepaths.

The filestructure layout ends up looking like:

.tasks/
|   # Required, where all the task files actually live
├── all/
|   └── <NNNNN>/
|       ├── <NNNNN-task-slug>.md
|       ├── <NNNNN-task-slug>.md
|       └── <NNNNN-task-slug>.md
|   # Required, symlinks to all the completed tasks
├── complete/
|   └── <NNNNN>/
|       |   # ... & are just symlinks back to `all/`.
|       └── <NNNNN-task-slug>.md
|   # Task statuses are sibling subdirectories, ...
├── needs-definition/
|   └── <NNNNN>/
|       |   # ... & are just symlinks back to `all/`.
|       └── <NNNNN-task-slug>.md
├── ready/
├── in-progress/
├── on-hold/
├── done/
└── wontfix/

Other subdirectories live alongside all/complete, and make up task statuses. These status subdirectories have an identical internal structure to the all/ directory. Inside each are symlinks back to the task file’s original location in all/.

Status directory names are arbitrary. You can rename these directories, remove them, or add others. In this way, taskdb adapts to your desired set of statuses without code changes/configuration.


Status Model

Status is not stored in task frontmatter.

A task’s current status is determined by which status directory contains its symlink. Status transitions remove the old symlink and create a new one in the target status directory.


Task File Format

Each task is stored as a Markdown file, with YAML frontmatter fields for structured metadata.

Required frontmatter fields:

  • id - The task id. Integer, but expressed as a zero-padded string, for easy splitting. Auto-incrementing & unique.
  • slug - The slug is computed from the title one-time (at creation). It’s immutable, & is non-unique.
  • title - The human-readable task title. What needs to be done.
  • labels - A YAML array of string labels/tags (e.g. ['feat'], or ['chore', 'easy']). Can be empty. Labels are arbitrary.
  • created - An RFC 3339-formatted datetime string of when the task was created. Computed one-time (at creation) & immutable.
  • updated - An RFC 3339-formatted datetime string of when the task was last updated. Computed every time there’s an update made to the task.

The Markdown body contains:

  1. ## Description section (free-form task details)
  2. horizontal rule (---)
  3. ## Task Comments table with columns Commented At and Comment

Example Task File

For example, the first task of a project (e.g. “Initial Setup”) would live at .tasks/all/00000/00001-initial-setup.md, & might look like:

---
id: "0000000001"
slug: "initial-setup"
title: "Initial Setup"
labels: ["feat"]
created: "2026-05-14T18:26:13.246-05:00"
updated: "2026-05-14T18:28:54.123-05:00"
---

## Description

Perform the initial setup steps on the codebase. This includes scaffolding out a `src/` directory, a `tests/` directory, creating all the project files, adding a `.gitignore` & a `README.md`, etc. Also run `git init .`.

---

## Task Comments

| Commented At                  | Comment                                  |
| ----------------------------- | ---------------------------------------- |
| 2026-05-14T18:27:10.246-05:00 | Status changed from ready to in-progress |

Slug Generation

Task slugs are generated from the task title at creation time, then treated as immutable.

Rules (in order):

  1. Trim surrounding whitespace.
  2. Lowercase the string.
  3. Remove any character that is not a-z, 0-9, whitespace, or -.
  4. Replace runs of whitespace with a single -.
  5. Collapse repeated hyphens (----).
  6. Remove leading/trailing hyphens.

Examples:

  • "My Cool Project!""my-cool-project"
  • " Hello World ""hello-world"

Notes:

  • Slugs are not unique; task identity comes from the numeric id.
  • Updating a task title does not change its slug or filename.

Task Identifiers

Commands that accept <task-identifier> support:

  • integer ID (1 or 00001)
  • full basename (00001-my-task)
  • path fragment (00000/00001-my-task.md), with or without status/project prefix