Skip to main content

flowlint — a linter for your user flows. A flow diagram where most of the path is healthy and one branch, drawn in red, ends in a screen with no way out.

CI PyPI Python 3.8+ Dependencies: none License: MIT

Your linter checks syntax. Your type checker checks types. Nothing checks whether a user can get out of the screen they just landed on.

flowlint reads an existing codebase, maps the flow the code actually implements, and reports what is wrong with it — dead ends, network calls with no error branch, OAuth screens with no cancel path. Every finding names a file:line.

pip install flowlint
flowlint check docs/ux-flows/*.flow.json --fail-on-high
✗ auth-login    4 findings (3 high) · primary path 7 steps · 0 dead ends
    high   Error never shown to the user — /login?error=auth   app/auth/callback/route.ts:17
    high   Waiting on an email with no resend — Sign-in link sent   app/login/page.tsx:68
    high   External hand-off with no cancel path — Google OAuth   app/login/page.tsx:20

Why

Every application has two flows: the one the designer drew, and the one the code implements. The second is written down nowhere. Error states, empty states, cancel paths, permission denials — rarely designed, usually improvised while coding.

Here is a real finding from the first run against a real app:

/auth/callback redirects to /login?error=auth when OAuth fails — but the login page never reads that query parameter. A user whose sign-in fails lands on an empty login screen with no message, taps the same button, and gets the same result.

Nobody designed that. It is what the code does.


How it works

You (or your agent) describe the flow once as JSON. flowlint does everything else, deterministically.

   you / your agent                 flowlint
   ────────────────                 ────────
   read the code   ──►  flow.json  ──►  <id>.drawio      <id>.md
                        (in git)        (regenerated, never hand-edited)

The JSON is the source of truth: it lives next to the code, reviews like code, diffs like code. The diagrams are build artefacts, and flowlint stale fails the PR when they drift apart. That is what stops flow diagrams from rotting.

Three files per flow:

File What it is
<flow>.flow.json the description you edit
<flow>.drawio editable diagram — real shapes on a real canvas, one tab per view
<flow>.md the report: summary, action list, diagram inline, metrics, findings

Quick start

With an agent — install as a skill (see Installing), then ask:

Map the checkout and signup flows in this app and show me where the friction is.

It reads SKILL.md (Claude) or AGENTS.md (Cursor, Codex, Copilot, Cline, Aider, Gemini…), inventories your routes, asks which flows to map, traces them, and renders.

By hand

flowlint init checkout -o docs/ux-flows
# describe the flow in docs/ux-flows/checkout.flow.json
flowlint validate docs/ux-flows/checkout.flow.json
flowlint render   docs/ux-flows/checkout.flow.json -o docs/ux-flows

Try it on the bundled examples

flowlint check  examples/*.flow.json
flowlint render examples/checkout.flow.json -o /tmp/demo

What it catches

Straight from the graph — no heuristics, no guessing.

Can the user get stuck? dead ends · screens whose only exit is backwards · unreachable nodes · orphans · redirect loops with no way to change the outcome

What happens when it fails? network calls with no error branch · external hand-offs (OAuth, 3-D Secure, payment) with no cancel path · out-of-band waits (magic link, OTP) with no resend · decisions with one branch

Is it longer than it needs to be? step count · taps · required fields · friction tags, severity-ranked

Plus a model-quality check: nodes with no source anchor are flagged, because a claim you cannot verify weakens the whole map.

Findings are written to be acted on

Not no_error_state · "No error state." Each finding says what the code does, what the user experiences, what to change, and where — enough to paste into a tracker unchanged. Each carries a stable id, so a team can accept one without it disappearing:

flowlint ignore UXF-NOERR-0A7D --reason "scheduled for Q3"

It moves to an "accepted" section and stops failing CI. That is what makes --fail-on-high adoptable on a codebase that already exists.

See references/findings-guide.md for every rule and what makes it fire.


Before / after

Model the flow as it is, copy it, fix it, render the delta:

flowlint diff checkout.flow.json checkout-proposed.flow.json -o docs/ux-flows

From examples/:

metric before after
steps on the primary path 9 8
required form fields 14 9
modelled error branches 2 6
high-severity findings 7 0

A design argument you can take to a stakeholder, with a colour-coded diff diagram to go with it.


In CI

Copy examples/ci/flowlint.yml into your app. The two lines that matter:

- run: flowlint stale docs/ux-flows/*.flow.json -o docs/ux-flows
- run: flowlint check docs/ux-flows/*.flow.json --fail-on-high

stale fails when someone edited a flow without regenerating its diagram. --fail-on-high blocks merges that introduce a dead end or an unhandled failure.


Supported stacks

Stack Playbook
Next.js (App + Pages Router), React, React Router, Remix, TanStack discovery-web.md
React Native, Expo (Expo Router + React Navigation) discovery-react-native.md
Flutter (GoRouter, AutoRoute, named routes) discovery-flutter.md
SwiftUI, UIKit, Jetpack Compose discovery-native.md

The description format is stack-agnostic — anything you can read, you can model. Adding a stack means adding one Markdown file; the renderer does not change.


CLI

flowlint check    <flow.json>...  lint the flows           [-o DIR] [--fail-on-high]
flowlint render   <flow.json>...  diagram + full report    [-o DIR] [--formats …]
flowlint diff     <before> <after>  before/after + metric delta
flowlint stale    <flow.json>...  CI: diagrams out of sync with their source
flowlint validate <flow.json>...  schema and integrity check
flowlint ignore   <FINDING-ID>    accept a finding         [--reason TEXT]
flowlint init     <flow-id>       scaffold a flow file
flowlint id       <route>         mint a stable node id

Installing

CLI

pip install flowlint

Skill in Claude Code — a skill is a directory with a SKILL.md, and the directory may be a symlink, so the checkout stays current with every git pull:

git clone https://github.com/js-lover/flowlint.git ~/src/flowlint
mkdir -p ~/.claude/skills && ln -s ~/src/flowlint ~/.claude/skills/flowlint

Use a project's .claude/skills/ instead to scope it to one repo.

Skill in Claude Cowork — download flowlint.skill from Releases and open it.

Vendored — works with any agent, and with none:

git clone --depth 1 https://github.com/js-lover/flowlint.git && rm -rf flowlint/.git
python3 flowlint/scripts/flowlint.py --help

All four run the same code. Zero dependencies is deliberate: the tool has to run inside whatever environment an agent finds itself in, with no resolver step.


Why it is built this way

Layout is ours, not draw.io's. The same input must produce identical geometry everywhere, or every regeneration is a 400-line diff. flowlint implements a layered (Sugiyama-style) layout: break cycles → layer → barycenter ordering → coordinates.

Node ids are stable and semantic. That is how diff tells "this screen changed" from "this screen was deleted and another added".

Every node carries a source anchor. A diagram nobody can verify is a diagram nobody trusts. Hover a box in draw.io and you get src/app/checkout/payment/page.tsx:24.

The primary path is exact, not sampled. Two earlier versions were wrong in instructive ways. Following happy edges greedily meant a guard like "already signed in? → home" ended the search after two hops, and every metric described a path no real user walks. Enumerating paths under a visit budget was worse: a wide fan-out exhausts the budget and returns a truncated answer without saying so. Removing cycle-closing edges gives a DAG, where longest path is linear and exact. Silently wrong metrics are the worst kind of bug in a tool people are supposed to trust.


Contributing

python3 -m unittest discover -s tests -v

69 tests, no dependencies. See CONTRIBUTING.md — adding a stack is one Markdown file, and every audit rule has to state precisely when it is right.

Licence

MIT — see LICENSE.

Metadata

Release files for flowlint 1.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for flowlint 1.4.0
File Size Uploaded
flowlint-1.4.0.tar.gz 56.3 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for flowlint 1.4.0
File Interpreter ABI Platform
flowlint-1.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 110.2 kB

Release files / flowlint-1.4.0.tar.gz

Download URL flowlint-1.4.0.tar.gz
Size 56.3 kB
Tags Source
SHA-256 checksum
How to use checksums
d2748e61efd056436a2328fe494921a7a208d61e5e36153023cf28d2075c6f71
BLAKE2b-256 checksum
How to use checksums
3da067a9ae6763e5c856d1164b30cfd93f270508c70e8663960f3e10aa4720ab
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 12, 2026.

Transparency log

Release files / flowlint-1.4.0-py3-none-any.whl

Download URL flowlint-1.4.0-py3-none-any.whl
Size 53.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
bea4c2e192d09b346afd606250e8e0b9db2852c4ef179954846442f11b4da011
BLAKE2b-256 checksum
How to use checksums
c6e8be55eb4bcb8fc04b3b1d5034896f2839dfdca6c331fac3383028ff7733f8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 12, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

1.4.0 This release

2 release files

1.3.1

2 release files

1.3.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page