Timeline Planning Skill
Generate Mermaid Gantt charts that visualize project timelines, task scheduling, phases, and milestones based on GitHub issues and their estimates.
When to Use
Activate this skill when:
- User asks "what would the timeline look like?"
- User says "create a Gantt chart", "show me the project schedule"
- After running the
issue-decompositionskill, to visualize scheduling of the resulting issues - During sprint planning to allocate work
Mermaid Gantt Syntax
Basic Structure
gantt
title Project Timeline
dateFormat YYYY-MM-DD
section Phase 1
Task A :a, 2025-01-15, 2d
Task B :b, after a, 3d
section Phase 2
Task C :c, after b, 2d
Key Elements
title- Chart titledateFormat- How dates are parsedsection- Groups related tasks- Task format:
Name :id, start, duration
CRITICAL: Never put a colon in a task name
Mermaid splits each task row at the first colon. Any colon inside the task name silently
eats the rest of the row — no parse error, no warning, just a wrong label on a chart that still
renders. This bites constantly because real issue titles are full of colons
(skill: timeline-planning — update, fix: handle empty input).
WRONG — renders as a task literally labelled #49 skill:
#49 skill: timeline-planning — update :i49, 2026-09-15, 3d
CORRECT — replace each colon with -:
#49 skill - timeline-planning — update :i49, 2026-09-15, 3d
Sanitize every title before emitting it: replace : with -, then collapse runs of whitespace.
Parentheses and commas are safe inside a Gantt task name — unlike flowchart node labels, where
dependency-mapping and mermaid-diagrams correctly forbid parentheses. Do not strip them here;
only the colon matters. (A comma after the name is a field separator and still significant.)
Task IDs
Derive the id from the issue number — i<number> — so every bar traces back to its issue and the
same issue can never appear twice under two ids. Prefix the visible name with #<number> too.
Estimate to Duration Conversion
Where estimates actually live
Look in this order and stop at the first hit:
- A
points-<n>label (points-4,points-8), written byestimating-work-items. The number after the dash is the estimate; look it up in the points table below. - A size/effort label. Lowercase it, strip a leading
size/effort/estimate/t-shirttoken and the one separator that follows it, and look the remainder up in the table below:effort/large→large,size/M→m,size:medium→medium,effort/x-large→x-large. Do not split on every-, orx-largecollapses tolargeand an XL task gets sized as an L. - The issue body.
issue-decompositionandestimating-work-itemswrite the estimate into the body under an## Estimateheading. The first line of that section is**<n> points**; older issues may carry a T-shirt word there instead. Read whichever it is. - A numeric points field in the body (
Story points: 5) or on the repo's GitHub Project, if the project uses one: look it up in the points table below, and if an issue's number is not on the powers-of-two scale, round it up to the next bucket and say so. Never round an epic's points: they are the exact sum of its children. - Nothing found → fall through to the No Estimates edge case below.
Never assume a repo uses the words "Small/Medium/Large/XL" verbatim. Run gh label list first to
see the vocabulary in play; most repos use something else.
Points table
Points are relative size, not time, so this mapping is a planning default, not a fact. Use it, state it in the mapping report, and change it if the user gives a different velocity.
| Points | Duration | Notes | |---|---|---| | 1 | 1d | | | 2 | 2d | | | 4 | 4d | | | 8 | 8d | Largest single issue | | Epic (any sum) | Sum of children | An epic's points are the sum of its children, with no upper limit. Draw the children and the epic as a milestone |
T-shirt table (legacy)
For repos that still size with words:
| Estimate (normalized) | Also written as | Duration | Notes |
|---|---|---|---|
| Small | s, xs, tiny, small | 1d | Single-day task |
| Medium | m, medium | 3d | Few days of work |
| Large | l, large | 5d | Almost a week |
| XL | xl, xxl, x-large, huge | 8d | Full week+ |
Adjust either table for team velocity, complexity indicators, and buffer for unknowns, and say what you adjusted.
Report the mapping you used — issue, estimate, source, duration — so the user can correct a bad label read before you draw anything.
Task States
| Modifier | Meaning | Example |
|----------|---------|---------|
| done | Completed | Task A :done, a, 2025-01-15, 2d |
| active | In progress | Task B :active, b, after a, 3d |
| crit | Critical path | Task C :crit, c, after b, 5d |
| milestone | Milestone | MVP :milestone, m, after c, 0d |
Timeline Patterns
Reusable Gantt patterns are in gantt-patterns.md: linear project, parallel workstreams, milestones, critical path, and one built from real GitHub issues. Pick the pattern that matches the dependency shape you found in Step 4 and adapt it — don't start from scratch.
Timeline Generation Process
Step 1: Gather Issues
Prerequisite: the gh CLI. Fetch everything the later steps consume in one call:
gh issue list --state open --limit 100 \
--json number,title,labels,body,milestone,blockedBy,blocking,parent
The three relationship fields matter: labels and body alone cannot see GitHub's native issue
dependencies or sub-issues, so a fetch without them silently reports "no dependencies" on a repo
that has them. milestone is what Step "After Generating Timeline" needs to flag a missed deadline.
Field shapes to expect:
| Field | Shape | Read |
|---|---|---|
| blockedBy / blocking | {"nodes": [...], "totalCount": N} | .blockedBy.nodes[].number |
| parent | object or null | .parent.number |
| milestone | object or null | .milestone.dueOn |
Note the nodes/totalCount wrapper — indexing blockedBy directly as a list fails.
If gh is unavailable, ask the user to paste the issue list instead.
Step 2: Determine Start Date
- Use today's date as default start
- Or ask user for preferred start date
- Consider sprint boundaries
Step 3: Convert Estimates
Apply the rules in Estimate to Duration Conversion above, then show the table you derived:
#100 points-4 → 4d (label)
#101 effort/small → 1d (label, legacy T-shirt)
#102 ## Estimate: **8 points** → 8d (body)
#103 none → 4d (default, flagged)
Step 4: Respect Dependencies
Read dependencies from these sources, in order of authority:
-
blockedBy/blocking— GitHub's native dependencies. Authoritative when present. -
Body text keywords — same table
dependency-mappinguses, so the two skills agree:| Pattern | Meaning | |---|---| | "Blocked by #X" | This issue depends on #X | | "Depends on #X" | This issue depends on #X | | "After #X" | Do this after #X | | "Blocks #X" | This issue must complete before #X | | "Required for #X" | This issue must complete before #X | | "Prerequisite: #X" | This issue depends on #X |
-
parent— a sub-issue relationship is containment, not sequencing. Do not turn it into anafter. A tracking/epic parent is not work; render it as asectionor a closingmilestone, never as its own bar, or you double-count its children's duration.
Drop references to issues outside the fetched set, and say which ones you dropped.
Then order the tasks:
#101 → start immediately
#102 blocked by #101 → after i101
#103 blocked by #101 and #102 → after i101 i102
after accepts several ids separated by spaces; the task starts when the last of them ends.
Check for cycles before emitting any Mermaid — see the Circular Dependencies edge case.
Step 5: Group into Phases
Organize tasks into logical sections: Setup/Foundation, Core Features, Testing, Polish.
Step 6: Generate Gantt Chart
Start from the closest pattern in gantt-patterns.md and fill in the tasks, dependencies, and sections. Before emitting, confirm:
- No task name contains a colon
- Every
afterid exists in the chart - Durations are working days if you set
excludes weekends, calendar days if you did not
Step 7: Render-Check the Chart
Mermaid accepts plenty of input that produces a broken picture. Do not hand over a chart you have
not checked: paste it into a Markdown preview, or run
npx -y @mermaid-js/mermaid-cli -i chart.mmd -o chart.svg and confirm it produces an SVG with no
NaN coordinates and with every task label intact.
Chart Directives and Formats
| Directive / format | Example | Effect |
|---|---|---|
| dateFormat | dateFormat YYYY-MM-DD | How input dates are parsed |
| axisFormat | axisFormat %b %d | How axis labels are printed |
| tickInterval | tickInterval 1week | Axis tick spacing — use for plans over ~3 weeks, or daily ticks turn to mush |
| excludes | excludes weekends | Skip non-working days, so 5d means five working days |
| todayMarker | todayMarker stroke-width:2px,stroke:#f66 | Highlight today; todayMarker off to hide |
| Absolute start | 2025-01-15 | Fixed start date |
| after id | after a | Start after one task ends |
| after id id | after b d | Start after the last of several tasks ends |
| Nd / Nw / Nh | 3d, 2w, 4h | Duration in days / weeks / hours |
| 0d | :milestone, m1, after i, 0d | Zero-length — for milestones |
Durations are calendar days unless you add excludes weekends. For sprint planning, set it —
otherwise a 5d task starting Thursday reports a finish date on a weekend.
Best Practices
- Use sections - Group related tasks for clarity
- Show dependencies - Use
after idto chain tasks - Mark milestones - Highlight key deliverables
- Indicate critical path - Use
:critfor must-do-first tasks - Keep titles short - Fit in the chart view, and strip colons
- Add buffer - Include slack for unknowns
- Render before you deliver - See Step 7
Example Output
For a set of issues about user authentication:
gantt
title User Authentication System
dateFormat YYYY-MM-DD
axisFormat %b %d
excludes weekends
section Foundation
Setup auth module :a, 2025-01-20, 1d
Database schema :b, after a, 1d
section Core Auth
Registration endpoint :c, after b, 3d
Login endpoint :d, after b, 3d
Password reset :e, after c, 2d
section Security
JWT implementation :f, after d, 2d
Rate limiting :g, after f, 1d
section Testing
Unit tests :h, after e g, 2d
Integration tests :i, after h, 2d
section Milestone
Auth Complete :milestone, m1, after i, 0d
Handling Edge Cases
No Estimates
If issues lack estimates after checking every source in Estimate to Duration Conversion:
- Use 4 points (4d) as default
- List exactly which issues were defaulted, and say the timeline is a guess until they are sized
- If most issues are unestimated, say so before drawing — an all-3d chart tells the user nothing
they didn't already know, and
estimating-work-itemscan size the work first
Circular Dependencies
Mermaid will not catch this for you. Given A after B and B after A it emits a chart with
NaN geometry and no error — on GitHub that renders as a blank or mangled diagram, which reads
like a rendering bug rather than a planning bug.
So detect the cycle yourself, before emitting any Mermaid:
- Build the dependency map from Step 4.
- Walk it depth-first, tracking the current path; a node you meet twice on one path closes a cycle. (Equivalently: repeatedly remove tasks with no unmet dependencies — whatever remains is cyclic.)
- Report the cycle as an error, naming the issues in the loop: "#102 → #103 → #102".
- Stop. Do not emit a partial chart, and do not silently break the loop — ask which dependency is wrong. A cycle means the plan is impossible, not that the diagram is hard to draw.
Too Many Tasks
Mermaid renders 25+ rows without complaint; the limit is human, not technical. Past roughly 20–25 bars the chart stops being readable, so:
- Split the work into
sections — Mermaid has no summary/rollup task, so sections are the only grouping it offers - Past ~25 bars, emit one chart per phase and a small top-level chart of phase-length bars plus milestones
- Or suggest filtering to one milestone or sprint
After Generating Timeline
- Show total project duration
- Identify the end date
- Flag if the end date falls after any
milestone.dueOnfrom Step 1, or any deadline the user stated - Offer to save the chart to a markdown file in the repo
- Suggest adding milestones if missing
- Suggest
dependency-mappingif the blocking structure needs its own diagram