Tidigar Roadmap Interoperability Specification
See the roadmap JSON example for an activity with dates, progress and a dependency in this format.
Status: public pre-release specification. It defines how another application, script or AI agent can create, read and edit Tidigar roadmaps stored as JSON files or .tidigar documents. Calendar events are described in the calendar storage specification.
License: published by Hamacher Software AB under CC BY 4.0. Anyone may implement the formats it describes, in any software, without asking.
The keywords MUST, MUST NOT, SHOULD, SHOULD NOT and MAY are normative. The canonical Tidigar project is specified field by field, followed by the envelopes and mappings used by each storage backend. JSON field names remain descriptive; only the calendar URL has a separate packed transport, and shareable share links have their own compact encoding in the share link specification. The same project can also be written by hand or reviewed as Markdown, as the Markdown specification describes.
Implementer quick start
- Read a project through one of the storage profiles.
- Validate and normalize it using every rule in the field tables. Unknown project fields MUST NOT be relied upon: Tidigar drops them when it writes a canonical copy.
- Preserve array order. IDs are UUIDs and MUST remain stable when editing an existing record. New IDs SHOULD be cryptographically random UUIDv4 values.
- Write the same storage profile unless the user explicitly converts it.
- For calendars, follow the calendar storage specification.
A conforming reader rejects unsupported versions and invalid required fields rather than guessing. A conforming writer emits all required fields, valid references and exactly the current versions in the version table. Tidigar is pre-release: there is no compatibility reader or migration for an older incompatible version.
How to read the tables
- Required:
yesmeans a record without the field is rejected.nomeans a missing field gets the default.derivedmeans the value is computed from other fields and any stored value is replaced. - Rule: a constraint a valid value MUST meet, such as
max N chars,max N entriesorone of …. - Default: a JSON value in backticks is used when the field is missing; a sentence describes a computed default.
- Calendar: where the field lives in a calendar event.
URL m.<key>means inside the compact metadata of the event URL (see the calendar storage specification). - Used by: where Tidigar shows or uses the field.
Normalization is a whitelist: any field not listed here is dropped when a project is loaded or saved. All IDs are UUIDs (versions 1–8), unique across the whole project. Dates are YYYY-MM-DD calendar dates from 1900-01-01 through 2200-12-31, without time or time zone. Colors are #rrggbb.
Project
{ manifest, items, sharedViews, milestones, periodIndicators }
| Collection | Holds | Limit |
|---|---|---|
items | Activities | 10,000 |
sharedViews | Saved views | 99 |
milestones | Milestones | 1,000 |
periodIndicators | Period indicators | 1,000 |
Array order is meaningful and preserved in every backend; a calendar keeps it as a rank in each record's event URL.
Manifest
| Field | Type | Required | Rule | Default | Calendar | Used by |
|---|---|---|---|---|---|---|
projectId | UUID | yes | Project identity; every record's projectId must equal it | — | URL p on every event | Identity in all storage profiles and links |
formatVersion | integer | yes | one of 1 | — | Manifest Notes | Rejects other model versions on load |
name | text | yes | max 100 chars; not blank | — | Manifest Notes; manifest event title | Roadmap title, library card, calendar name at creation |
description | text | yes | max 500 chars; may be empty | — | Manifest Notes | Roadmap dialog |
dimensions | array of dimension | yes | max 20 entries | — | One dimension event each | Board axes, filters, colors, grouping, List columns |
year | integer | yes | 1900 through 2200 | — | Manifest Notes | Default timeline range of a view without one |
schedule | enum | no | one of calendar-days, work-days; see Work days | "calendar-days" | Manifest Notes | Date changes, activity dialog, compacting the plan |
shareLinkCreatedAt | UTC timestamp | no | ISO 8601 YYYY-MM-DDTHH:mm:ssZ, valid date and time | absent | Manifest Notes | Shared version in the library and properties |
refPrefix | text | no | 1 to 10 capital letters A–Z and digits, starting with a letter; omitted when empty | absent | Manifest Notes | Reference keys of new records; library card |
shareLinkCreatedAt records when the sharing link that created this copy was created. It is preserved when editing or exporting the copy, independently of storage creation and modification times. An older link has no timestamp; readers must not invent one. Sharing the copy again creates a new link timestamp.
refPrefix starts the reference keys an editor gives new activities and milestones.
Dimension
| Field | Type | Required | Rule | Default | Calendar | Used by |
|---|---|---|---|---|---|---|
id | UUID | yes | — | — | Dimension Notes; URL i | Item values, view axes, filters and columns |
name | text | yes | max 100 chars; may be empty | — | Dimension Notes; event title | Dimension labels and column headers |
options | array of option | yes | max 100 entries | — | Dimension Notes | Values an item can take in this dimension |
Option
| Field | Type | Required | Rule | Default | Calendar | Used by |
|---|---|---|---|---|---|---|
id | UUID | yes | — | — | Dimension Notes | Item values, filters, colorOverrides |
name | text | yes | max 100 chars; may be empty | — | Dimension Notes | Option labels |
color | color | yes | #rrggbb | — | Dimension Notes | Item color when the view colors by this dimension |
Item (activity)
| Field | Type | Required | Rule | Default | Calendar | Used by |
|---|---|---|---|---|---|---|
id | UUID | yes | — | — | URL i | Identity, dependencies, links |
projectId | UUID | yes | Equals manifest.projectId | — | URL p | Ownership check |
title | text | yes | max 180 chars; may be empty | — | Event title | All views, activity dialog |
description | text | yes | max 12000 chars; may be empty | — | Event notes | Activity dialog, List |
progress | integer | no | one of 0, 25, 50, 75, 100 | 0 | URL m.progress | Progress bar, List |
start | date | yes | Not after end | — | All-day event start | Timeline, List, dependencies |
end | date | yes | Inclusive last day | — | All-day event end, the day after end | Timeline, List, dependencies |
values | object | no | Dimension ID → option ID of that dimension, or "" | {} | URL m.values | Board placement, filters, colors, List |
dependsOn | array of IDs | no | Items and fixed milestones; no duplicates or cycles; start at least one day after each item's end unless startAfter allows overlap, and not before each fixed milestone's date | [] | URL m.dependsOn | Timeline arrows, date cascades |
startAfter | object | no | Item ID → one of 0, 25, 50, 75; IDs outside dependsOn are dropped; such an item's end rule becomes start not before its start; omitted when empty | — | URL m.startAfter | Overlap, timeline arrows, date cascades |
ref | text | no | max 32 chars; omitted when empty; need not be unique | absent | URL m.ref | Reference key before the title, search |
startAfter lets an activity overlap one it depends on. The value is how much of that activity’s planned duration has passed when this one may start, independently of its reported progress: 0 with its start, 50 when half its days have passed. An editor places the activity on the day after that share of days, rounded up and counted in work days when manifest.schedule is work-days, and moves it along when the other activity moves. The stored rule only requires that it does not start before the other activity, so a new vacation period never invalidates a project. An absent entry means 100%: start at least one day after the predecessor ends. The latest threshold applies when there are several dependencies. Fixed milestones cannot appear in startAfter; activities may start on their date. Milestones that depend on activities always wait for those activities to end.
Milestone
| Field | Type | Required | Rule | Default | Calendar | Used by |
|---|---|---|---|---|---|---|
id | UUID | yes | — | — | URL i | Identity, links |
projectId | UUID | yes | Equals manifest.projectId | — | URL p | Ownership check |
title | text | yes | max 180 chars; not blank | — | Event title | Timeline, milestone dialog |
date | date | yes | At least one day after each dependency's end | — | Timed event 12:00–12:15 local time on that day | Timeline |
dependsOn | array of item IDs | no | Items only; no duplicates; empty when fixed | [] | URL m.dependsOn | Timeline arrows, date cascades |
fixed | boolean | no | An event on a set date that items wait for | false | URL m.fixed | Timeline, date cascades |
ref | text | no | max 32 chars; omitted when empty; need not be unique | absent | URL m.ref | Reference key before the title, search |
source | object | no | projectId and id, both UUIDs, of another plan's milestone; only when fixed | absent | URL m.source | Incoming milestone, warnings |
A milestone is either ordinary or fixed. An ordinary milestone, the default, marks a result: it waits for the items in its dependsOn and its date follows them. A fixed milestone is an event on a set date, such as a decision or a delivery: it waits for nothing, items list it in their own dependsOn, and they may start on its date. Editors draw an ordinary milestone as a filled diamond and a fixed one as an outlined diamond. Editors move a fixed milestone only when the user does, and move the items that wait for it with it.
An incoming milestone is a fixed milestone whose source names a milestone of another plan: that plan's manifest.projectId and the milestone's id. Its title and date are this plan's own copy. An editor compares them with the newest copy of the source plan it has and shows when the source moved or removed the milestone; it changes the date only when the user takes the change in. Removing source makes the milestone this plan's own. A source that names this plan itself is invalid. A milestone that stops being fixed loses its source, and a share link leaves source out, so the milestone opens as an ordinary fixed one.
Reference keys
ref is the people-facing key of an activity or a milestone, such as LPP-12, or a number from another system typed or pasted by a person, such as a ticket number. It is free text that a person may change at any time. Keys are not unique: two people may give records the same key before their copies merge, so a reader MUST accept duplicates, and an editor SHOULD point them out rather than refuse them.
An editor that numbers records gives a new activity or milestone the key <refPrefix>-<n>, where n is one more than the highest number any activity or milestone already uses after that prefix, compared without regard to letter case. It does not number existing records. It derives refPrefix once, the first time the project has a name other than the editor's default name for a new roadmap: from two or three words their initials, otherwise the name's first two letters, with letters folded to A–Z and everything else left out, so Launch partner portal gives LPP, Tidigar TI and Öresund OR. A name without such letters gives no prefix. Renaming the project later does not change the prefix; a person may.
Period indicator
| Field | Type | Required | Rule | Default | Calendar | Used by |
|---|---|---|---|---|---|---|
id | UUID | yes | — | — | URL i | Identity, links |
projectId | UUID | yes | Equals manifest.projectId | — | URL p | Ownership check |
title | text | yes | max 180 chars; not blank | — | Event title | Timeline band label, period dialog |
start | date | yes | Not after end | — | All-day event start | Timeline band |
end | date | yes | Inclusive last day | — | All-day event end, the day after end | Timeline band |
color | color | yes | #rrggbb, stored in lowercase | — | URL m.color | Timeline band color |
vacation | boolean | no | No work days during the period | true | URL m.vacation | Work days in a work-days roadmap |
Work days
A roadmap whose schedule is work-days plans activities in work days: Monday through Friday, except the days of a period whose vacation is true. An activity's duration is the number of work days from start through end, at least 1. Editors keep start on a work day and end on the last work day of that duration, and move the dates when the days off change. Readers must not move stored dates. With calendar-days, every day counts and vacation has no effect.
Saved view
A saved view is a shared, named presentation. Every field is stored in the view's own event Notes; the event title is Tidigar View – <name>.
| Field | Type | Required | Rule | Default | Calendar | Used by |
|---|---|---|---|---|---|---|
id | UUID | yes | — | — | URL i | Identity, links, active saved view |
projectId | UUID | yes | Equals manifest.projectId | — | URL p | Ownership check |
name | text | yes | max 100 chars; not blank | — | View Notes | Saved-view menu |
view | enum | no | one of grid, timeline, list, flow | "grid" | View Notes | Main mode; grid is the Board |
row | axis | no | Dimension ID, "assignee" or "" | "" | View Notes | Board rows |
col | axis | no | Dimension ID, "assignee" or "" | "" | View Notes | Board columns |
color | dimension ID | no | Dimension ID or "" | "" | View Notes | Coloring in all views and exports |
presentationStyle | enum | no | one of "standard", "minimal", "airy", "contrast", "terminal" | "standard" | View Notes | Coordinated appearance in light and dark mode |
colorOverrides | object | no | Option ID → #rrggbb; at most 100 kept; unknown options dropped | {} | View Notes | Per-view option colors |
group | axis | no | Dimension ID, "assignee" or "" | "" | View Notes | Timeline grouping |
resolution | enum | no | one of year, quarter, month, week | "month" | View Notes | Legacy scale preference |
timelineScaleRows | enum | no | one of 1, 2, 3 | 3 | View Notes | Maximum timeline scale rows |
timelineShowWeeks | boolean | no | — | true | View Notes | Allow weeks in the adaptive timeline scale |
timelineShowQuarters | boolean | no | — | true | View Notes | Allow quarters in the adaptive timeline scale |
timelineWeekNumbers | boolean | no | — | true | View Notes | Week numbers; otherwise each week's first date |
timelineShowToday | boolean | no | — | false | View Notes | Today marker in the timeline and exported images |
timelineShowPeriodNames | boolean | no | — | false | View Notes | Period names in the timeline legend and exported images |
textMode | enum | no | one of hide, beside, run-past, fit, under | "beside" | View Notes | Timeline labels, exports |
filters | object | no | Dimension ID → option ID or ""; unknown entries dropped | {} | View Notes | Filtering in all views |
query | text | no | max 200 chars | "" | View Notes | Saved search text |
hideEmptyRows | boolean | no | — | true | View Notes | Board |
hideEmptyColumns | boolean | no | — | true | View Notes | Board |
hideEmptyTimelineGroups | boolean | no | — | true | View Notes | Timeline |
hideUnconnectedActivities | boolean | no | — | false | View Notes | Hide activities without incoming or outgoing dependencies in Flow |
flowGroupMilestones | boolean | no | — | false | View Notes | Group Flow milestones in a separate leading lane |
hideEmptyFlowGroups | boolean | no | — | false | View Notes | Hide Flow dimension lanes without visible activities |
flowHideDates | boolean | no | — | false | View Notes | Hide activity and milestone dates in Flow and image exports |
showAssignee | boolean | no | — | true | View Notes | Assignee faces and List column in Tidigar Business plans |
showRefs | boolean | no | — | false | View Notes | Reference keys before titles in Timeline, List and exports |
timelineActivityRows | enum | no | one of single, single-label, compact | single, or single-label when an older hideTimelineLabels: false is present | View Notes | Timeline row layout |
hideTimelineLabels | boolean | derived | true exactly when timelineActivityRows is single | Computed | View Notes | Fallback only; see removal candidates |
timelineMilestoneMode | enum | no | one of hide, marker, label, flag | "label" | View Notes | Timeline milestones |
timelineMilestonesBesideWork | boolean | no | — | false | View Notes | Timeline milestones in the row of their latest predecessor |
timelineSummarizeBy | dimension ID | no | A manifest dimension ID or "" | "" | View Notes | Timeline summary rows within groups |
timelineWeekDays | enum | no | one of work, all | "work" | View Notes | Timeline day scale |
timelineDependencyArrows | enum | no | one of none, grouped, every | "grouped" | View Notes | Timeline dependency arrows |
flowDirection | enum | no | one of auto, horizontal, vertical | "auto" | View Notes | Flow direction |
flowArrows | enum | no | one of curved, right-angle | "curved" | View Notes | Flow dependency arrow shape |
flowGroup | dimension ID | no | A manifest dimension ID or "" | "" | View Notes | Flow swimlanes |
flowShapes | enum | no | one of capsules, flowchart | "capsules" | View Notes | Flow activity and milestone shapes |
timelineStart | date | no | Not after timelineEnd | January 1 of manifest.year | View Notes | Timeline range |
timelineEnd | date | no | — | December 31 of manifest.year | View Notes | Timeline range |
listColumns | array of column keys | no | title, duration, start, end, dependsOn or a dimension ID; duplicates and unknown keys dropped | title, start, end, then every dimension ID | View Notes | Visible List columns |
listColumnOrder | array of column keys | no | Same keys as listColumns | Same as listColumns | View Notes | List column order, including hidden ones |
"assignee" in row, col or group arranges activities by the person each is assigned to, with one group per person and one for unassigned activities. showAssignee shows assignees on activities and as a List column. Both are meaningful only in a plan stored in Tidigar Business, where assignments are kept beside the plan; the project itself never holds assignments or people. Anywhere else an "assignee" axis arranges nothing, as "" does, showAssignee changes nothing, and both values are kept.
duration is the activity's length in days, or in work days with a work calendar. A view whose listColumnOrder does not contain duration shows it before the first of start and end, or after title without them.
dependsOn lists the activity's predecessors by name. A view whose listColumnOrder does not contain dependsOn shows it after the last of duration, start and end, or after title without them.
timelineScaleRows limits the adaptive time header to at most one, two or three rows. timelineShowWeeks and timelineShowQuarters allow the corresponding levels by default. Setting either to false excludes that level while the remaining levels still adapt to available width. Without weeks, three day-level rows use year/month/day. With timelineWeekNumbers set to false, each week is labelled with the date of its first day in the reader's own first weekday: the day alone below a month row, otherwise a short date. As space increases, a single row shows year, quarter, month, week, then day; two rows show year/quarter, year/month, month/week, then month/day; three rows show year/month/week, then month/week/day. Quarters, months, weeks and days become visible at average widths of 40, 34, 28 and 22 pixels respectively. Coarser views use fewer rows. resolution is retained for compatibility; it no longer fixes the displayed time scale. Year labels remain visible while scrolling horizontally. No row shows a single value: when a row would span the whole visible range with one cell, quarters replace a lone year if they divide the range, and otherwise the row is left out and its label moves into the first cell of the row below, so a view may show fewer rows than configured, but always at least one. Without a year row, the top row names the year in its first cell, and in the first cell of each new year, or in the next cell of that year with room for it; a top row of weeks or days also names the month. timelineShowToday shows the Today marker when enabled; it is hidden by default. timelineShowPeriodNames lists the periods in the visible range in the timeline's legend, each with its color and name; by default periods show only as colored bands.
timelineDependencyArrows chooses which dependency arrows a timeline draws: none draws no arrow at rest: an arrow appears only while one of its ends is pointed at, focused, dragged, selected or open for editing; grouped also draws the arrows between activities in the same timeline group, and treats every other arrow, including every arrow into a milestone, as none does; every draws every arrow. Without grouping all activities share one group. Printed and exported images draw only the arrows that are always visible.
hideEmptyFlowGroups defaults to false. When enabled, dimension lanes without visible activities are hidden in Flow, including lanes made empty by filters. Milestone lanes remain independent of this choice.
flowGroupMilestones defaults to false: Flow milestones are placed near their dependent activities without dimension membership. When true, milestones use a separate lane, leftmost in vertical Flow and topmost in horizontal Flow.
hideUnconnectedActivities hides activities with no incoming or outgoing dependencies in Flow. Connections to milestones or activities outside the view still count. It affects the view and its exported images.
flowDirection sets which way a flow view reads. horizontal runs each chain of dependencies from left to right and stacks separate chains below each other; vertical runs each chain from top to bottom and places separate chains side by side. auto reads vertically where the view is taller than it is wide, and otherwise horizontally. Exported and printed images read the way the view is shown.
flowArrows chooses curved dependency arrows or arrows with right-angle (90-degree) bends. flowGroup optionally groups activities into swimlanes in dimension option order. Unset values use a No value lane. Milestones sit near their dependency activities, or in a separate leading lane when flowGroupMilestones is enabled. Empty option lanes remain visible unless hideEmptyFlowGroups is enabled. Grouping does not assign or change values.
flowShapes chooses capsules for every node, or flowchart with diamonds for activities with more than one distinct incoming dependency, rectangles for other activities, and capsule milestones retaining their milestone marker. Classification includes dependencies hidden by view filters. These independent choices affect Flow, exported and printed images. Milestone capsules represent milestones, not process start/end or decisions.
textMode sets how an activity's title appears at its bar: hide omits it, beside places it outside the bar whenever the full title fits after its end or before its start. If neither side has room, a title that fits stays inside; longer titles use the side with more room and truncate. run-past lets text run past the bar, fit shows only what fits, and under shows one line below the bar. The former wrapped-below choice reads as under.
timelineMilestoneMode shows milestones as a diamond (marker), a diamond with its title (label), a small flag on a thin pole with its title beside it (flag), or not at all (hide). With timelineMilestonesBesideWork, a milestone that is not fixed and waits for shown activities sits in the row of the one that ends last instead of the milestone group; other milestones keep the milestone rows, whose heading is left out when grouping is disabled or when there are no separate milestones. Under timelineDependencyArrows: "grouped" such a milestone counts as part of its row's group.
timelineSummarizeBy adds a summary row for each of its dimension's values within every timeline group, holding the group's activities with that value; activities without a value follow the summaries. A summary spans from the earliest start to the latest end of its shown activities and is computed, not stored. Summarizing by the group dimension gives each group one summary. A view never records which summaries are collapsed: collapsing a summary is each reader's own choice, outside the project. A reader ignores a timelineCollapsedSummaries field that older data may still hold.
Some values were renamed when their default changed. A reader treats the older timelineMilestoneMode value show as label, every older timelineDependencyArrows value (all, groups, off and activities) as grouped, and every older textMode value (clip, below, wrap and overflow) as beside; a milestone diamond without its title is now marker.
Personal view state
The current mode, selection, search, unsaved view changes, scroll and zoom are personal and device-local. They are never part of the project, exports, backups, documents or calendars, and this specification defines no schema for it.
Storage profiles
All JSON is UTF-8. Object key order and insignificant whitespace have no meaning; array order is significant. A file extension helps users and tools but does not replace the format discriminator.
| Profile | Suggested extension | Root discriminator | Contents |
|---|---|---|---|
| Project exchange file | .json | format: "tidigar-project" | Exactly one canonical project in project |
| Multi-project browser backup | .json | format: "tidigar-backup" | Canonical projects in projects |
| Native document | .tidigar | format: "tidigar-document", version: 1 | One project exchange value in roadmap, plus document metadata |
| Calendar | — | Valid v3 HTTPS URL on every owned event | One event per project record |
Data a browser or app keeps for itself, such as browser storage or personal view state, is not an interchange format and is outside every project payload.
Project exchange file
The root object has exactly this semantic shape:
{
"format": "tidigar-project",
"project": {
"manifest": {},
"items": [],
"sharedViews": [],
"milestones": [],
"periodIndicators": []
}
}
project MUST satisfy every canonical-model rule below. Tidigar readers ignore unknown root keys, but interoperable writers SHOULD emit only format and project. The following is a complete minimal file, not pseudocode:
{
"format": "tidigar-project",
"project": {
"manifest": {
"projectId": "00000000-0000-4000-8000-000000000001",
"formatVersion": 1,
"name": "Example roadmap",
"description": "",
"dimensions": [],
"year": 2026,
"schedule": "calendar-days"
},
"items": [],
"sharedViews": [],
"milestones": [],
"periodIndicators": []
}
}
Multi-project backup
The root object is { "format": "tidigar-backup", "projects": [...] }. Every entry in projects is a canonical project object, without its own tidigar-project wrapper. Order is preserved. This profile is for whole-browser backup and restore; use a project exchange file when sharing one roadmap.
Native .tidigar document
A .tidigar file is JSON and can be processed without Apple frameworks. Its roadmap value is the complete project exchange object above, not only the inner canonical project.
| Field | Type | Required for writers | Rule |
|---|---|---|---|
format | text | yes | Exactly "tidigar-document" |
version | integer | yes | Exactly 1 |
id | UUID | yes | Document identity; independent of manifest.projectId |
title | text | yes | Library label; the canonical roadmap name remains roadmap.project.manifest.name |
createdAt | text | yes | ISO 8601 timestamp; legacy readers fall back to updatedAt |
updatedAt | text | yes | ISO 8601 timestamp |
roadmap | object | yes | A valid tidigar-project exchange value |
thumbnail | text | no | Standard padded Base64 bytes of an optional PNG preview in a light color scheme |
thumbnailDark | text | no | The same preview in a dark color scheme; only beside thumbnail |
Readers MUST use roadmap as the roadmap authority. title, createdAt, updatedAt, thumbnail and thumbnailDark are document-library metadata and MUST NOT overwrite canonical project fields. A reader showing a dark library uses thumbnailDark and falls back to thumbnail when it is missing. Tidigar currently accepts a missing title as "Roadmap", an invalid/missing timestamp as an old timestamp, and an invalid thumbnail as no thumbnail; interoperable writers MUST NOT depend on those recovery defaults.
To edit a file-backed roadmap: decode the applicable envelope, validate the complete canonical project, apply changes while retaining stable IDs and array order, validate again, then atomically replace the file. Writers SHOULD retain the native document id when updating an existing .tidigar file.
Calendar storage profile
A roadmap can also be stored as events in a calendar, one event per record. The calendar storage specification defines the events, their identity URLs and the rules for reading and writing them.
Versions
| Version | Where | Current | Changes when |
|---|---|---|---|
formatVersion | manifest | 1 | The canonical model changes incompatibly |
v | Calendar event URL | 3 | The URL or m layout changes |
version | Calendar Notes envelope | 2 | The Notes envelope or its payload keys change |
version | Native .tidigar document | 1 | The document envelope changes |