A lightweight workflow automation engine for Bun inspired by n8n and Node-RED. Build powerful automation workflows with a visual node-based editor and real-time HTML dashboards.
- π Fast & Lightweight - Built on Bun for maximum performance
- π¨ Visual Editor - Drag-and-drop workflow builder with React Flow
- π‘ Real-time Communication - Built-in MQTT & WebSocket brokers
- π HTTP Endpoints - Create REST APIs with HTTP In/Out nodes
- π€ AI Integration - Connect to OpenAI, DeepSeek, OpenRouter, and more
- π HTML Dashboards - Serve real-time HTML pages with WebSocket updates
- β° Interval Triggers - Schedule recurring tasks with dynamic payloads
- οΏ½ THyperflow DAG - Advanced parallel execution with dependencies
- πΎ Persistent Deployments - Workflows survive server restarts
- π Authentication - Built-in user authentication system
# Install dependencies
bun install
# Start the server
bun run dev
# Open in browser
open http://localhost:3000| Node | Description |
|---|---|
| Inject π | Manually trigger a flow with static payload |
| Interval β° | Send messages at regular intervals |
| HTTP In π | Create REST endpoints |
| MQTT In πΆ | Subscribe to MQTT topics |
| WebSocket In π | Subscribe to WebSocket topics |
| Node | Description |
|---|---|
| Debug π | Log messages for debugging |
| HTTP Response π€ | Send HTTP response |
| MQTT Out π’ | Publish to MQTT topic |
| WebSocket Out π | Publish to WebSocket topic |
| HTML Output π | Serve HTML page with real-time data |
| UI Gauge/Text/Number/Switch π | Dashboard widgets |
| Node | Description |
|---|---|
| Function βοΈ | Run custom JavaScript code |
| Filter π | Route messages by condition |
| Transform π | Modify message properties |
| Template π | Generate text from template |
| Loop π | Iterate over arrays or count |
| AI Generate π€ | Generate text with AI |
| Hyperflow π | Execute DAG pipelines |
| Node | Description |
|---|---|
| HTTP Request π | Make API calls |
| Delay β±οΈ | Pause flow execution |
| Split βοΈ | Split array into messages |
| Join π | Combine messages into array |
| Data Table π | Create/manipulate data tables |
Serve dynamic HTML pages with real-time data updates via WebSocket.
- Add an Interval node to generate data
- Connect to an HTML Output node
- Configure the slug (e.g.,
demo) - Deploy the workflow
- Visit
http://localhost:3000/demo/ui
<!-- Bind text content to a property -->
<div data-bind="value">--</div>
<!-- Nested properties -->
<div data-bind="user.name">--</div>
<div data-bind="sensor.temperature">--</div><!-- Render HTML (not escaped) -->
<div data-bind-html="htmlContent"></div><!-- Dynamic styles (format: property:path) -->
<div data-bind-style="width:progress">0%</div>
<div data-bind-style="backgroundColor:statusColor"></div>Use the htmloutput:update event for complex transformations:
<div id="formatted-temp">--</div>
<div id="status-badge">--</div>
<script>
window.addEventListener('htmloutput:update', (e) => {
const data = e.detail;
// Format temperature with unit
document.getElementById('formatted-temp').textContent =
data.temperature.toFixed(1) + 'Β°C';
// Conditional styling
const badge = document.getElementById('status-badge');
if (data.value > 80) {
badge.textContent = 'β οΈ HIGH';
badge.style.color = 'red';
} else {
badge.textContent = 'β Normal';
badge.style.color = 'green';
}
// Calculate derived values
const heatIndex = data.temperature + (data.humidity * 0.1);
document.getElementById('heat-index').textContent = heatIndex.toFixed(1);
});
</script>// Get current data
const data = HtmlOutput.getData();
// Send data back to workflow (bidirectional)
HtmlOutput.send({ buttonClicked: true, value: 42 });
// Listen for updates
window.addEventListener('htmloutput:update', (e) => {
console.log('New data:', e.detail);
});Interval Node Payload:
{
value: Math.round(Math.random() * 100),
temperature: 20 + Math.random() * 15,
humidity: 40 + Math.random() * 40,
status: Math.random() > 0.5 ? 'online' : 'warning',
timestamp: Date.now()
}HTML Template:
<!DOCTYPE html>
<html>
<head>
<title>Dashboard</title>
<style>
body { font-family: system-ui; background: #1a1a2e; color: #fff; padding: 2rem; }
.card { background: rgba(255,255,255,0.05); border-radius: 12px; padding: 1.5rem; margin: 1rem 0; }
.value { font-size: 3rem; color: #00d9ff; }
.label { color: #888; font-size: 0.9rem; }
</style>
</head>
<body>
<h1>π Real-time Dashboard</h1>
<!-- Simple data-bind -->
<div class="card">
<div class="label">Current Value</div>
<div class="value" data-bind="value">--</div>
</div>
<!-- Custom JS formatting -->
<div class="card">
<div class="label">Temperature</div>
<div class="value" id="temp-display">--</div>
</div>
<!-- Status with conditional styling -->
<div class="card">
<div class="label">Status</div>
<div id="status-display">Waiting...</div>
</div>
<!-- History chart -->
<div class="card">
<div class="label">History</div>
<div id="chart" style="display:flex;gap:2px;height:60px;align-items:flex-end;"></div>
</div>
<script>
const history = [];
window.addEventListener('htmloutput:update', (e) => {
const data = e.detail;
// Format temperature
document.getElementById('temp-display').textContent =
data.temperature.toFixed(1) + 'Β°C';
// Status with color
const status = document.getElementById('status-display');
status.textContent = data.status === 'online' ? 'π’ Online' : 'π‘ Warning';
// Update history chart
history.push(data.value);
if (history.length > 20) history.shift();
document.getElementById('chart').innerHTML = history
.map(v => `<div style="flex:1;background:#00d9ff;height:${v}%;border-radius:2px;"></div>`)
.join('');
});
</script>
</body>
</html>Send messages at regular intervals with dynamic JavaScript payloads.
| Field | Description |
|---|---|
| Interval (ms) | Time between messages (default: 1000) |
| Payload | JavaScript expression that returns an object |
| Max Count | Stop after N messages (0 = infinite) |
// Random sensor data
{
value: Math.round(Math.random() * 100),
temperature: (20 + Math.random() * 15).toFixed(1),
timestamp: new Date().toISOString()
}
// Counter
{
count: Date.now() % 1000,
tick: true
}
// Simulated metrics
{
cpu: Math.random() * 100,
memory: 30 + Math.random() * 50,
requests: Math.floor(Math.random() * 1000)
}Connect to various AI providers using the Vercel AI SDK.
- OpenAI Compatible - Any OpenAI-compatible API
- DeepSeek - DeepSeek AI models
- OpenRouter - Access multiple models via OpenRouter
- Zhipu - Zhipu AI (GLM models)
- Click "Add New AI Config"
- Select a preset or enter custom settings
- Enter your API key
- Select the model
Use {{path}} syntax to inject message data:
Summarize this text: {{payload.text}}
User: {{payload.question}}
Context: {{payload.context}}
| Endpoint | Method | Description |
|---|---|---|
/api/workflow/deploy |
POST | Deploy a workflow |
/api/workflow/undeploy |
POST | Undeploy a workflow |
/api/workflow/stop |
POST | Stop intervals without undeploy |
/api/workflow/deployed |
GET | List deployed workflows |
/api/workflow/inject |
POST | Trigger an inject node |
/api/status |
GET | Get server status |
/api/metrics |
GET | Get execution metrics |
/:slug/ui |
GET | Serve HTML output page |
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
β NodeFlow Server β
β (Bun + React) β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ€
β βββββββββββ βββββββββββ βββββββββββ βββββββββββ β
β β HTTP In β β MQTT β β WS β β Intervalβ β
β β :3001 β β :1883 β β :1884 β β Timer β β
β ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ ββββββ¬βββββ β
β β β β β β
β ββββββββββββββ΄βββββββββββββ΄βββββββββββββ β
β β β
β ββββββββββββΌβββββββββββ β
β β Workflow Engine β β
β β (Node Executor) β β
β ββββββββββββ¬βββββββββββ β
β β β
β βββββββββββββββββββΌββββββββββββββββββ β
β β β β β
β ββββββΌβββββ βββββββΌββββββ ββββββΌβββββ β
β β Debug β β HTML Out β β MQTT Outβ β
β β Logs β β /slug/ui β β Publish β β
β βββββββββββ βββββββββββββ βββββββββββ β
βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Run in development mode
bun run dev
# Run tests
bun test
# Type check
bun run lintMIT