OVERVIEW
Architecture Overview
How Floww works internally — the rendering model, execution engine, and file format.
High-Level Architecture
Floww is a desktop-first node-based workflow engine. Its architecture is composed of four cooperating layers:
| Layer | Technology | Responsibility |
|---|---|---|
| Shell | Electron / Tauri | Window management, native menus, file-system access, IPC bridge |
| Renderer | WebKit2GTK (Linux), Chromium (Electron) | HTML/CSS/JS rendering, DOM management |
| Canvas Engine | Custom HTML5 Canvas + DOM hybrid | Wire rendering, node layout, viewport transforms, hit-testing |
| Execution Runtime | JavaScript (sandboxed worker) | Node execution, data propagation, variable resolution, error handling |
The shell process launches the renderer process and communicates with it over an IPC channel. The renderer hosts both the canvas engine (which draws the visual graph) and the execution runtime (which runs workflows). These two subsystems are decoupled: you can execute a workflow headlessly without the canvas engine, which is how the localFloww SDK works.
The .floww File Format
A .floww file is a JSON document that describes a directed acyclic graph (DAG) of nodes and edges, along with workflow-level variables and metadata. Here is a minimal example:
{
"version": "1.0",
"metadata": {
"name": "My Workflow",
"description": "A simple two-node pipeline",
"author": "you",
"created": "2025-06-15T10:00:00Z",
"modified": "2025-06-15T12:30:00Z"
},
"variables": {
"apiKey": { "type": "string", "value": "", "sensitive": true },
"retryCount": { "type": "number", "value": 3, "sensitive": false }
},
"nodes": [
{
"id": "node-001",
"type": "http-request",
"position": { "x": 120, "y": 200 },
"config": {
"method": "GET",
"url": "https://api.example.com/data"
}
},
{
"id": "node-002",
"type": "json-parse",
"position": { "x": 420, "y": 200 },
"config": {}
}
],
"edges": [
{
"id": "edge-001",
"source": { "nodeId": "node-001", "port": "response" },
"target": { "nodeId": "node-002", "port": "input" }
}
]
}
Schema rules
- version — currently
"1.0". Floww will migrate older files automatically on open. - nodes — each node has a unique
id, atypethat maps to a registered node definition, a canvasposition, and an arbitraryconfigobject shaped by the node's configuration schema. - edges — each edge connects an output port on a source node to an input port on a target node. Cycles are rejected at parse time.
- variables — workflow-level key-value pairs accessible to any node at execution time. Variables marked
sensitiveare encrypted at rest.
Node Execution Lifecycle
Every node follows a strict state machine during execution:
┌──────────┐
│ IDLE │
└────┬─────┘
│ trigger
▼
┌──────────┐
│ INIT │ ← resolve config, bind variables
└────┬─────┘
│
▼
┌──────────┐
│ VALIDATE │ ← check inputs match expected types
└────┬─────┘
│ pass
▼
┌──────────┐
│ EXECUTE │ ← run async execute() function
└────┬─────┘
│
┌───┴───┐
▼ ▼
┌────────┐ ┌───────┐
│COMPLETE│ │ ERROR │
└────────┘ └───────┘
- IDLE — the node is waiting. It has not been triggered yet.
- INIT — the runtime resolves configuration values, substitutes variable references (
{{variableName}}), and prepares the execution context. - VALIDATE — input data from upstream nodes is checked against the port type declarations. If validation fails, the node transitions directly to ERROR.
- EXECUTE — the node's
execute()function runs asynchronously. It receives inputs and config, and must return an outputs object. - COMPLETE — execution finished successfully. Output data is propagated to downstream nodes.
- ERROR — something went wrong. The error is captured, and the workflow engine decides whether to halt or continue depending on the error handling strategy.
stop (halt the entire workflow), skip (skip the failed node and continue downstream with null), and retry (retry the node up to N times with exponential backoff).
Canvas Rendering Model
Floww uses a hybrid rendering approach that balances performance with interactivity:
- Wires — drawn on an HTML5
<canvas>element using Bézier curves. The canvas layer sits behind the node DOM and is redrawn on every frame during panning, zooming, or dragging. - Nodes — rendered as regular DOM elements positioned with CSS transforms. This means nodes support standard text selection, form inputs, and accessibility features that would be difficult in a pure-canvas approach.
- Virtual viewport — the visible area is tracked as a viewport rectangle. Only nodes within or near the viewport are mounted in the DOM (a technique similar to virtual scrolling in list UIs). This allows Floww to handle workflows with thousands of nodes without degrading frame rate.
- Culling — wires whose start and end are both off-screen are skipped during the canvas draw pass. A spatial index (R-tree) accelerates these visibility checks.
Coordinate system
Positions in the .floww file are in world coordinates. The canvas engine maintains a transform matrix that maps world coordinates to screen coordinates based on the current pan offset and zoom level:
screenX = (worldX - panX) * zoom
screenY = (worldY - panY) * zoom
Event System
Floww uses an internal publish/subscribe event bus for communication between the canvas engine, execution runtime, and UI layer. This is also the primary extension point for plugins.
Core events
| Event | Emitted when |
|---|---|
node:added | A node is placed on the canvas |
node:removed | A node is deleted |
node:executed | A node finishes execution (success or error) |
edge:connected | Two ports are wired together |
edge:disconnected | A wire is removed |
workflow:started | A workflow execution begins |
workflow:completed | All nodes have finished |
workflow:error | A workflow-level error occurs |
canvas:viewport-changed | The user pans or zooms |
variable:changed | A workflow variable is updated |
Subscribing from a plugin
// Listen for node execution completions
floww.events.on('node:executed', (event) => {
console.log(`Node ${event.nodeId} finished in ${event.duration}ms`);
console.log('Outputs:', event.outputs);
});
// Emit a custom event
floww.events.emit('myplugin:data-ready', { rows: 42 });
Custom events should be namespaced with your plugin name to avoid collisions (e.g., myplugin:event-name).