This page is the machine-readable specification of the Gantora plan format — a description written for AI assistants and other software that reads or writes .gantora files. The same text is served raw at https://gantora.net/skill.md and its format-only half at https://gantora.net/format.md.


Gantora plans

Gantora is a single-file HTML Gantt / project planner. It keeps its plans as plain text files in a DB/ folder — one file per plan, one row per task, cells separated by |, a header row first. There is no server, no lock and no database: the file on disk is the whole truth, and the app rewrites the whole file on every save. This document is the format as the app's own parser reads and writes it (checked against the parser on 2026-08-19), plus the rules that keep a plan safe when something other than the app writes it.

FileWhat it is
DB/<plan>.gantora (older plans: .csv)one plan — header row, task rows, then a few #-lines
DB/_lists.csvthe folder's shared registry of Teams / Systems / People — read it, never write it; the app maintains it
DB/_* anything elsethe app's own; leave alone

0. Read this part twice — it is where plans actually go wrong

These are not style notes. Every rule below is here because an assistant broke it on a real plan, on three separate attempts at the same one (measured 2026-08-20). None of the breakages looked like errors: the file parsed, opened, and was quietly wrong.

  1. Read this whole file before you write anything. If you cannot read it — the fetch failed, the content came back as binary, you are guessing from the name — stop and say so. Ask the person for the file — or, if they do not have one yet because you are about to make their first plan, open https://gantora.net/spec. If neither works, say what you could not read and ask them to paste the spec; do not reconstruct the format from a sample — the parts that matter are the ones a sample does not show. (An assistant wrote this rule for itself, in those words, after failing the other way.)
  1. 🔴 Milestone=yes is a project-level, standalone, zero-day event. It is NOT a way to mark a task as important, and a milestone is never a member of a group. This is the single most common mistake, and the most damaging: a milestone inside a group breaks the group's own dates and the health of everything above it. If you want to say "this one matters", say it in Notes.
  1. Never write dates on a group row. A group row — one that other rows name in their Parent cells — is a container, and the app computes its Start, End and Duration from its members every time it loads. Dates you type there are overwritten — and until they are, they disagree with the plan and mislead whoever reads it.
  1. Status is exactly one of six words: Planned, In Progress, On Hold, Blocked, Done, Cancelled. Not a sentence, not a percentage, not a description. Anything the person wants to say about a task belongs in Notes. Cancelled means it will not happen — never use it for "finished", and never delete a row to mean it.
  1. The 28 columns keep their order, and every row has all of them. Do not skip a column because it is empty — write the empty cell. A single missing | shifts every value after it into the wrong field, and the file still opens. And no trailing | after the last cell — 28 cells is 27 pipes; a pipe at the row's end makes a 29th cell, and the app refuses the file as truncated rather than guess which cell is the ghost.
  1. 🔴 If you are unsure about grouping, leave it out. A flat list of tasks with correct dependencies is a valid, good, useful plan. A wrong Parent cell — one naming a UID that is not in the file, or a chain nested past four levels — is worse than no grouping at all, and the person can group rows in the app in seconds. When in doubt: no groups.
  1. Parent is the child's cell, and it holds a UID. Membership is written exactly where every assistant expects it: on the member, naming its parent — by the parent's UID, never its Seq (positions renumber; identity does not). The parent row's own Parent cell stays empty unless it too sits inside a larger group.

`` ✓ RIGHT — each child names its parent, by UID 5|Packing|…|2026-03-02|…|2026-03-06|…||no|…|uid-of-nobody-needed 6|Boxes|…|2026-03-02|…|2026-03-03|…|pk4x-…-packing|no|… 7|Labelling|…|2026-03-04|…|2026-03-05|…|pk4x-…-packing|no|… ``

(In an old file — format 1, no #META|format line — membership sits the other way round: a Group column on the parent, listing its members' IDs. Read it if you meet it; the app upgrades such files on their next save, and everything you write is format 2.)

Two more that cost real time on the same test:

1. The file, byte by byte

Seq|Activity Name|Duration|Start|Hour (Start)|End|Hour (End)|Predecessors|Team|System|Accountable|Responsible|Parent|Milestone|Notes|Status|History|Event|Emoji|Not Finalized|Baseline End|Date History|UID|Created|Modified|History By|Reported|Labels

The reader maps columns by header name, not by position. A name it does not know is carried through open and save untouched — but carries no meaning to any reader. Do not invent columns.

2. The columns

ColumnFormatNotes
Seqpositive integerA position, not a name — which is why it is no longer called "ID" (the app shows it as #). Rows are kept in time order and numbered 1, 2, 3… down the file; a group takes the number directly above its members. The app renumbers on open, so a new row with max+1 is fine — what matters is that Predecessors reference Seq values in this same file. For identity, use UID.
Activity Nametext
Durationcalendar days, both ends counted (End − Start + 1); 0 on a milestone and on an event (even a period)Recalculated by the app on every save. Write the arithmetic; never trust it from a hand-edited file.
Start, EndYYYY-MM-DDEnd ≥ Start. A group's dates — and its hours — are derived: its members' first start (with that member's start hour) and last end (with that end hour); the app recalculates them on every save.
Hour (Start), Hour (End)HH:MM or emptyempty = any time of that day
PredecessorsIDs, ;-separatedmust exist in this file; a task may not precede itself; a task may not start before a predecessor ends
Team, Systemtextfree text; the registry offers suggestions, nothing enforces them
Accountableone Name or Name:hourshours a day, 0 < h ≤ 24, optional; a name without a number costs nothing
ResponsibleName[:hours] entries, ;-separatedsame rule
Parentthe parent row's UIDon the CHILD row — a row somebody names as their parent is a group. Hierarchy is by reference, not indentation, and by identity, not position: the cell survives renumbering untouched. A task belongs to at most one group; groups nest at most 4 levels (group → sub → sub² → sub³ → task). A milestone can neither be nor hold a member.
Milestoneyes / noa point in time: End = Start, no team/system/people/group
Notestext, max 500 charsline breaks escaped as ~n
Statusexactly one of Planned · In Progress · On Hold · Blocked · Done · Cancelledempty on an event. Not derived from dates — a task past its End is not Done until somebody says so. Cancelled is work that will not happen: it counts towards nothing, it can never be late, and the row is kept rather than deleted so the plan still records that somebody meant to do it. Never use it to mean "finished".
HistoryYYYY-MM-DD Status;…, oldest firstappend-only. Every status change is one entry dated the day it happened; the first entry is the day the row entered the plan. Two entries on one day are allowed. Setting the status a row already has is not a change and writes nothing. Empty on an event.
Eventyes / noan event is a milestone flavour the plan does not own (a holiday, a freeze): Milestone is yes on it too, it may have End > Start (then it is a period), it carries no status and no history
Emojione emoji, events onlyempty otherwise
Not Finalizedyes / no"these dates are still a guess". Never yes on a milestone or an event.
Baseline EndYYYY-MM-DD or emptyonly when typed by hand to overrule the recorded baseline; empty means "work it out from Date History"
Date HistoryYYYY-MM-DD YYYY-MM-DD;…the day it was written, then the End date that dayWritten by the app, never by you. It opens the first time a task's End moves after the task has left Planned (first entry = the End it had, and that entry is never rewritten); then one entry per day, last value wins. Empty on an event.
UID[A-Za-z0-9_-]{4,40}the row's permanent name — minted once, never changed, shown nowhere, what a merge matches rows by. See §4 for minting one.
CreatedYYYY-MM-DDTHH:MM:SSZ <writer-id> <name>UTC to the second, then a short writer id, then a display name (optional). Written once.
Modifiedsame shapethe last change that actually changed something. A renumber is not a change.
History ByYYYY-MM-DD <name>;…one entry per History entry, same order, same dates; - for an unknown name. If the dates fall out of step with History, the app drops all the names and keeps the history.
Reportedyes / nowhether the management report speaks about this row. A main task (top level, not in any group, not a milestone) is always yes — the app forces it.
Labelswords, ;-separatedfree tags; two labels differing only in case are one label

Reading the 'yes/no' columns: the parser accepts anything starting with y, i, 1 or t as yes. Always write yes / no.

3. The #-lines at the end of the file

After the last task row, in this order. Preserve every one of them verbatim when you rewrite a file — they are somebody's settings and somebody's deletions — except the single writer line, which you replace with your own.

#VIEW|hiddenColumns|duration;team
#VIEW|shownColumns|history
#VIEW|columnOrder|id;name;status;start;end;…
#VIEW|collapsed|3;7
#VIEW|savedView|<name>|view=zoom;level=day;px=24;at=2026-04-15
#DEL|<uid>|<stamp>|<name>
#META|format|2
#META|readOnly|yes
#META|writer|<writer-id>
#META|spec|https://gantora.net/format.md

The text carries a tiny inline markup, and nothing outside it — no HTML, ever, because a plan is a file that travels and its summary is drawn in somebody else's app:

| | | |---|---| | bold | bold | | _italic_ | italic | | [red]…[/red] | and [blue], [orange], [green] | | anything else | text, including a stray bracket |

Emoji need no markup — they are characters. Escape the cell like any other (~t ~p ~n): a summary sentence normally contains a pipe or a line break. Keep it under 2000 characters; longer is cut.

4. Minting identity: UID and stamps

Pick yourself a writer id: 3–8 lowercase letters/digits. Use clde unless the user gives you one. Then:

5. The rules, in order of importance

  1. Never lose what you don't understand. Round-trip every cell and every #-line you are not deliberately changing, byte for byte. Unknown columns are someone's data; unknown #-lines are someone's settings.
  2. Respect #META|readOnly|yes — read freely, write never; offer a copy.
  3. Read fresh, then write, in one breath. Never write over a plan you did not read immediately before: the user may have edited it in the app in the meantime, and these folders are often synced (Dropbox, OneDrive), so somebody else's save may have landed. Re-read, compare with what you read earlier, and if it changed, re-apply your edit to the new content — or stop and say so. Write the whole file in one operation; where the channel lets you, write a temp file in the same folder and rename it over the original. Never leave a half-written plan.
  4. Prefer adding over editing. Safe: a new task row; a status move (Status + History append + History By append + Modified). Leave to the app: deleting rows, changing UIDs, restructuring groups, rewriting dates that are already being worked to (that is what Date History tracks), editing #VIEW lines.
  5. Keep the file's invariants — exactly six status words; yes/no; dates as YYYY-MM-DD; references to IDs that exist; one group per task; nothing on a milestone but a name, a date and a status; Reported = yes on every main task; a group's dates are its members' first start and last end; rows in time order (by Start, then by Start hour; a group directly above its members; events last) and numbered 1, 2, 3… in that order — the app renumbers on open anyway, but a file already in order is one it rewrites nothing in; the header row first; the final line break.
  6. Stamp your writes: Modified on every row you touched, Created on every row you made, #META|writer|claude-skill and #META|spec|https://gantora.net/format.md at the end.
  7. Never refuse a newer file. A column, a #-line or a writer you do not recognise is not an error — read what you can, say what you skipped.

5b. Who owns the plan — two patterns, and the file says which

5c. How you reach the file — the channels

The rules above are the same on every channel; what differs is the mechanics.

ChannelReadWriteMind
The user attaches a .gantora to the chatthe attachmenthand back a complete file to download — never a fragment, never "paste this row"say what you changed, row by row
A Dropbox (or similar) connector — the user's DB folder in their Dropboxread the file through the connector, immediately before you writeone call that writes the whole file; no temp file is possible, so the fresh-read rule carries the weightthe app's own Dropbox mode writes the same folder — never assume the file is as you left it
A local folder the user has opened for youread ittemp file + renameDB/_lists.csv and anything _* are the app's
The Gantora MCP serverits toolsits tools — they enforce these rules in codeprefer it when it is there
A link…/gantora.html#plan=…hand the user one link; the app opens it read-only, as a shared copy, with no file and no folderfor showing a plan, not for keeping one: nothing comes back

Making a plan link. The whole plan travels after the #, so it never reaches a server. Take the file's text (UTF-8, exactly as you would write it), deflate-raw it, base64url-encode the bytes (A–Z a–z 0–9 - _, no padding), and put a ~ in front: #plan=~<that>. Add &file=<name> for the plan's name (the extension is added if missing), and &view=gantt / &lang=hu if you want a view or a language. Without the ~ the value is read as plain UTF-8 in base64url — a shape a person can make by hand. A fifty-row plan is about 4–8 KB of link; most chat clients carry that, a few truncate very long messages — if in doubt, send the file as well. The base address is whatever Gantora page the user opens: the copy on gantora.net once it is published, or their own gantora.html.

5d. Before you write a plan from scratch — the planner's interview

A good plan is decided before the file: ask, briefly, then write.

  1. What is being done, and in what breakdown — by room or by trade? by phase or by deliverable? That choice is the group structure, so it is worth one question.
  2. Who does whatTeam / Responsible, even roughly ("me", "the electrician", "the venue").
  3. What has to wait for what — the real Predecessors, not every row chained to the one above it.
  4. The waiting times, as rows of their own — drying, curing, delivery lead time, approvals. They are what turn "two weeks of work" into eight weeks on the chart, and a plan without them is a plan that will be late.
  5. Dates you are unsure of: write them and tick Not Finalized = yes — the app draws those pale and dashed, which is exactly what they are.

Then write it in time order (rule 5), stamp it (rule 6), and tell the user what the file is called and where it is.

6. Reading a plan well

7. Typical asks

7a. Bringing a plan in from another tool

First, do not do it by hand if the app can do it

The app reads four foreign formats itself, through its Import… menu item:

XML Spreadsheet 2003.xml — what Excel writes on "Save as… XML Spreadsheet"
HTML table.html, .htm, and the .xls Excel writes as "Web Page"
Microsoft Project XML (MSPDI).xml — MS Project's own "Save as… XML"
Delimited text.csv, .tsv, .txt — comma, semicolon or tab, sniffed from the header line

If the user has one of those and the app in front of them, say so and stop. The app's importer shows a preview at the depth the rows will land at, repairs what it can, and names every column it could not place — none of which a conversion in a chat window can offer. Converting by hand is the worse road when the better one exists.

Two things it deliberately will not do: a real .xlsx (a zip of parts — the app bundles no library and fetches nothing), and a pipe-delimited file, which is one of its own plans and belongs in the DB menu instead.

When conversion IS your job

A format the app cannot read (.xlsx, a Jira/Asana JSON export, a screenshot, a description in prose), or a plan the user wants written straight into their cloud folder without opening the app. Then you are writing a .gantora from §1–§5, and this is what the source usually gives you:

The source's columnGoes toWatch for
Task / Summary / Activity / Name / TitleActivity Name
Start / Start Date / BeginStartreformat to YYYY-MM-DD; a time goes to Hour (Start), not into the date
Finish / End / Due date / DeadlineEndsame
Duration / Effortnothing🔴 Duration is derived from Start and End by the app. Never carry a source duration into it — if the source has only a start and a duration, compute the End and write that
Predecessors / Depends on / Blocked byPredecessorssee below
Resource Names / Assignee / OwnerTeam or Responsiblea person → Responsible; a team, department or role → Team. If unsure, Responsible and say so
Outline level / indent / Parent taskParentsee below
% CompleteStatus100Done, 1–99In Progress, 0 → leave empty, not Planned. "Not started" and "planned" are different claims and only one of them was made
Status / StateStatusmust become one of the six exactly (§2). A word you cannot map is reported, never guessed — a plan silently reset to Planned on the way in is worse than one that arrives with no status
Notes / Description / CommentNotesescape it: ~~t, `\~p, a line break → ~n`
Labels / Tags / CategoryLabels
the source's own IDnothing — see below

The four that go wrong

  1. Hierarchy is carried by the CHILD. A source that indents (an outline level, a "Parent task" column, a WBS code) becomes Parent cells: each child row holds its parent's UID. The parent row is not touched, and there is no list of members anywhere. Convert the outline into UIDs after you have minted every row's UID, not while writing the rows.
  2. Predecessors are the file's own Seq numbers, not the source's IDs and not UIDs. So: mint the plan first, then translate each source dependency through a source-ID → new-Seq map you built as you went. A dependency you cannot resolve is dropped and reported — never pointed at a guess.
  3. Do not carry the source's ID into UID. UIDs are minted here (§4) and must be unique in this file forever; a foreign ID collides the day two exports are merged. If the source ID is worth keeping, it goes in Notes.
  4. Do not carry the source's timestamps into Created / Modified / History. Those record what happened to this file. Everything you write is created now, by you, with your stamp — that is what makes the plan's own history readable later.

Finish with a report, not a file

Say what came across and what did not, in this order: how many rows, how many groups, how many milestones; every column you could not place; every status word you could not map; every dropped dependency. A conversion that reports nothing is not a clean conversion — it is an unexamined one, and the person has no way to tell those two apart.

8. A minimal example

Seq|Activity Name|Duration|Start|Hour (Start)|End|Hour (End)|Predecessors|Team|System|Accountable|Responsible|Parent|Milestone|Notes|Status|History|Event|Emoji|Not Finalized|Baseline End|Date History|UID|Created|Modified|History By|Reported|Labels
1|Design|10|2026-09-01|09:00|2026-09-10||||||||no||In Progress|2026-08-19 Planned;2026-09-01 In Progress|no||no|||clde-mfa3k9x2-0-7hq4|2026-08-19T14:02:11Z clde Claude|2026-09-01T08:10:00Z ak7 Anna Kovacs|2026-08-19 Claude;2026-09-01 Anna Kovacs|yes|
2|Workshop|3|2026-09-01|09:00|2026-09-03|16:00||PMO|ERP|Anna Kovacs:4|Bela Nagy:6|clde-mfa3k9x2-0-7hq4|no|Room booked~nAgenda: scope~p risks|Done|2026-08-19 Planned;2026-09-03 Done|no||no|||clde-mfa3k9x2-1-q2m8|2026-08-19T14:02:11Z clde Claude|2026-09-03T15:30:00Z ak7 Anna Kovacs|2026-08-19 Claude;2026-09-03 Anna Kovacs|no|wave-1
3|Write the spec|7|2026-09-04||2026-09-10||2|PMO|ERP|Anna Kovacs||clde-mfa3k9x2-0-7hq4|no||Planned|2026-08-19 Planned|no||yes|||clde-mfa3k9x2-2-z0p1|2026-08-19T14:02:11Z clde Claude|2026-08-19T14:02:11Z clde Claude|2026-08-19 Claude|no|wave-1;risk
4|Spec approved|0|2026-09-11||2026-09-11||3||||||yes||Planned|2026-08-19 Planned|no||no|||clde-mfa3k9x2-3-8hd3|2026-08-19T14:02:11Z clde Claude|2026-08-19T14:02:11Z clde Claude|2026-08-19 Claude|yes|
5|Christmas freeze|0|2026-12-22||2026-12-29||||||||yes||||yes|🎄|no|||clde-mfa3k9x2-4-k4n7|2026-08-19T14:02:11Z clde Claude|2026-08-19T14:02:11Z clde Claude||yes|
#META|format|2
#META|writer|claude-skill
#META|spec|https://gantora.net/format.md

Row 1 is a group (rows 2 and 3 carry its UID in their Parent cells); its dates, its 09:00 start hour and its status are derived from them. Row 2 shows a note with an escaped line break and pipe, people with hours, and a Done row. Row 4 is a milestone; row 5 an event — a period with an emoji, no status, no history, and Duration 0 like every milestone-flavoured row.

---

This format description may be used freely to read and write Gantora plans from any tool. The Gantora application itself is separately licensed.