nvim-flow is a pure lua Neovim workflow runner for file-based commands defined in .flow.yml.
Create a .flow.yml next to your project files:
test.py:
cmd: python "{{filepath}}" --name mike
Open the file in Neovim and run :FlowRun or :FlowDebug — nvim-flow resolves the command for the current file and executes it in a terminal split:

I wanted a workflow that matches how I actually work in Neovim: simple YAML config, fast command resolution, and quick run/debug feedback without extra runtime dependencies.
nvim-dap integration through FlowDebug:FlowEdit) to open the matched .flow.yml definition:FlowToggleLock):FlowPreview):FlowQuickfix).flow.yml)$HOME, closer wins)match arrays for reusable command definitionssetup()return {
{ "mikeboiko/nvim-flow" },
}
return {
{
"mikeboiko/nvim-flow",
event = { "BufReadPost", "BufNewFile" },
cmd = { "FlowRun", "FlowDebug", "FlowEdit", "FlowToggleLock", "FlowPreview", "FlowQuickfix" },
opts = {
config_file = ".flow.yml",
terminal_height = 15,
terminal_position = "top",
edit_open_command = "tabedit",
stop_at_home = true,
show_command = true,
keymaps = {
run = "<CR>",
debug = "<leader>fd",
edit = "<leader>fe",
toggle_lock = "<leader>fl",
preview = "<leader>fp",
quickfix = "<leader>fq",
},
},
},
}
setup() is only needed when you want to override defaults.
If you skip setup, nvim-flow still works with built-in defaults.
require("nvim-flow").setup({
config_file = ".flow.yml",
terminal_height = 15,
terminal_position = "top", -- "top" | "bottom"
edit_open_command = "tabedit", -- e.g. "tabedit" | "edit" | "split" | "vsplit"
stop_at_home = true,
show_command = true,
keymaps = {
run = nil,
debug = nil,
edit = nil, -- suggested: "<leader>fe"
toggle_lock = nil,
preview = nil,
quickfix = nil,
},
})
config_file (string, default: ".flow.yml")terminal_height (number, default: 15)FlowRun.terminal_position ("top" | "bottom", default: "top")edit_open_command (string, default: "tabedit")FlowEdit to open the matched .flow.yml location (for example: tabedit, edit, split, vsplit).stop_at_home (boolean, default: true)$HOME instead of /.show_command (boolean, default: true)keymaps (table, default: all nil)rundebugedittoggle_lockpreviewquickfixnil to leave it unmapped.:FlowRun - run resolved flow command in a terminal split:FlowDebug - resolve the same flow command and launch a matching nvim-dap debug session:FlowEdit - open the matched .flow.yml file and jump to the resolved command line:FlowToggleLock[ {filepath}] - toggle lock (or set lock to explicit path):FlowSet {filepath} - compatibility alias for setting lock directly:FlowPreview - show resolved command for current (or locked) file:FlowQuickfix - parse the last flow output as Python traceback and fill quickfixFlowEdit follows the same resolution pipeline as FlowRun / FlowPreview, then opens the corresponding .flow.yml and jumps to the resolved command line.
By default it opens in a new tab (edit_open_command = "tabedit"). Change edit_open_command if you prefer edit, split, or vsplit.
nvim-dap)FlowDebug uses the same command resolution pipeline as FlowRun, then parses the command to create a debug configuration for nvim-dap and calls dap.continue().
Supported command families include python / python3, uv run ... (including module mode), and node.
Example:
py:
cmd: python "{{filepath}}" --env dev
Running :FlowDebug on a Python buffer resolves this flow, builds the debug launch config, and starts the debugger.
.flow.yml formatdefault:
cmd: '{{filepath}}'
py:
cmd: python "{{filepath}}"
main.py:
cmd: python "{{filepath}}" --mode=dev
python-group:
match: [py, pyw, 'test_*.py', 'scripts/']
cmd: python "{{filepath}}"
If match is omitted, the top-level key is used as before.
Resolution order:
main.py)match entries.py then py)defaultExample definitions for each priority type:
main.py:
cmd: echo "1 basename"
python-group:
match: [py, 'test_*.py']
cmd: echo "2 match"
tests:
cmd: echo "3 folder"
my-repo:
cmd: echo "4 repo"
.py:
cmd: echo "5 extension-dot"
py:
cmd: echo "5 extension"
default:
cmd: echo "6 default"
Mini winner scenario:
/work/my-repo/tests/main.py, main.py (basename) wins.match entries are checked before folder/repo/extension/default.If multiple match entries apply, nvim-flow uses deterministic precedence: nearest config file first, then YAML declaration order within that file.
When running from /a/b/c/file.py, nvim-flow searches for .flow.yml in:
/a/b/c/.flow.yml/a/b/.flow.yml/a/.flow.yml$HOME/.flow.yml (if stop_at_home = true)All found configs are merged. Closer files override farther files.
nvim-flow expands these variables in cmd:
{{filepath}}{{dir}}{{filename}}{{ext}}{{repo}}{{folder}}runner: vim or omitted)terminal_position = "bottom" to open below.show_command = true, the separator line is sized to the command width (capped by terminal width).runner: debug or :FlowDebug (requires nvim-dap)FlowQuickfix parses the last FlowRun terminal output and extracts Python traceback lines:
File "/path/file.py", line 42, in ...
Then it populates and opens the quickfix list.
This plugin uses plenary's busted harness.
Run tests:
nvim --headless -u tests/minimal_init.lua \
-c "PlenaryBustedDirectory tests/nvim-flow { minimal_init = 'tests/minimal_init.lua' }" \
-c "qa"
This repo includes a VHS tape at vhs/nvim-flow-demo.tape that records a demo using ~/demo/test.py and ~/demo/.flow.yml.
The demo shows:
:FlowRun — execute the command in a terminal split<space>db, then :FlowDebug (nvim-dap)Run it with:
vhs vhs/nvim-flow-demo.tape
vhs publish nvim-flow-demo.gif
The tape outputs vhs/nvim-flow-demo.gif, which autoplays when embedded in README markdown.
This project was inspired by ideas from vim-flow, with substantial changes for this codebase and workflow:
https://github.com/jonmorehouse/vim-flow