Self-hosted Zapier-style workflow editor. Build automations visually from triggers, actions, transforms, and AI steps. Next.js full-stack — the UI, the API, the execution engine, the scheduler, and the SQLite database all live in one process.
Visual editor Execution engine
(canvas + node palette) (topo-sorted DAG runner)
│ │
▼ ▼
/api/workflows ─────► workflows.graph_json (nodes + edges)
│
/api/webhooks/:token ◄───┐ ▼
/api/workflows/:id/run ◄──┤ for each node in topological order:
│ resolve {{ steps.X.path }} templates
node-cron scheduler ─────┘ run node implementation (lib/nodes/*)
persist run_nodes row
│
▼
SQLite (WAL)
runs, run_nodes, webhook_log
- Webhook — opens a public URL at
/api/webhooks/<token>. Any POST body becomes the trigger payload. - Schedule — fires on a 5-field UNIX cron expression. The scheduler is a background task running inside the Next.js process.
- Manual run — fired by hitting "Test run" in the editor.
- HTTP Request — any method, headers, body. Returns
{ status, ok, headers, body }. - Send Message — POSTs
{ text }to a webhook URL (Slack-compatible out of the box). - Log — writes a structured line into the run trace.
- Template — string with
{{ steps.X.path }}placeholders, rendered before the node runs. - Transform (JS) — body of a function:
input,steps,triggerare in scope. Sandboxed vianew Function(...)— local-only, do not trust user code from outside.
- AI Generate — free-form prompt, returns
{ text }. - AI Summarize — concise summary with configurable max words + style.
- AI Classify — assign to one of N declared categories with confidence.
- AI Extract — pull structured fields out of free text per a JSON shape.
Every config field that's a string runs through a template resolver before the node executes:
{{ trigger.body.email }}
{{ steps.n_extract.email }}
{{ steps["n_request"].body.items.0.title }}
Resolution is dot/bracket paths against a single scope { trigger, steps }.
Undefined paths render as empty string. Non-string values are JSON-stringified.
1. topo-sort the workflow graph (cycles are rejected)
2. for each node in order:
a. compute input = output of the node's incoming edge (trigger payload for triggers)
b. resolve {{ ... }} templates inside the node's config against `steps`
c. await node.run({ input, config, steps, trigger, log })
d. persist the node's input/output/status into `run_nodes`
3. mark the run succeeded; or, on the first failure, mark remaining nodes
"skipped" and the run "failed"
Requires Node 18+. OpenAI key only needed if you use AI nodes.
cp .env.example .env
# Edit .env: set SESSION_SECRET to something long+random; add OPENAI_API_KEY
# if you want to run AI nodes.
npm install
npm run seed # creates 2 demo workflows + sets passcode to "0000"
npm run devOpen http://localhost:4848, enter passcode 0000. Two workflows are seeded:
- Support email triage (enabled) — POST to
http://localhost:4848/api/webhooks/support-intakewith{ "text": "..." }→ AI classifies urgency → posts to a Slack-compatible webhook (placeholder — edit the URL in the editor). - Minute heartbeat (disabled by default) — every minute, render a template, write a log line. Enable it from the Workflows page to see the scheduler in action.
Try it from the workflow detail page: hit Test run with no trigger
payload, watch the per-node trace appear at /runs/<id>.
- Add node — the floating + button. Categorised palette: triggers, actions, transforms, AI.
- Wire two nodes — click the cyan dot at the bottom of the source node, then click anywhere on the target node.
- Remove an edge — click on its curved line.
- Drag the node header to reposition. The right panel edits the selected node's config.
- Save persists; Test run triggers the workflow manually with
whatever body you typed (default
{}).
app/
(app)/ authenticated panel
workflows/ list + [id]/editor
runs/ list + [id]/detail
webhooks/ active URLs + recent payloads
settings/ static info
api/
auth/ passcode + cookie
workflows[/[id][/run]] CRUD + manual trigger
runs[/[id]] list + detail
webhooks/[token] PUBLIC trigger receiver
webhook-log admin view of recent calls
node-catalog schema of every node kind (for the editor)
lock/ unlock screen
globals.css, layout.tsx, page.tsx
components/
Modal.tsx, ConfirmModal.tsx, Sidebar.tsx
editor/
Editor.tsx canvas, drag, connections
NodePalette.tsx cmd-palette-style node picker
NodeForm.tsx right-rail config form (renders from FieldSpec)
lib/
db.ts SQLite WAL + migrations
auth.ts passcode + signed cookie
api-helpers.ts handle() wrapper
client-api.ts fetch wrapper
engine.ts THE execution engine + variable resolver
scheduler.ts node-cron registrar
nodes/ one file per category + index registry
types.ts, format.ts
scripts/seed.ts demo data
- One big graph_json on the workflow row. Per-node tables are normal in larger systems but for a few KB of JSON on each workflow, denormal saves a lot of join code and keeps the editor's save path one row.
- Inline scheduler in-process. Acceptable for a single-node Next.js deployment. For HA / multi-replica you'd want a leader election so only one process schedules. The hooks are obvious (DB-backed leader lock).
- JS sandbox is
new Function(). Real sandboxing needs an isolate (VM2, isolated-vm, deno_core, etc.). Conduit is self-hosted single-user — the threat model is "you write a bug", not "untrusted user code". - Webhook URLs are bearer-by-obscurity. Anyone with the URL can trigger.
If you need stronger auth, add an
X-Conduit-Secretheader check inside a Transform JS node at the start of the workflow. - AI nodes call OpenAI directly. No retry/timeout/cost limits yet — the hooks are obvious (per-node max tokens, per-workspace daily cap).
MIT.