Generate user story maps from markdown. Write your product spec as a readable document — personas, releases, activities, tasks, and stories — and render it as a styled, interactive HTML page.
pip install storymapstorymap init # create a skeleton storymap.md
storymap init myproduct.md # create a named skeleton
storymap render mymap.md # → mymap.html (same directory)
storymap render mymap.md -o out # → out/mymap.htmlThe output is a single self-contained HTML file. Local images referenced in story descriptions are embedded as base64 data URIs — no external files needed.
PDF output: open the generated HTML in a browser and use print-to-PDF (Ctrl+P → Save as PDF). The HTML includes print-optimised CSS for landscape layout and color preservation.
A storymap file is a standard markdown document with three reserved top-level
sections: # Releases, # Personas, and # Map. Any other # heading is
treated as the document title (first one) or ignored.
# My Product
Short description.
# Releases
## MVP
First public release.
## Beta
Invite-only beta with selected users.
# Personas
## Margie the Manager
- **Age:** 45–55
- **Tech level:** Low
Margie manages a team of 8 and primarily uses the app on mobile.
# Map
## User Management
### Authentication
#### Sign in [status:: done] [persona:: Margie the Manager] [release:: MVP]
User can log in with email and password.
#### Password reset [status:: in-progress] [deadline:: 2026-03-01] [release:: Beta]
### Profile
#### Edit profile [status:: done] [release:: MVP]
#### Upload avatar [status:: blocked] [release:: Beta]
Blocked pending storage provider decision.| Section | Required | Purpose |
|---|---|---|
# Releases |
Yes | Defines release swimlanes |
# Personas |
No | UX persona descriptions |
# Map |
Yes | The story map itself |
Section names are case-insensitive.
## Activity (column group)
### Task (column)
#### Story (card in a swimlane)
Use the [release:: name] field on each story to assign it to a release
swimlane. The value must match a release name (or id, see below) defined in
# Releases. Stories without a [release::] field are parsed but not shown
in any swimlane.
### Authentication
#### Sign in [status:: done] [release:: MVP]
#### SSO [status:: not-started] [release:: Beta]If a release has a long display name, add an [id:: short-name] field to the
release heading. Stories then use the short id instead of the full name.
storymap will warn if a release name contains spaces and has no [id::].
# Releases
## Minimum Viable Product [id:: mvp]
## Private Beta [id:: beta]
# Map
## User Management
### Authentication
#### Sign in [status:: done] [release:: mvp]
#### SSO [status:: not-started] [release:: beta]This decouples the display name from the identifier — you can rename the release heading freely without updating every story.
Stories support optional inline fields using [key:: value] syntax.
Fields appear as badges on the rendered story card.
#### Story name [status:: done] [persona:: Margie the Manager] [deadline:: 2026-03-01] [release:: mvp]| Field | Values | Default |
|---|---|---|
status |
not-started, in-progress, done, blocked |
not-started |
release |
Release name or id from # Releases section |
— |
persona |
Any string matching a persona name | — |
deadline |
ISO date YYYY-MM-DD |
— |
Any other [key:: value] field is accepted and rendered as a badge.
Markdown content following a #### Story heading and before the next heading
is treated as the story description. Descriptions support standard markdown:
bold, italics, links, lists, and images.
The first paragraph is always visible on the story card. Additional paragraphs are shown only in Detail zoom level.
Images in story and persona descriptions are embedded as base64 data URIs in
the output HTML, making the file fully self-contained. Paths are resolved
relative to the source .md file. Remote URLs are left as-is.
The rendered HTML includes controls for navigating large maps:
Zoom levels — three buttons in the sticky header:
- Overview — story names only, compact layout
- Map — story names, status badges, and first-paragraph descriptions
- Detail — full descriptions expanded
Release focus — a dropdown to highlight one release swimlane and dim the rest. Works independently of the zoom level.
Story Lens — a toggle that enables click-to-zoom on individual story cards. When active, clicking a card expands it to ~40% of the viewport width. Clicking outside collapses it.
Usage: storymap [OPTIONS] COMMAND [ARGS]...
Generate user story maps from markdown.
Options:
--version Show the version and exit.
--help Show this message and exit.
Commands:
init Create a skeleton storymap markdown file to get started.
render Render a story map markdown INPUT_FILE to HTML.
Usage: storymap render [OPTIONS] INPUT_FILE
Options:
-o, --output DIR Output directory. Defaults to the input
file's directory.
-t, --template FILE Path to a custom Jinja2 template (.html.j2).
--status-colors KEY=COLOR,... Override status colors.
Example: done=#00FF00,blocked=#FF0000
--ui-colors KEY=COLOR,... Override UI colors.
Example: activity=#1565C0,task=#90CAF9
--help Show this message and exit.
Usage: storymap init [OUTPUT_FILE]
OUTPUT_FILE defaults to storymap.md in the current directory.
Refuses to overwrite an existing file.
storymap render mymap.md \
--status-colors "done=#27AE60,in-progress=#2980B9,blocked=#E74C3C" \
--ui-colors "activity=#2C3E50,task=#34495E"Default status colors:
| Status | Color |
|---|---|
not-started |
#E0E0E0 grey |
in-progress |
#90CAF9 blue |
done |
#A5D6A7 green |
blocked |
#EF9A9A red |
storymap render mymap.md --template my-template.html.j2The template receives:
| Variable | Type | Description |
|---|---|---|
document |
StorymapDocument |
The full parsed document |
status_colors |
dict[str, str] |
Resolved status → hex color |
ui_colors |
dict[str, str] |
Resolved UI element → hex color |
render_md |
callable |
Render a markdown string to HTML |
render_md_intro |
callable |
Render first paragraph only |
render_md_rest |
callable |
Render everything after first paragraph |
The darken filter is also available: {{ color | darken }}.
git clone https://github.com/mozaicworks/storymap
cd storymap
just install
just teststorymap/
├── model.py — dataclasses and default color constants
├── parser.py — markdown-it-py state machine parser
├── renderer.py — Jinja2 HTML renderer with base64 image embedding
├── cli.py — click CLI entry point
└── templates/
└── default.html.j2
tests/
├── test_model.py
├── test_parser.py
├── test_renderer.py
└── test_cli.py
MIT