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 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.
| File | What it is |
|---|---|
DB/<plan>.gantora (older plans: .csv) | one plan — header row, task rows, then a few #-lines |
DB/_lists.csv | the folder's shared registry of Teams / Systems / People — read it, never write it; the app maintains it |
DB/_* anything else | the app's own; leave alone |
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.
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.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.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.| 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.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.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:
Notes, or in a #META line if it is about the whole plan.Predecessors before you renumber anything. They point at IDs, and IDs are positions in time order — see §5. Renumbering without fixing them turns a schedule into a shuffle.\r\n line ends and always ends the file with a line break; the reader accepts \n too. Write it the way the app does..trash. That is the app's wastebasket: a plan somebody deleted, kept under a name the plan list does not recognise so it can be brought back. Do not read it, do not write it, do not offer to tidy it away. If somebody asks for a deleted plan, tell them the Wastebasket is in the app's DB menu.| (format 2, 2026-08-23; the older names ID and Group still read, but everything you write uses these):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.
#VIEW / #DEL / #META lines below work.| and line breaks cannot appear as themselves. They are escaped — ~p is |, ~n is a line break, ~t is a literal ~ — and the reader decodes every cell. Encode ~ first on the way out, or you encode your own output. (A note typed on three lines is stored as line one~nline two~nline three.) Do not replace a pipe with / — that was the app's behaviour until August 2026 and is now simply data loss.| Column | Format | Notes |
|---|---|---|
Seq | positive integer | A 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 Name | text | |
Duration | calendar 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, End | YYYY-MM-DD | End ≥ 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 empty | empty = any time of that day |
Predecessors | IDs, ;-separated | must exist in this file; a task may not precede itself; a task may not start before a predecessor ends |
Team, System | text | free text; the registry offers suggestions, nothing enforces them |
Accountable | one Name or Name:hours | hours a day, 0 < h ≤ 24, optional; a name without a number costs nothing |
Responsible | Name[:hours] entries, ;-separated | same rule |
Parent | the parent row's UID | on 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. |
Milestone | yes / no | a point in time: End = Start, no team/system/people/group |
Notes | text, max 500 chars | line breaks escaped as ~n |
Status | exactly one of Planned · In Progress · On Hold · Blocked · Done · Cancelled | empty 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". |
History | YYYY-MM-DD Status;…, oldest first | append-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. |
Event | yes / no | an 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 |
Emoji | one emoji, events only | empty otherwise |
Not Finalized | yes / no | "these dates are still a guess". Never yes on a milestone or an event. |
Baseline End | YYYY-MM-DD or empty | only when typed by hand to overrule the recorded baseline; empty means "work it out from Date History" |
Date History | YYYY-MM-DD YYYY-MM-DD;… — the day it was written, then the End date that day | Written 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. |
Created | YYYY-MM-DDTHH:MM:SSZ <writer-id> <name> | UTC to the second, then a short writer id, then a display name (optional). Written once. |
Modified | same shape | the last change that actually changed something. A renumber is not a change. |
History By | YYYY-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. |
Reported | yes / no | whether 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. |
Labels | words, ;-separated | free 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.
#-lines at the end of the fileAfter 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
#VIEW — how the plan is looked at (folded columns, column order, folded groups, saved views). ~s escapes a ; inside a saved view's values. Never generate these; carry them through.#DEL — a tombstone: this uid was deleted, when (a Created-style stamp) and what it was called. It is what lets a merge tell deleted here from added there. Therefore: do not delete rows. A row that simply goes missing comes back in the next merge with somebody's copy, old uid and all. If the user insists, either point them at the app, or remove the row and append #DEL|<uid>|<your stamp>|<name>.#META|format|2 — the format's version stamp (2026-08-23). Write it on every file. A file without it is format 1 — older column names, membership the other way round (see rule 7) — which the app reads forever and upgrades on its next save. The number moves only on a breaking change: a new column or a new #META key is NOT one.#META|readOnly|yes — the plan declares itself read-only (2026-08-19). It is accident-prevention, not protection — but it is the file's own statement, and the app honours it: when it is present, do not write the file. Offer to write an editable copy under another name, without this line. Absence of the line means writable; never write readOnly|no. And if you set this lock while working on a file, LIFT it when you deliver — the lock belongs to the writer, and a lock left behind closes the plan to the person it was written for.#META|writer|<id> — which program saved the file last. The app writes gantora-app; you write claude-skill. It is a fact for a support conversation, never a gate: never refuse a file because of what this line says, and never refuse one with a #-line you do not recognise.#META|spec|https://gantora.net/format.md — where this very format is written up. Write it on every save: it is the file carrying its own manual, so a .gantora dropped into any assistant with zero context can explain itself. Never a gate, never fetched by the app.#META|summary|<text> — the plan's written executive summary (2026-08-25). Optional: with no such line the report works its own summary out from the state of the plan, and that is the normal case. When the line is present it replaces the automatic one rather than being added to it, so write it only when the user has actually said what the summary should be.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.
Pick yourself a writer id: 3–8 lowercase letters/digits. Use clde unless the user gives you one. Then:
<writer-id>-<ms since epoch in base36>-<counter in base36>-<4 random base36 chars>, e.g. clde-mfa3k9x2-0-7hq4. Check it is not already in the file. Never change an existing UID — not on a rename, not on a move, not on a copy; a duplicate row is a new row and gets a new uid.Created, Modified, #DEL): 2026-08-19T14:02:11Z clde Claude — UTC ISO-8601 to the second, no milliseconds, then the writer id, then a name. Name may be the user's name if they gave one.#-line you are not deliberately changing, byte for byte. Unknown columns are someone's data; unknown #-lines are someone's settings.#META|readOnly|yes — read freely, write never; offer a copy.#VIEW lines.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.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.#-line or a writer you do not recognise is not an error — read what you can, say what you skipped.readOnly line.#META|readOnly|yes (so a stray tap in the app cannot write over your next update — the app offers Save As for an editable copy), and the file name carries an __ai suffix before the extension: Kitchen renovation__ai.gantora. The suffix is a beacon for people and programs; the truth is the flag and the writer stamp. And keep it current as you go — a status board that lags the conversation is one the person stops trusting, so the plan must never say less than what you have already been told. What "as you go" means depends on the channel (§5c): where you can write — connector, local folder, MCP — every new task and every status change goes into the file the moment it comes up, not batched for later; on a channel with no way back (an attachment, a link) hand over the updated file as soon as the conversation has changed the plan, without waiting to be asked.The rules above are the same on every channel; what differs is the mechanics.
| Channel | Read | Write | Mind |
|---|---|---|---|
The user attaches a .gantora to the chat | the attachment | hand 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 Dropbox | read the file through the connector, immediately before you write | one call that writes the whole file; no temp file is possible, so the fresh-read rule carries the weight | the 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 you | read it | temp file + rename | DB/_lists.csv and anything _* are the app's |
| The Gantora MCP server | its tools | its tools — they enforce these rules in code | prefer 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 folder | for 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.
A good plan is decided before the file: ask, briefly, then write.
Team / Responsible, even roughly ("me", "the electrician", "the venue").Predecessors, not every row chained to the one above it.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.
End < today and Status not Done/Blocked/On Hold), late starts (Start < today, still Planned), and what falls in the next week.Baseline End if set, else the End of the first Date History entry, else the current End. Drift = End − baseline, in days; positive is late. A row still Not Finalized has no drift to report.Blocked > On Hold > In Progress > Planned > Done — and one member begun makes the group In Progress.History / History By pair tells you who moved what and when; the Modified stamp tells you who touched the row last.Duration computed), Status = Planned, History = <today> Planned, History By = <today> <name or ->, fresh UID, Created = Modified = your stamp, Reported = yes if top level else no, Not Finalized = no. To put it in a group, write the parent row's UID into the new row's Parent cell — nothing else says membership, and the parent row is not touched at all. Then update #META|writer.Status, append to History (<today> <Status>) and to History By (<today> <name>), set Modified. Touch nothing else on the row. If the row already has that status, do nothing and say so.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.
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 column | Goes to | Watch for | |
|---|---|---|---|
| Task / Summary / Activity / Name / Title | Activity Name | ||
| Start / Start Date / Begin | Start | reformat to YYYY-MM-DD; a time goes to Hour (Start), not into the date | |
| Finish / End / Due date / Deadline | End | same | |
| Duration / Effort | nothing | 🔴 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 by | Predecessors | see below | |
| Resource Names / Assignee / Owner | Team or Responsible | a person → Responsible; a team, department or role → Team. If unsure, Responsible and say so | |
| Outline level / indent / Parent task | Parent | see below | |
| % Complete | Status | 100 → Done, 1–99 → In Progress, 0 → leave empty, not Planned. "Not started" and "planned" are different claims and only one of them was made | |
| Status / State | Status | must 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 / Comment | Notes | escape it: ~ → ~t, `\ | → ~p, a line break → ~n` |
| Labels / Tags / Category | Labels | ||
| the source's own ID | nothing — see below |
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.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.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.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.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.
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.