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.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

flowlint-1.4.0.tar.gz (56.3 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

flowlint-1.4.0-py3-none-any.whl (53.8 kB view details)

Uploaded Python 3

File details

Details for the file flowlint-1.4.0.tar.gz.

File metadata

  • Download URL: flowlint-1.4.0.tar.gz
  • Upload date:
  • Size: 56.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for flowlint-1.4.0.tar.gz
Algorithm Hash digest
SHA256 d2748e61efd056436a2328fe494921a7a208d61e5e36153023cf28d2075c6f71
MD5 d1a61109ce7578bc5c081b33e8709cf1
BLAKE2b-256 3da067a9ae6763e5c856d1164b30cfd93f270508c70e8663960f3e10aa4720ab

See more details on using hashes here.

Provenance

The following attestation bundles were made for flowlint-1.4.0.tar.gz:

Publisher: release.yml on js-lover/flowlint

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file flowlint-1.4.0-py3-none-any.whl.

File metadata

  • Download URL: flowlint-1.4.0-py3-none-any.whl
  • Upload date:
  • Size: 53.8 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for flowlint-1.4.0-py3-none-any.whl
Algorithm Hash digest
SHA256 bea4c2e192d09b346afd606250e8e0b9db2852c4ef179954846442f11b4da011
MD5 3937a74a1fa77788f697f13b8ee7a8ac
BLAKE2b-256 c6e8be55eb4bcb8fc04b3b1d5034896f2839dfdca6c331fac3383028ff7733f8

See more details on using hashes here.

Provenance

The following attestation bundles were made for flowlint-1.4.0-py3-none-any.whl:

Publisher: release.yml on js-lover/flowlint

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page