Skip to content

Grammar

Examples with Playground links are written as short, complete .tdsl files. Each link passes the same content to the Playground as a source query.

timeline "Reading Notes" {
title "Reading Notes";
unit year;
range 2024..2026;
calendar proleptic_gregorian;
}
lane "Books" as books { kind custom; order 10; }
event books 2025 "Trying Timeline DSL" { id "event:first"; };

Open in Playground

A .tdsl file combines timeline (the overall timeline), lane (vertical axis), and time elements: event, span, and event_range.

timeline "Product Roadmap" {
title "Product Roadmap";
unit year;
range 2025..2027;
calendar proleptic_gregorian;
}
lane "milestones" as milestones { kind custom; order 10; }
event milestones 2026 "Public Beta" {};

Open in Playground

range 2025..2027; defines the display and validation range. A good starting point is to fix unit year; and calendar proleptic_gregorian; to build a year-based timeline.

lane is the vertical axis of the timeline. Give it a short ID with as milestones, which is then referenced by subsequent event, span, and event_range entries.

lane "milestones" as milestones { kind custom; order 10; }
event milestones 2026 "Public Beta" {};

order controls display order — smaller values appear higher. kind is treated as classification metadata.

group bundles multiple lanes and visually organizes them with a group label and separator lines (v1.13.0+). Place lanes inside group "<name>" { ... }.

timeline "Org Timeline" {
title "Org Timeline";
unit year;
range 2025..2027;
calendar proleptic_gregorian;
}
group "Engineering" {
lane "Frontend" as frontend { kind custom; order 10; }
lane "Backend" as backend { kind custom; order 20; }
}
event frontend 2026 "UI Revamp" { id "event:ui"; };
event backend 2026 "API v2" { id "event:api"; };

Open in Playground

The group name is shown as a label above its lanes, separated by boundary lines. A lane that does not belong to a group is rendered independently, so you can adopt grouping incrementally in existing timelines (backward compatible).

event is a point event placed at a single year.

timeline "Product Roadmap" {
title "Product Roadmap";
unit year;
range 2025..2027;
calendar proleptic_gregorian;
}
lane "milestones" as milestones { kind custom; order 10; }
event milestones 2026 "Public Beta" { tags ["release"]; id "event:beta"; };

Open in Playground

Write it as event <lane-id> <year> "<label>" { ... };. The label is displayed on the timeline.

span is used for items with a start year and end year, such as durations or phases.

timeline "Small Project" {
title "Small Project";
unit year;
range 2025..2028;
calendar proleptic_gregorian;
}
lane "phases" as phases { kind custom; order 10; }
span phases 2025..2026 "Design" { tags ["phase"]; id "span:design"; };
span phases 2026..2028 "Build" { tags ["phase"]; id "span:build"; };

Open in Playground

Write it as span <lane-id> <start>..<end> "<label>" { ... };.

event_range is used for periods you want to treat as events, such as wars, transition periods, or incident responses.

timeline "Release Plan" {
title "Release Plan";
unit year;
range 2025..2027;
calendar proleptic_gregorian;
}
lane "Work" as work { kind custom; order 10; }
event_range work 2025..2026 "Transition Period" { tags ["migration"]; };
event work 2027 "New system goes live" {};

Open in Playground

It looks similar to span in that it has a duration, but semantically it means "an event with a duration." Use span for eras or membership periods, and event_range for work, incidents, or wars — the distinction makes the timeline easier to read.

The end of a span or event_range can use the now keyword from v1.23.0 onwards. This lets you express a period that is "still ongoing" — such as the current era — without inventing a fictional end year.

timeline "Reign Period" {
title "Reign Period";
unit year;
range 2019..2030;
calendar proleptic_gregorian;
}
lane "Era" as era { kind custom; order 10; }
span era 2019..now "Reiwa" { tags ["era"]; id "span:reiwa"; };

now is only valid in the end position of span / event_range. The IR gains an end_open flag, which is non-breaking for existing JSON consumers. tdsl fmt and tdsl decompile round-trip now as-is.

event, span, and event_range all accept common metadata inside { ... }.

timeline "Release History" {
title "Release History";
unit year;
range 2024..2026;
calendar proleptic_gregorian;
}
lane "Releases" as releases { kind custom; order 10; }
event releases 2026 "v1.0 Released" {
tags ["release", "stable"];
source wd:Q95;
origin manual;
id "event:v1";
};

Open in Playground

tags is for classification, id is a stable identifier used for review and diff tracking. source wd:Q95; records a Wikidata QID as provenance metadata. origin manual; indicates hand-written data.

To use Wikidata, import QIDs with import wikidata and transform them into timeline elements with map.

timeline "Chinese Dynasties" {
title "Chinese Dynasties";
unit year;
range -300..300;
calendar proleptic_gregorian;
}
lane "Dynasties" as dynasties { kind dynasty; order 10; }
event dynasties -221 "Qin unifies China" { tags ["manual"]; };
import wikidata as wd {
entity Q7209 as han_dynasty;
policy merge_by_source;
}
map wd.han_dynasty to span {
lane dynasties;
start claim(P571).year;
end claim(P576).year;
label label@ja ?? label@en;
tags ["dynasty", "imported"];
}

Open in Playground

In the browser-based Playground, Wikidata network fetches are not performed. You can open the file for syntax checking, but items from import may be omitted from the rendered output. For content you want reliably displayed on the web, write it as static event, span, or event_range entries — like the event dynasties -221 ... example above.

To resolve Wikidata and output JSON or HTML, use the CLI.

Terminal window
tdsl build china.tdsl --pretty --output china.json
tdsl render china.tdsl --output china.html

To avoid network fetches in CI, add the --offline flag.

Terminal window
tdsl build china.tdsl --offline --pretty

You can add a filter clause to a map block to include only entities that match the condition, filtering out unwanted data.

map wd.items to span {
lane dynasties;
start claim(P571).year;
end claim(P576).year;
label label@ja ?? label@en;
filter claim(P31) == "Q7209";
}

Operators available in filter: ==, !=, <, <=, >, >=, &&, ||, !, null, claim(). Multiple filter clauses are combined as AND conditions.

Since v1.16.0, you can use the string-matching operators contains / startswith against labels. Use label@<lang> contains "..." to keep entities whose label contains a substring, and label@<lang> startswith "..." to keep entities whose label starts with the given string.

map wd.items to span {
lane dynasties;
start claim(P571).year;
end claim(P576).year;
label label@ja ?? label@en;
filter label@ja contains "王朝"; // label contains "王朝"
filter label@en startswith "Han"; // label starts with "Han"
}

These combine with !, &&, and || (e.g. filter !(label@ja contains "候補");). Entities that lack a label in the specified language are treated as false (excluded); there is no silent fallback to another language.

Month and Day Precision on the Timeline Axis

Section titled “Month and Day Precision on the Timeline Axis”

Since v1.7.0, the timeline axis supports month and day precision. In v1.9.0, you can now write date literals YYYY-MM-DD / YYYY-MM directly in the time / start / end fields of static event / span entries.

timeline "Modern History" {
title "Modern History";
unit year;
range 1900..2000;
calendar proleptic_gregorian;
}
lane "events" as events { kind custom; order 10; }
event events 1945-08-15 "End of World War II" {};
event events 1914-07 "Outbreak of World War I" {};

Date literals are placed at their exact positions on the timeline axis. When you specify month precision (YYYY-MM), it is treated as the first day of that month. Wikidata claim values that carry month or day precision are placed at their exact positions in the same way.

Hour and minute precision (unit hour / unit minute)

Section titled “Hour and minute precision (unit hour / unit minute)”

Setting unit hour; or unit minute; draws a sub-day time axis finer than day precision. Write start / end / time as YYYY-MM-DDTHH:MM date-time literals. From v1.23.0 onwards, axis ticks thin out automatically based on density (1h/3h/6h/12h for hour units, 1min/5min/15min/30min for minute units), with HH:MM / MM-DD HH:MM labels.

timeline "Apollo 11 Landing Day" {
title "Apollo 11 — Landing Day (UTC)";
unit hour;
range 1969-07-20T00:00..1969-07-21T06:00;
calendar proleptic_gregorian;
}
lane "Mission" as mission { kind custom; order 10; }
event mission 1969-07-20T20:17 "Moon Landing" { tags ["highlight"]; id "event:landing"; };

Open in Playground

The ?? operator can be used not only for label but also for start, end, and time. When the left-hand side is null, the right-hand side is tried.

map wd.items to span {
lane dynasties;
start claim(P580).year ?? claim(P571).year;
end claim(P582).year ?? claim(P576).year;
label label@ja ?? label@en;
}

Fully compatible with existing single-expression forms (e.g., start claim(P571).year). Use this when you want to extract the first valid value from multiple properties.

Since v1.16.0, writing expand claim(P) inside a map block expands every non-deprecated Statement of the entity's property P, generating one item per Statement. Without expand, only the first Statement is referenced as before.

map wd.elizabeth_ii to span {
lane offices;
expand claim(P39); // expand every office held (P39)
start claim(P39).qualifier(P580).year; // start qualifier of each Statement
end claim(P39).qualifier(P582).year ?? 9999;
label label@ja;
}

If P39 has multiple Statements, multiple spans are generated. A Statement without the qualifiers (P580 / P582) is skipped for that item, since start / end cannot be resolved.

In a map block's time-value expressions (start / end / time), you can add or subtract an integer from a claim-derived year (v1.11.0+). Write +N / -N immediately after the accessor (e.g. .year).

map wd.people to span {
lane people;
start claim(P569).year +1; // birth year + 1
end claim(P570).year -5; // death year − 5
label label@ja ?? label@en;
}
  • The offset is applied to the year after the claim is resolved. Negatives are written -N, and +0 is valid.
  • It composes with ?? (fallback); the offset attaches to each claim term: start claim(P580).year ?? claim(P571).year +1;.
  • decompile / inspect output and the Playground Format reproduce it as + N / - N.