A visual flow-based programming platform with NATS orchestration. Build data pipelines by wiring nodes together on a canvas — every connection is a NATS pub/sub under the hood, so nodes can run in-process or as independent processes without changing the wiring.
Key features:
- 🎨 Visual flow editor with zoom, drag-and-drop node palette
- 🔄 Hot-sync: edit flows while they're running, changes deploy instantly
- 🚀 Process-mode nodes: run compute-heavy nodes as isolated OS processes
- 📊 Real-time debug feed and node activity monitoring
- 🎯 Bidirectional process control: ping, status, reload code, shutdown
- 🌊 Pure NATS messaging: nodes communicate via publish/subscribe only
Two core files:
flow_editor.html— complete visual editor (single HTML file, no build step)orchestrator.py— backend runtime that manages flows and NATS connections
docker compose up -d
# or without docker:
# brew install nats-server && nats-serverpip install -r requirements.txt
python3 orchestrator.pyListens on http://localhost:8000. The visual editor is served at the root URL. If NATS isn't ready, the orchestrator stays up and retries every 3 seconds — watch the status indicator (top right) in the UI.
Visit http://localhost:8000 in your browser to access the visual flow editor.
Alternatively, you can open flow_editor.html directly as a standalone file (double-click or open flow_editor.html). In standalone mode, the backend URL defaults to http://localhost:8000 — change it in the toolbar if your orchestrator runs elsewhere.
Try the example flow:
- Select "AdvScrnr" from the dropdown (stock screener demo)
- Click Deploy to validate and persist
- Click Run ▶ to start the flow
- Watch the Debug tab for real-time output
Every node publishes to flow.<node_id>.out.<port>. When you wire node A to node B, the orchestrator subscribes B to A's output subject. That's it — no direct function calls, no in-process graph traversal, just NATS subjects.
Packet format (JSON dict):
{
"_id": "abc123", # unique packet ID
"_ts": 1234567890.123, # timestamp
"payload": <any>, # your data
"subject": "flow.xyz.out", # originating subject
# ... custom fields ...
}| Type | Category | Description |
|---|---|---|
inject |
source | Fires on interval/manual trigger with fixed payload |
nats-in |
source | Bridges external NATS subject into flow |
function |
transform | Pure Python code: payload = packet['payload'] |
function-process |
transform | Runs in separate OS process (true parallelism, crash isolation) |
switch |
transform | Routes to different ports based on rules (==, >, contains) |
delay |
transform | Delays message by N milliseconds |
http-request |
transform | HTTP GET/POST, response goes in packet['payload'] |
nats-out |
sink | Bridges flow out to external NATS subject |
debug |
sink | Shows in Debug tab and flow._internal.debug subject |
Function nodes run in the orchestrator process:
# Your code in the editor:
quotes = packet['payload']
filtered = [q for q in quotes if q['price'] > 100]
packet['payload'] = filtered
node.emit(packet)Gets wrapped automatically:
def _user_fn(packet, node, context):
quotes = packet['payload']
filtered = [q for q in quotes if q['price'] > 100]
packet['payload'] = filtered
node.emit(packet)
return packetFunction-process nodes run as independent workers (see worker_template.py):
- Separate OS process with dedicated CPU core
- Can't crash or block the orchestrator
- Full bidirectional control via NATS (ping, reload code, shutdown)
- Status heartbeat every 10 seconds
- Perfect for CPU-intensive tasks (data processing, ML inference, screening)
GET /api/flows list all flow names
GET /api/flow?name=X load flow definition
POST /api/deploy?name=X deploy flow {nodes:[...], wires:[...]}
POST /api/run?name=X start deployed flow
POST /api/stop stop running flow
POST /api/inject/{node_id} manually fire inject node
GET /api/status orchestrator + NATS state, per-node stats
POST /api/process/{id}/control send control command to process node
WS /ws/debug live debug feed + wire activity (drives UI animations)
Send via POST /api/process/{node_id}/control:
{"command": "ping"} // health check
{"command": "get_status"} // full status dump
{"command": "reload_code"} // hot-reload user function
{"command": "shutdown"} // graceful shutdowntantu-flow/
├── flow_editor.html # visual editor (single-file SPA)
├── orchestrator.py # backend runtime (FastAPI + NATS)
├── worker_template.py # template for process-mode workers
├── requirements.txt # Python dependencies
├── docker-compose.yml # NATS server setup
└── flows/ # deployed flows
└── AdvScrnr/ # example: stock screener
├── flow.json # node graph + wires
├── n_1_mndx.py # fetch quotes (yfinance)
├── n_3_5me4.py # screen by criteria
└── n_5_zi85.py # rank & format
Your code has access to three magic variables (no imports needed):
# 'packet' — incoming message dict
symbol = packet['payload']['symbol']
# 'node' — emit packets to output ports
node.emit(packet) # default 'out' port
node.emit(other_packet, port='alt') # named port
# 'context' — persistent dict (survives across fires)
if 'count' not in context:
context['count'] = 0
context['count'] += 1No return statement needed — just mutate packet and call node.emit().
Add to orchestrator.py:
@register_node("my-type")
class MyNode(BaseNode):
async def handle(self, packet: dict):
# your logic here
await self.emit(packet)Add to NODE_TYPES in flow_editor.html:
"my-type": {
category: "transform",
color: "#22d3ee",
icon: "fa-star",
label: "My Node",
outputs: {out: "output"},
config: [
{key: "myfield", label: "My Field", type: "text", default: ""}
]
}- Security: Function nodes run via
exec()in-process. Only deploy code you trust. - Persistence: Last deployed flow saved to
flows/<name>/flow.jsonand auto-deploys on restart once NATS connects. - Error handling: Node crashes are logged to Debug tab. The rest of the flow continues running.
- Scalability: Single-process by default. For horizontal scaling, run function-process nodes as separate containers — they're already NATS-native.
- No validation: Wire connections don't validate message schemas. If a node expects different data, it will error.
inject (daily) → fetch quotes (yfinance) → screen criteria → rank → debug
- Inject fires daily at market close
- Fetch quotes (function-process): pulls data for S&P 500 via yfinance
- Screen (function-process): filters by price change %, volume ratio, distance from 52w low
- Rank (function): sorts by score and formats output
- Debug: displays top picks in Debug tab
All compute-heavy nodes run as isolated processes with full status monitoring.
MIT — do whatever you want with it.
🎯 tantu flow — visual + isolated + observable