This document describes the syntax the current parser actually implements
(planscript/parser/parser.py). Anything not described here is not accepted
today. See Not yet supported and
Known sharp edges before reaching for a feature.
- PlanScript files use the
.plansuffix and are UTF-8 plaintext. - One project per file.
- The plan file is the single source of truth. It holds the project definition and, optionally, the tracking history after it.
- A nested line is indented with either a tab or exactly four spaces.
- Blank lines are ignored. Whole-line comments start with
;. - Unrecognized input fails with a
ParseErrorthat names the line.
project declaration required, exactly once, first content line
project attributes calendar / start / finish / metadata
task definitions each followed by its own indented lines
depends / budget / - metadata
tracking events optional, dated, after the project definition
invoices optional, dated, after the project definition
Attributes apply to the immediately preceding entry, so an indented line always attaches to the line above it.
project: Example
task 1 Design 5d
task 2 Build 10d
depends 1
General concept:
project: <project name>
Example:
project: DOTI LS6
- Client: DOTI
- Project Number: TBA
- Description: Decommission LS6 and Install 18" Sewer
- The project name is the remainder of the line and is not quoted.
- Exactly one project declaration is allowed, and it must appear before any other content. A second declaration, or content before it, is a parse error.
The following indented lines are recognized on the project itself:
| Line | Meaning |
|---|---|
start: YYYY-MM-DD |
Planned/target project start date. Soft; does not constrain CPM. |
finish: YYYY-MM-DD |
Planned/target project finish date. Soft; does not constrain CPM. |
calendar: <name> |
Calendar selection. Stored on the project, not yet applied by the scheduler. |
- <Key>: <Value> |
Project metadata. |
Rules:
calendar,start, andfinishmay each appear only once, and only in the indented project-attribute position. A duplicate is a parse error.startandfinishmust be exactYYYY-MM-DDdates.startmust not be later thanfinish(validation error).- Any calendar name is accepted today; the name is stored but not interpreted.
<tab>- <key>: <value> or - <key>: <value>
- User defined and generated; not required for CPM scheduling.
- Referenced programmatically as
project.metadata["Client"]. - A tab or four spaces is required before
-, a space is required after-, and a space is required after:. - Keys are free-form and are not predefined by the scheduler.
- Repeating a key silently overwrites the earlier value (last one wins).
General concept:
task <ID> <description> [duration]
Examples:
task 1 Project Management
task 1.1 Kickoff 0d
task 1.2 Project Plan 5d
- Description: Prepare report with intent to submit to CDPHE
task 4.2.1 30% 98d
Task IDs are hierarchical alphanumeric identifiers, separated by periods. Each segment is one or more letters or digits.
Examples:
1
1.1
1.2
1.2.1
1.1.a
1.1.a.2
A.1
Invalid examples:
1..2
.1
1.
1-2
DESIGN-01
A duplicate task ID is a parse error.
Task names are user-defined strings and are otherwise unconstrained. A name may
contain spaces, digits, and symbols such as %. The name ends before the final
duration token when one is present.
The optional last entry on the task line defines the length of the task.
Supported units on the task line: h = hours, d = days, w = weeks.
Examples:
✔ 8h, 5d, 2w, 2.5d, 1.5w
✘ 5 d, d4, 3days, -2d, +2d
Rules:
- The unit is required when a duration is present.
- A duration must be separated from the name by whitespace.
- Signed or negative durations are rejected with an explicit error.
- A trailing token that is not a valid duration is folded into the name rather than rejected; see Known sharp edges.
Default behavior:
- No duration ⇒ summary task. Its schedule is derived from its descendants.
0d⇒ milestone.- A summary task may not have a duration, and a task without children (a leaf) must have a duration. Both are validation errors.
<tab>- <key>: <value> or - <key>: <value>
Same rules as project metadata, but attached to the preceding task. Referenced
programmatically as task.metadata["Owner"].
General concept:
<tab>depends <predecessor_id> [relationship_type] [lag]
A depends line attaches to the task above it. The task above is the
successor; the referenced task is the predecessor.
task 1.1 Kickoff 0d
task 1.2 Project Plan 5d
depends 1.1
task 1.3 Review 2d
depends 1.2 SS +3d
Task IDs are used for references, so the example above reads "task 1.3 depends on 1.2". The referenced task must exist in the project.
Supported relationship types:
FS— Finish-to-StartSS— Start-to-StartFF— Finish-to-FinishSF— Start-to-Finish
Default behavior:
- If the type is omitted, the default is
FS. - The type must be separated from the predecessor ID by whitespace.
depends 1.1FSis rejected with an explicit error rather than parsed.
Lag follows the dependency type.
Supported units: h = hours, d = days, w = weeks.
Examples:
✔ FS, FS 0d, FS +2w, SS -1d, +2w, 3d
✘ FS+2w, FS 2 w, FS +2
Default behavior:
- An omitted lag is
0. - An omitted sign defaults to
+.
- The predecessor and successor must both exist.
- A task may not depend on itself.
- Summary tasks may not be a predecessor or a successor. Dependencies attach to leaf tasks only.
- An identical dependency (same pair, type, and lag) may not be repeated.
- Circular dependencies are rejected during validation.
Budget lines are indented under the task.
task 1.1 Kickoff 0d
budget $3400
task 2 Design
budget $250000
task 2.1 Preliminary Design
budget 10%
task 2.1.1 Site Layout 5d
budget 40%
<tab>budget $<amount>
- The amount is in dollars, as a plain number such as
$250000or$6250.25. Zero, one, or two decimal places are accepted and mean the same amount:$10is$10.00and$10.5is$10.50. Three or more decimal places is a parse error. - An explicit budget may not be negative.
<tab>budget <percent>%
- The percentage is a share of the nearest explicitly budgeted ancestor.
- Weights are
0–100. - A weighted task must have an explicitly budgeted ancestor.
- A task's budget is explicit or weighted, never both.
- Siblings must not mix explicit and weighted allocations.
- If siblings are explicit, their amounts must sum exactly to the parent's explicit budget.
- If siblings are weighted, every sibling must carry a weight and the weights
must sum to exactly
100%. - A task derived by weight may not have explicitly budgeted children.
Unallocated budgets are reported rather than guessed: a summary task with no budget of its own rolls up only the children that resolve.
Invoices record money already billed. An invoice line is not indented; its allocations are.
2026-09-30 invoice $45362.24
1.1 $2500
2.1.2 $2500.24
3.1 $40000
3.2 $362
- The invoice date is
YYYY-MM-DD. - Invoice and allocation amounts follow the same rule as budget amounts: zero,
one, or two decimal places (
$2500means$2500.00). - Each allocation references an existing task.
- Allocations must sum exactly to the invoice amount or validation fails.
- A task may be allocated at most once per invoice.
- An allocation line before any invoice is a parse error.
- An allocation may target a summary task; a charge made directly to a summary is additional to the charges of its descendants.
- An invoice dated before the project's
start:date fails validation. When the project has nostart:date there is nothing to compare against, so any invoice date is accepted. - An invoice dated after a report's as-of date is excluded from actual costs; it is not an error.
Tracking records what actually happened. Events are dated, task-level records placed after the project definition.
;Tracking
2026-08-01
1.1 start
1.1 complete
2026-08-03 1.2 start
2026-08-09 1.2 progress 60%
2026-08-12 1.2 complete
2026-08-15 2.1.2 start
2026-08-24 2.1.2 progress +20%
<date> <task_id> <directive> [argument]
- The date comes first because tracking is historical and chronological.
- A date-only line begins a group: the following indented lines inherit that date.
- A
;Trackingcomment before the events is a convention only. There is noTrackingsection keyword; events are recognized after the project definition wherever they appear.
| Directive | Argument | Meaning |
|---|---|---|
start |
none | Work actually began. Sets the actual start date and 0% progress. |
progress <n>% |
percentage | Absolute reported completion. |
progress +<n>% |
signed percentage | Incremental change (-<n>% reduces). |
complete |
none | The task finished. Sets the actual finish date and 100% progress. |
note <text> |
free text | A remark; no effect on derived state. |
Anything else, including progress 50 without %, is a parse error.
startmay occur only once.progressrequires a priorstart.progressmay not followcomplete.completemay occur only once and only afterstart.- Resulting progress must stay within
0%–100%. progress 100%does not complete a task; onlycompletedoes.- The referenced task must exist.
- Tracking is currently accepted on any task, including summary tasks.
Comments use ; and occupy a whole line:
; Preliminary design estimate
task 1.2 Preliminary Design 30d
Inline comments such as:
task 1.2 Design 30d ; preliminary estimate
are intentionally not part of the syntax.
- Human-readable plaintext is the source of truth.
- Syntax should favor readability over unnecessary punctuation.
- Invalid input should fail explicitly.
- Parser errors should identify the line whenever possible.
These exist in design notes but are not implemented in the parser, scheduler, or CLI today:
- Inline comments.
dependency <predecessor> > <successor> <type><lag>as a standalone entry line. Dependencies are written as indenteddependslines instead.- Constraints of any kind, and task-level dates.
- Task-level calendars, working hours, holidays, and calendar-aware
scheduling. A
calendar:name is parsed and stored but never applied. - Month (
m) durations.parse_durationunderstandsmas 30 days, but no syntax pattern reaches it, so5mon a task line is not a duration. - Baselines, revised plans, and plan version history.
- Multiple projects per file.
- Quoted or escaped strings.
- A literal
Trackingsection keyword.
These behave in ways that are surprising for hand-written plans. They are
tracked in ROADMAP.md.
- A token that is not a valid duration becomes part of the name.
task 1.2 Design 5mparses as a task namedDesign 5mwith no duration, and then fails validation because a task without children must have a duration. The same happens for typos such astask 1.2 Design 5 d. - Inline comments are absorbed into names.
task 1.2 Design 5d ; rushproduces a name ofDesign 5d ; rushand an undated task rather than a clear comment error. - A malformed budget is reported as a tracking error. An indented
<word> $<amount>line that is not a valid budget, such asbudget $10.555(three decimal places), falls through to the tracking-entry branch and fails withTracking entry has no date.instead of a budget-specific error. - Same-day event order is file order.
progresswritten beforestarton the same date is rejected rather than reordered by lifecycle precedence. - Future-dated tracking events are accepted. A tracking date later than today is not currently rejected.
- A date-only tracking line with no entries is accepted and ignored.