Skip to content

Repository files navigation

FlowForge

A visual workflow builder with a simulated execution engine — 100% client-side, zero backend.

CI

https://github.com/user-attachments/assets/demo-placeholder

⬇ Replace this with your 10-second GIF — see Recording the demo

![FlowForge demo](docs/demo.gif)

What is this?

FlowForge lets you compose automation workflows on an infinite canvas: drag nodes (webhook trigger, AI prompt transformer, conditional branch, HTTP request), wire them into a directed graph, configure each step live, then hit Run and watch execution flow through the graph node-by-node with a payload inspector.

There is no server. Every hard problem here — cycle prevention, undo/redo, execution scheduling, schema validation — is solved in the browser, in TypeScript, with tests.

Features

  • Drag-and-drop canvas (@xyflow/react) with 4 custom node types and per-type config panels that update without re-rendering sibling nodes
  • DFS cycle prevention — circular connections are blocked at drag time, with the offending path surfaced in the UI (b → a → b)
  • Time-travel debugging — hand-written command-pattern history engine; every mutation (add, move, connect, edit, delete) is one atomic undo step; consecutive config edits coalesce into a single step
  • Execution simulator — event-driven runner animates idle → running → success/failed/skipped states, routes branches by evaluated conditions, supports fail-fast and mid-run cancellation
  • Payload inspector — pretty-printed input/error/output for any executed node
  • Import/Export + autosave — versioned Zod schemas gate every byte entering the canvas; LocalStorage persistence survives refreshes

Architecture

Two Zustand stores with deliberately separated lifecycles:

flowchart LR
    subgraph UI["React components"]
        Canvas["Canvas · Nodes"]
        Panels["Config panel<br/>Inspector"]
        Toolbar["Toolbar"]
    end

    subgraph FS["useFlowStore · document domain"]
        Graph[("nodes · edges<br/>selection")]
        History["Command history<br/>past ⟷ future"]
    end

    subgraph ES["useExecutionStore · runtime domain"]
        Status["phase · nodeStatuses"]
        Payloads[("inputs · outputs<br/>errors")]
    end

    Runner["WorkflowRunner<br/>(event callbacks)"]

    Toolbar -- "commit(command)" --> History
    History -- "do()/undo()" --> Graph
    Canvas -- "onNodesChange/onConnect" --> Graph
    Graph -- "snapshot" --> Runner
    Runner -- "events" --> Status
    Status --> Payloads
    Graph -- "memoized selectors" --> Canvas
    Payloads -- "selectors" --> Panels
Loading

Key decisions:

Decision Why
React Flow is a dumb renderer The store owns nodes/edges; RF changes are applied through it. One source of truth.
Commands over snapshots Each history entry closes over only what changed ({ do, undo }), so memory stays flat and deletes are trivially reversible
Separate execution store Run state mutates constantly during a run; keeping it out of the document store keeps undo semantics clean
Memoized custom nodes Editing one node's config replaces exactly one node object → React.memo blocks every other node from re-rendering

Cycle prevention (the DFS)

Before any edge source → target is committed, we ask one question: can target already reach source?

export function createsCycle(adjacency: AdjacencyList, sourceId: string, targetId: string): CycleReport {
  if (sourceId === targetId) return { hasCycle: true, cyclePath: [sourceId, targetId] }
  const pathBack = findsPath(adjacency, targetId, sourceId)
  if (!pathBack) return NO_CYCLE
  return { hasCycle: true, cyclePath: [sourceId, ...pathBack] }
}

findsPath is depth-first search with backtracking and a visited set — O(V + E) time, O(V) space, and it returns the actual path so the UI can show Cycle blocked · b → a → b instead of a generic error.

The same module also exports findCycle, a three-color DFS (white/grey/black) used to validate whole imported documents — because a file can contain cycles even though the editor can never create them:

Function Runs when Complexity
createsCycle every connection attempt (incremental guard) O(V+E) worst case
findCycle import / schema parsing (full-graph audit) O(V+E)

Full source: src/lib/validation/dag.ts · tests: dag.test.ts

Tech stack

Layer Choice
Framework Vite · React 19 · TypeScript (strict)
Canvas @xyflow/react v12
State Zustand (+ hand-written temporal command engine)
Validation Zod v4 (discriminated unions, versioned envelopes)
Styling Tailwind CSS v4 · shadcn/ui · Radix primitives · lucide icons
Testing Vitest · Testing Library · jsdom

Quickstart

npm install
npm run dev       # http://localhost:5173
npm test          # 78 unit/integration tests
npm run build     # typecheck + production bundle

Testing philosophy

Tests are written against the modules that carry logic, not against pixels:

  • lib/validation/dag.test.ts — self-loops, back-edges across chains, diamonds, disconnected cyclic subgraphs
  • stores/flow-store-history.test.ts — coalescing, atomic delete+restore, redo-stack invalidation, click-without-move
  • lib/engine/runner.test.ts — branch routing both ways, fail-fast ordering, fan-out dedup, cancellation gates, cyclic-input survival
  • lib/persistence/io.test.ts — round-trips, malformed configs with named paths, duplicate ids, ghost edges, imported cycles
  • lib/persistence/storage.test.ts — debounce timing, corrupt payloads, schema drift

Project structure

src/
├── components/
│   ├── canvas/          # React Flow wrapper, palette, notices
│   ├── nodes/           # BaseNodeShell + 4 typed nodes
│   ├── panels/          # Config panel, payload inspector
│   ├── toolbar/         # Import/export controls
│   └── ui/              # shadcn primitives
├── lib/
│   ├── engine/          # WorkflowRunner + payload math
│   ├── history/         # Command-pattern temporal engine
│   ├── persistence/     # Zod schema, IO, localStorage
│   └── validation/      # DAG utilities (DFS)
├── stores/              # useFlowStore, useExecutionStore
├── types/               # Node registry & contracts
└── hooks/               # Shortcuts, persistence

Recording the demo

  1. Build this storyboard first: (drag trigger → wire branch → run → cycle-block attempt → Ctrl+Z) — 10 seconds, no dead air
  2. Record at 1200×700 with ScreenToGif (Windows), Kap (macOS) or ffmpeg
  3. Export as docs/demo.gif, keep it under ~4 MB (ffmpeg -i in.gif -vf "fps=12,scale=900:-1" out.gif)
  4. Swap the placeholder embed above for ![FlowForge demo](docs/demo.gif)

Deployment

Static SPA — deploys anywhere:

  • Vercel: import repo → framework auto-detected → done
  • Netlify: build npm run build, publish dist
  • GitHub Pages: vite build --base=/<repo-name>/ and publish dist

CI (.github/workflows/ci.yml) type-checks, builds and runs the full suite on every push and PR.

License

MIT

About

Visual Workflow Builder with Simulated Execution Engine

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages