a visual, node-based workflow engine with native CLI integration and user nodes in Python, Typescript, and Rhai. the general purposeness of n8n and the design sensibility of ComfyUI.
chain together LLMs, shell commands, HTTP requests, scripts, web search, audio, and images by wiring up typed nodes on a canvas.
build a graph, hit run, and flow walks the DAG, caches each node by the hash of its inputs, streams partial output as it arrives, and shows what every step produced.
grab a prebuilt release for your platform from the releases page, extract, and run:
tar xzf flow-*-<platform>-<arch>-py*.tgz
cd flow-*/
./flow-serverthen open http://127.0.0.1:3000. binaries are published for linux on
x86_64 and arm64 and for macos on arm64, in py<ver> (with python
user-node support) and nopy variants. every release also carries a
SHA256SUMS file, so a download can be checked with
sha256sum -c SHA256SUMS --ignore-missing.
to point flow at a local or remote openai-compatible LLM:
OPENAI_API_BASE=http://localhost:8080/v1 ./flow-servereach py<ver> build is linked against a specific cpython minor version.
flow embeds the interpreter and links against libpython<ver> at
startup, so the minor version must match what's available on your
system -- a py3.12 build will fail to start on a host that only has
python 3.13 installed. pick the build matching your system's
python3 --version, install a matching python, or use the nopy
release, which drops .py user-node support entirely and has no python
runtime dependency.
prefer to build from source? jump to building from source.
- local-first, single binary.
flow-serveris a self-contained rust binary that serves both the HTTP API and the UI (the react build is embedded at compile time viarust-embed). offline by default, with no telemetry or cloud account. - bring your own LLM. talks to anything that speaks the openai HTTP
API --
llama.cpp,llama-swap,ollama,vllm, or the real openai. setOPENAI_API_BASEand the bundled LLM/STT/TTS/TTI nodes pick it up automatically. - content-addressed caching. every node's output is hashed by its inputs and persisted to disk. re-running a workflow only re-executes the nodes that actually changed.
- streaming output through the DAG. long-running nodes (LLM
generation,
ShellCommand, HTTP SSE) emit partial output that propagates through downstream nodes and updates in the UI live. - two ways to drive it.
- web UI -- drag-and-drop canvas with an interactive node browser, keyboard shortcuts, dark/light/system themes, a job queue, and inline result viewers (markdown, JSON, image, audio).
flow-cli-- run any saved workflow from the command line, pipe stdin into aReadnode, override input values with--set, or run a single node with--node.
- hackable nodes. drop a file into
user_nodes/and flow picks it up on next start:.rhai-- rhai scripting (sandboxed, baked into the binary)..py-- full cpython via pyo3..ts-- typescript via the embedded boa engine..json-- declarative compositions of existing nodes, no code at all.
| category | nodes |
|---|---|
| core | Echo, Read, RandomInteger |
| process | ShellCommand (with stdin, streaming stdout, glob expansion) |
| web / HTTP | HttpRequest, WebFetch, WebSearch, HtmlToMarkdown |
| data | JsonQuery, Templatize, Join, Split, ListToJson, RegexpExtract, List, AudioConvert |
| display | DisplayMarkdown, DisplayJson, DisplayImage, DisplayAudio, AudioInput |
the bundled scripted user nodes in user_nodes/ add
openai-compatible LLM, TTS, STT, and text-to-image nodes (OpenAI_LLM,
OpenAI_TTS, OpenAI_STT, OpenAI_TTI), plus a MeshtasticSend python
node and a couple of declarative compositions (SpeechToImage,
ShellToSpeech).
every input on every node can be set via environment variable using the
auto-convention FLOW_<NODE_TYPE>_<INPUT_NAME> (all uppercase, special
characters replaced with underscores). some nodes also define shorter
explicit aliases -- for example, the openai nodes accept OPENAI_API_BASE
and OPENAI_API_KEY.
resolution priority: user-set value > FLOW_<NODE>_<INPUT> > alias env var > default.
the more-specific auto-convention var always wins over a shared alias,
so you can set OPENAI_API_BASE globally and override a single node with
FLOW_OPENAI_LLM_API_BASE.
the openai nodes read environment variables at runtime -- set these before starting the server or CLI:
| variable | purpose | default |
|---|---|---|
OPENAI_API_BASE |
base URL of any openai-compatible API server | https://api.openai.com/v1 |
OPENAI_API_KEY |
bearer token sent with each request | (empty) |
when an env var is active, the UI shows a badge on the input field. values set directly on the node always take priority over environment variables.
the model dropdown on the openai nodes lists models fetched from the
server, filtered to those whose input/output modalities suit the node. when
the server advertises llama-swap aliases, each alias is offered as its own
row at the top of the list, labelled with the model id it currently points
at -- picking the alias keeps the indirection, so the workflow follows the
alias when you re-point it.
the voice dropdown on OpenAI_TTS prefers the presets the server publishes
for the selected model. a preset belongs to one model, and picking one the
loaded model cannot load synthesizes empty audio instead of reporting a bad
voice, so that per-model listing is used on its own. where the server
publishes no voice data, or the model accepts a reference sample (and so can
speak as a clone), the voices enrolled on the server are listed too.
to list all available env vars:
cargo run --bin flow-cli -- envboth flow-server and flow-cli load a .env file from the current
directory on startup, so you can set these once instead of passing them
on every command:
# .env
OPENAI_API_BASE=http://localhost:8080/v1
OPENAI_API_KEY=sk-...or pass them inline:
# use a local llama.cpp / llama-swap / ollama server
OPENAI_API_BASE=http://localhost:8080/v1 make start
# use the real openai API
OPENAI_API_BASE=https://api.openai.com/v1 OPENAI_API_KEY=sk-... make start- rust (stable toolchain)
- node.js 16 or newer (for building the UI)
- optional: an openai-compatible HTTP endpoint if you want to run the
bundled LLM / STT / TTS / TTI workflows. examples that work locally:
llama.cpp,llama-swap,ollama,vllm. point flow at it withOPENAI_API_BASE. - optional: python 3 (for
.pyuser nodes -- enabled by default but can be turned off via cargo features).
git clone https://github.com/khimaros/flow.git
cd flow
make deps
make buildmake deps installs the UI dependencies from ui/package-lock.json, the
playwright browser used by the UI tests, the rust crates, and the python
packages the shipped .py nodes import (requirements-nodes.txt, into
.venv -- XmlSelect needs lxml). run it after a fresh clone and whenever
the lockfiles change.
flow adds that venv to the embedded interpreter's path itself, since an
embedded interpreter picks up no virtualenv on its own. set
FLOW_PYTHON_VENV to use one from elsewhere; otherwise an activated
VIRTUAL_ENV is used, falling back to .venv in the working directory.
make build runs npm run build in ui/ and then cargo build --workspace. the rust build embeds the freshly-built UI into the server
binary, so no extra static-file step is needed.
make start
# or, with an LLM backend:
OPENAI_API_BASE=http://localhost:8080/v1 make startthen open http://127.0.0.1:3000.
flow-server accepts a few flags:
flow-server \
--listen 127.0.0.1:3000 \
--data-dir . # where workflows/ and generated_assets/ live# run a saved workflow (`run` is the default subcommand)
cargo run --bin flow-cli -- workflows/shell-pipes.json
# pipe stdin into a Read node
fortune | cargo run --bin flow-cli -- workflows/read-echo.json
# override an input value at the command line. overriding a wired input
# severs that edge and skips the upstream node that only fed it.
cargo run --bin flow-cli -- workflows/text-to-speech.json \
--set tts_input/text="hello from flow"
# run only one node from a workflow
cargo run --bin flow-cli -- workflows/haiku-echo.json openai_llm_xxxx
# suppress diagnostic output on stderr (machine-friendly)
cargo run --bin flow-cli -- -q workflows/uuid-echo.json
# inspect a workflow
cargo run --bin flow-cli -- nodes workflows/haiku-echo.json
cargo run --bin flow-cli -- handles workflows/haiku-echo.json # all nodes
cargo run --bin flow-cli -- handles workflows/haiku-echo.json 'openai_*'
# lint workflows for inconsistencies; --fix to rewrite
cargo run --bin flow-cli -- lint
cargo run --bin flow-cli -- lint --fix
# cache management
cargo run --bin flow-cli -- cache stats # add `workflows/` for live/stale
cargo run --bin flow-cli -- cache prune
# route stdin to a specific node input (default: all Read nodes)
echo "hello" | cargo run --bin flow-cli -- workflows/read-echo.json \
--stdin read_node_id/input
# route a specific node output to stdout (default: terminal Echo nodes)
cargo run --bin flow-cli -- workflows/shell-pipes.json \
--stdout shellcommand_abc/stdout
# combine for full pipeline composability
fortune | cargo run --bin flow-cli -- workflows/haiku-echo.json \
--stdin read_xyz/input --stdout openai_llm_abc/responsethe workflows/ directory ships with examples covering most
of the builtin nodes. a few highlights:
| workflow | what it shows |
|---|---|
shell-pipes |
multi-stage shell pipelines built from ShellCommand, Templatize, List, and Split. |
shell-stream |
streaming stdout from a long-running shell command rendered live in DisplayMarkdown. |
http-request |
HttpRequest -> JsonQuery -> Echo, the canonical "talk to a JSON API" example. |
regexp-extract |
pulling matches out of free-form text with RegexpExtract. |
read-echo |
stdin -> Read -> Echo, the smallest interactive flow. |
uuid-echo |
demonstrates a typescript user node (UUID) chained with Echo. |
haiku-echo |
LLM prompt -> JsonQuery -> Echo. needs an openai-compatible backend. |
fetch-summarize |
web search -> fetch -> readability -> LLM summary. |
text-to-speech |
text input -> openai-compatible TTS -> DisplayAudio. |
speech-to-image |
microphone -> STT -> LLM rewrite -> text-to-image -> DisplayImage. |
declarative-s2s |
speech-to-speech pipeline assembled entirely from declarative JSON. |
stable-diffusion |
random seed + prompt -> text-to-image -> DisplayImage. |
mesh-quip |
generate a one-liner with the LLM and broadcast it over meshtastic. |
workflows that hit an LLM/STT/TTS/TTI endpoint will pick up
OPENAI_API_BASE from the environment.
a few of the most-used keyboard shortcuts:
| action | binding |
|---|---|
| run current workflow | Ctrl+Shift+Enter |
| save / save as | Ctrl+S |
| new workflow | Ctrl+N |
| toggle sidebar | B |
| pan canvas | scroll / middle-drag / arrows |
| zoom canvas | Ctrl+scroll |
| step through nodes | [ / ] |
| delete selected node | Delete / Backspace |
| show full shortcut overlay | ? |
right-click the canvas to add a node, right-click a saved workflow in the sidebar to rename or delete it, and drag from any node output handle onto empty canvas to add a connected downstream node.
drop a file into user_nodes/ and restart the server. the
existing examples are the best starting point:
openai_llm.rhai-- rhai user node with streaming HTTP, inputs withenv_varoverrides, and dynamic option fetching.meshtastic_send.py-- minimal python node showing the pyo3 surface.uuid.ts-- a tiny typescript node.speech_to_image.json-- a declarative composition that wires existing nodes together into a reusable unit, no code required.
see CONTRIBUTING.md for the node interface contract and the architecture overview.
src/ # rust backend
bin/ # flow-server, flow-cli
nodes/ # builtin node implementations
scripting/ # rhai / python / typescript runtimes
engine.rs # DAG scheduler + result cache
graph.rs # workflow / node-instance types
node.rs # the Node trait
value.rs # dynamic Value type used on the wire
ui/ # react + vite + reactflow frontend
# (built into the server binary via rust-embed)
workflows/ # bundled example workflows (.json)
user_nodes/ # bundled scripted / declarative user nodes
tests/ # API, CLI, and playwright UI end-to-end tests
make build # build server + UI
make start # run flow-server on http://127.0.0.1:3000
make test # run all e2e tests (API + CLI + UI + workflows)
make test-e2e # run the shipped workflows against a mock openai server
make lint # cargo clippy + ui eslint
make format # cargo fmt + prettier + black
make precommit # lint + test, run before pushingmake test-e2e runs the workflows in workflows/ end to end
against fake-openai, so the
openai-backed nodes are covered without a live provider or an api key. clone it
beside this repo; the target skips with a message when it is absent.
see CONTRIBUTING.md for the full developer guide, ROADMAP.md for what's planned and what's done, and the issue tracker for known bugs and feature requests.
