A command-line tool that stores entries (tasks, notes) as markdown files and supports powerful filtering with a query language
- Features
- Usage Example
- Customizing Templates
- Sorting Entries
- Output Formats
- Query Syntax
- Installation
- Performance
- Interfaces
- Requirements
- Acknowledgments
- Per-project isolation: Each project has its own
MADO/directory, similar to.git— no global directories are used. This allows you to version‑controlMADO/alongside your code - Entry storage: Entries stored as
MAIN.mdfiles in timestamped directories (YYYYMMDDTHHMMSS/MAIN.md) inMADO/directory. The entry directory can also contain any additional files related to the entry — attachments, screenshots, logs, scripts, etc. Everything stays organized in one place
Start from scratch in a new project:
# Create a project directory and enter it
mkdir myproject
cd myproject
# Initialize MADO directory (like git init)
mado initOnce the MADO/ directory is initialized, you can work with entries
from any subdirectory within the project — just like Git, mado
automatically finds the nearest MADO/ directory by walking up the
file tree
# Create your first entry
mado newThe entry will be created with the following content:
- NAME:
- PRIORITY:
- TAGS:
- STATUS:
- DEADLINE:
You can fill it out as needed, for example:
- NAME: Fix login bug
- PRIORITY: 10
- TAGS: bug, critical, auth
- STATUS: opened
- DEADLINE: 20260615
The login page returns 500 error when using special characters.
...
No fields are required — you can omit any field entirely or leave its value empty. For example, when writing a note, you probably won't need the priority, status and deadline fields. When a field is omitted or left empty:
- NAME, STATUS default to empty string ""
- PRIORITY defaults to 0
- DEADLINE defaults to 99990101T000000
- TAGS defaults to a list with one empty string [""]
- PATH is a system field, always present and set to the full path of the entry's MAIN.md file. It cannot be changed or removed manually
- TIME is a system field, always present and set to the entry's directory name (creation timestamp in YYYYMMDDTHHMMSS format). It cannot be changed or removed — it reflects when the entry was created
- MTIME is a system field, automatically set to the modification time of the entry's file (YYYYMMDDTHHMMSS format). It updates whenever the entry file changes and cannot be manually edited
When you have many entries, you'll want to filter them:
# List all entries
mado list 'all'
# Find critical bugs
mado list 'tag = bug and priority > 5'
# Delete low priority entries
mado remove 'priority < 5'
# Filter by entry name (exact match)
mado list 'name = login'
# Filter by entry name (substring)
mado list 'name ~ login'
mado list 'name ~ "fix login"' # multiple words — quotes required
mado list 'name ^~ "Fix"' # Starts with "Fix"
mado list 'name !$~ "bug"' # NOT ends with "bug"
# Glob patterns (wildcards)
mado list 'name %~ "add*"' # Names starting with "add"
mado list 'name !%~ "temp*"' # Names NOT starting with "temp"
mado list 'name %~ "add\\*"' # Names literally "add*"
# Filter by entry name (fuzzy match)
mado list 'name ~~ "fx lgn b"' # Finds "Fix login bug"
mado list 'name !~~ lgn' # Entries that do NOT fuzzy match
# Filter by creation time
mado list 'time ~ 20260516' # Entries created on 2026-05-16
mado list 'time > 20260516T12' # Entries created after 2026-05-16 12:00:00
mado list 'time > 2023 and time < @year+1' # Entries created between 2023 and current year (inclusive)
# Search across all fields
mado list 'any ~ login'
mado list 'any ~ 10'
# Find entries with complex conditions
mado list '(tag = bug or tag = critical) and status = opened and deadline < @now'
mado list 'not (priority < 3 or status = closed)'
mado list '(priority > 5 and tag = urgent) or status = reopened'
# Syntactic sugar for matching multiple values
mado list 'status in (opened, reopened)' # Status equals "opened" OR "reopened"
mado list 'priority in (10, 20, 30)' # Priority equals 10 OR 20 OR 30
mado list 'tag has (bug, critical)' # Entry has BOTH "bug" AND "critical" tags
mado list 'tag ~~ anyof(lgn, fix, err)' # Tag fuzzy matches ANY of: "lgn", "fix", "err"
mado list 'tag ^~ allof("namespace1:","namespace2:")' # Entry has tags starting with ALL of the given prefixes
# Syntactic sugar for ranges
mado list 'priority in [50..100]' # Priority between 50 and 100 inclusive
mado list 'priority in [..50]' # Priority less than or equal to 50
mado list 'priority in [50..]' # Priority greater than or equal to 50
mado list 'deadline in [@today..@today+7]' # Deadline within next 7 days
mado list 'time in [20260601..20260630]' # Created in June 2026
mado list 'name in [h..z]' # Name starts with a letter from 'h' to 'z'
# Sort results
mado list -s +priority 'tag = bug' # bugs sorted by priority
mado list -s -priority,+time 'all' # highest priority first, oldest firstTemplates are stored in MADO/.templates/ as markdown files:
# Create a bug report template
cat > MADO/.templates/bug.md << 'EOF'
- NAME:
- PRIORITY: 10
- TAGS: bug
- STATUS: opened
## Steps to Reproduce
1.
## Expected Behavior
## Actual Behavior
EOFYou can create entries with custom templates:
mado new -t bug
# The template must exist at: MADO/.templates/bug.mdThe -s / --sort flag allows you to sort entries by one or more fields:
# Sort by priority (ascending by default)
mado list -s priority 'all'
# Sort by priority descending
mado list -s -priority 'all'
# Sort by multiple fields (priority ascending, then time descending)
mado list -s +priority,-time 'all'
# Sort by status, then name
mado list -s status,name 'all'Sort order:
+fieldorfield— ascending order (default)-field— descending order
Note: sort fields are matched fuzzily.
priois interpreted aspriority,sttsasstatus, etc. Exact match is always preferred
The -f flag controls how entries are displayed:
# Default format: path:1: fields
mado list 'all'
# Compatible with Emacs compile buffer and other tools that parse file:line:
# Paths only — useful for piping to other tools
mado list -f path 'all'
# Search across entry bodies with grep
grep 'match' $(mado list -f path 'all')
# Newline-delimited JSON for scripts
mado list -f jsonl 'all'
# Pipe JSON output to jq for advanced processing
mado list -f jsonl 'priority > 5' | jq '.name'
mado list -f jsonl 'all' | jq -s 'group_by(.status)'
mado list -f jsonl 'all' | jq -s 'sort_by(.priority)'Note: output formats are matched fuzzily.
jsis interpreted asjsonl,pthaspath, etc. Exact match is always preferred
The query language supports filtering entries using operators and keywords
| Operator | Description | Note |
|---|---|---|
> |
Greater than | For string/timestamp: lexicographic |
< |
Less than | For string/timestamp: lexicographic |
>= |
Greater than or equal | For string/timestamp: lexicographic |
<= |
Less than or equal | For string/timestamp: lexicographic |
= |
Equal / exact match | |
!= |
Not equal | |
~ |
Contains | For numbers: alias for = |
!~ |
Not contains | For numbers: alias for != |
~~ or f~ |
Fuzzy match | For numbers: alias for = |
!~~ or !f~ |
Not fuzzy match | For numbers: alias for != |
%~ or g~ |
Glob match (wildcard) | For numbers: alias for = |
!%~ or !g~ |
Not glob match | For numbers: alias for != |
^~ |
Starts with | For numbers: alias for = |
!^~ |
Not starts with | For numbers: alias for != |
$~ |
Ends with | For numbers: alias for = |
!$~ |
Not ends with | For numbers: alias for != |
Glob wildcards:
| Wildcard | Description | Example | Matches | Does not match |
|---|---|---|---|---|
* |
Matches any number of any characters including none | Law* |
Law, Laws, or Lawyer |
GrokLaw, La, or aw |
? |
Matches any single character | ?at |
Cat, cat, Bat or bat |
at |
[abc] |
Matches one character given in the bracket | [CB]at |
Cat or Bat |
cat, bat or CBat |
Note:
[a-z]range syntax is not supported yet
| Operator | Description |
|---|---|
and |
Logical AND |
or |
Logical OR |
xor |
Logical XOR (exactly one) |
not |
Logical NOT |
Precedence (from highest to lowest): not > and > xor > or
| Type | Description | Format | Examples |
|---|---|---|---|
| number | Integer value | 0-999 | 0, 10, 999 |
| string | Text value | Unquoted: [a-zA-Z_][a-zA-Z0-9_-]* or quoted: "..." or '...' |
bug, "fix login", 'проблема' |
| timestamp | Timestamp value | Subset of ISO 8601: YYYYMMDDTHHMMSS with optional shorter forms: YYYY, YYYYMM, YYYYMMDD, YYYYMMDDT, YYYYMMDDTHH, YYYYMMDDTHHMM, YYYYMMDDTHHMMSS |
2026, 202605, 20260516, 20260516T, 20260516T12, 20260516T1230, 20260516T123012 |
| Keyword | Type | Operators | Example |
|---|---|---|---|
priority |
number | all | priority > 5, priority != 10 |
tag |
string | all | tag = bug, tag ~ crit |
status |
string | all | status = opened, status ~ ope |
name |
string | all | name = "Fix bug", name ~~ "fx lgn" |
path |
string | all | path ~ "/home/user/projects" |
time |
timestamp | all | time > 20260505T1230 and time < 20260510T |
mtime |
timestamp | all | mtime > 20260505T1230 |
deadline |
timestamp | all | deadline > 20260505T1230 and deadline < 20260510T |
any |
string/timestamp/number | all | any ~ login, any = 10 |
all |
special | - | all |
untagged |
special | - | untagged |
unstatused |
special | - | unstatused |
unnamed |
special | - | unnamed |
unprioritized |
special | - | unprioritized |
undeadlined |
special | - | undeadlined |
Note: keywords are matched fuzzily.
priois interpreted aspriority,upredasunprioritized, etc. Exact match is always preferred
Note 2:
anysearches across all fields
Note 3: all operators also work with
anyof(...)andallof(...). These are syntactic sugar that expand to multiple conditions.anyof(...)expands withor,allof(...)expands withand. Examples:
status = anyof(opened, reopened)is equivalent tostatus = opened or status = reopenedtag ~ allof(bug, crit, fix)is equivalent totag ~ bug and tag ~ crit and tag ~ fixFor
=operator there are shorter alternatives:
status in (opened, reopened)is equivalent tostatus = anyof(opened, reopened)tag has (bug, crit)is equivalent totag = allof(bug, crit)
Note 4:
in [...]is syntactic sugar for range conditions. It expands to comparisons joined withand. Examples:
priority in [50..100]is equivalent topriority >= 50 and priority <= 100priority in [..50]is equivalent topriority <= 50priority in [50..]is equivalent topriority >= 50deadline in [@today..@today+7]is equivalent todeadline >= @today and deadline <= @today+7name in [a..z]is equivalent toname >= a and name <= z
Macros start with @ and are resolved at tokenize time — the value is
substituted before the AST is evaluated. Macros are simple: they
expand to a single token, not to expressions
Note: macros are matched fuzzily.
@tdayis interpreted as@today,@mnthas@month, etc. Exact match is always preferred
Resolve to timestamps. Accept optional offsets with +/-
| Macro | Resolution | Offset unit |
|---|---|---|
@now |
Current timestamp (YYYYMMDDTHHMMSS) |
days |
@today |
Today (YYYYMMDD) |
days |
@yesterday |
Yesterday (YYYYMMDD) |
days |
@tomorrow |
Tomorrow (YYYYMMDD) |
days |
@week |
Current week monday (YYYYMMDD) |
weeks |
@month |
Current month (YYYYMM) |
months |
@year |
Current year (YYYY) |
years |
@never |
99990101T000000 | days |
Examples:
mado list 'deadline > @week'
mado list 'deadline > @year'
mado list 'time > @today-4'
mado list 'time >= @week-1'
mado list 'deadline < @today+30'
mado list 'time ~ @month-2'
mado list 'mtime > @now-1'
mado list 'deadline != @never' # entries with non-default deadlineResolve to numeric values
| Macro | Value |
|---|---|
@max |
999 |
@min |
0 |
Examples:
mado list 'priority = @max' # highest priority entries
mado list 'priority = @min' # lowest/default priority entriesDownload the latest static binary from GitHub Releases. The binary works on any x86_64 Linux without dependencies
Clone the repository and build:
makeThis will produce an executable file ./mado
To build a statically linked binary:
make release
This will produce ./release/mado
Time to list 100,000 entries with filter query: ~1.2s
Note: benchmarked on AMD Ryzen 5 5600H (6 cores, 12 threads, 3.30 GHz base) with NVMe SSD
Note 2: Performance can be improved by hiding unnecessary fields. A field is considered unnecessary if it is hidden, not used in sorting, and not referenced in the query. Such fields are skipped during parsing. For example, hiding
mtime(--hide-mtime) eliminates onestat()system call per entry file, reducing I/O overhead
Time to list 100,000 entries with filter query and --hide-mtime:
~0.9s
mado is designed as a backend that can power different
frontends. The core is a fast C++ engine with a query language, while
interfaces handle display and interaction
emado provides a pretty Emacs interface with transient menus, interactive statistics, outline-mode sections, direct deletion from the buffer, etc
- Linux system (x86_64 for static binary; any arch for build from source)
- Build dependencies:
- C compiler
- C++ compiler
makeflexbison
- Tsoding's video on building a query language compiler — the original idea and inspiration
- philj56/fuzzy-match — fuzzy matching algorithm
- Glob implementation from Ciremun/password-manager

