Back to .md Directory

🧠 Zenode β€” AI Coding Prompt (Cursor / Claude Code / Copilot)

Paste this entire prompt at the start of your Cursor session or as a persistent `.cursorrules` file in your project root.

May 2, 2026
0 downloads
1 views
ai prompt claude cursor copilot workflow
View source

🧠 Zenode β€” AI Coding Prompt (Cursor / Claude Code / Copilot)

Paste this entire prompt at the start of your Cursor session or as a persistent .cursorrules file in your project root.


🎯 End Goal β€” What Zenode Becomes

Zenode is a modular JavaScript ecosystem for visual workflows.

Each package solves an independent problem. All packages work better together. Developers adopt one package and expand naturally. The full stack is more powerful than any individual part.

"Use just the canvas. Use just the runtime.
 Use the full stack. Each part works standalone
 and integrates with your existing tools."

This is the same model as TanStack, Prisma, and Babel β€” modular by default, opinionated when you want it.


πŸ“¦ The Zenode Ecosystem β€” Package Architecture

@zenode/designer       ← Layer 1 β€” Visual canvas only
@zenode/serializer     ← Layer 2 β€” Export/import formats only
@zenode/runtime        ← Layer 3 β€” Workflow execution engine only
@zenode/transformer    ← Layer 4 β€” Output shape transformation only

@zenode/core           ← All four layers, pre-wired, one install
@zenode/react          ← React wrapper around @zenode/designer
@zenode/vue            ← Vue wrapper around @zenode/designer
@zenode/bpmn-presets   ← Free BPMN 2.0 shape preset library

zenode.min.js          ← CDN/UMD bundle of @zenode/core

Why This Architecture Wins

Company A  β†’ Uses only @zenode/designer. Has their own execution engine.
             Exports Zenode JSON β†’ feeds their own backend.

Company B  β†’ Uses only @zenode/runtime. Has their own diagram tool (React Flow, etc).
             Imports their diagram JSON β†’ Zenode executes it.

Company C  β†’ Already uses BPMN.js. Has years of BPMN XML diagrams.
             @zenode/runtime imports and EXECUTES their existing BPMN XML.
             Zero diagram rebuild. Zero migration cost.
             ↑ This is a unique product. No JavaScript BPMN runtime exists.

Company D  β†’ Uses @zenode/core. Gets the full stack, tightest integration.

Interoperability Contract

Every package speaks the same JSON dialect. Clean boundaries:

// @zenode/designer β†’ outputs standard ZenodeState JSON
const diagramState = engine.toJSON();

// @zenode/serializer β†’ converts from/to any format
import { toBPMN, toMermaid, fromBPMN } from '@zenode/serializer';
const bpmnXml = toBPMN(diagramState);

// @zenode/runtime β†’ executes ANY compatible JSON (not just Zenode diagrams)
import { ZenodeRuntime } from '@zenode/runtime';
const runtime = ZenodeRuntime.fromBPMN(existingBpmnXml); // works with 3rd party XML

// @zenode/transformer β†’ shapes any execution result
import { createTransformer } from '@zenode/transformer';
const output = createTransformer(result => ({ ok: true, data: result.finalOutput }));

πŸ—Ί Four-Layer System

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚  LAYER 1 β€” @zenode/designer  (Visual Designer)             β”‚
β”‚  D3.js canvas, shapes, connections, context pad, plugins   β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  LAYER 2 β€” @zenode/serializer  (Serialization)             β”‚
β”‚  JSON, XML, BPMN 2.0, Mermaid, DOT, DSL export/import     β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  LAYER 3 β€” @zenode/runtime  (Execution Engine)             β”‚
β”‚  Converts diagram β†’ executable workflow, runs resolvers    β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚  LAYER 4 β€” @zenode/transformer  (Output Shaping)           β”‚
β”‚  User defines their own output object from execution resultβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

πŸ’° Pricing Per Package

PackageFreePaid
@zenode/designerCore canvas, shapes, basic connectionsPro $149/yr β€” minimap, themes, undo+, PNG export
@zenode/serializerJSON, Mermaid, DOT, DSLPro $99/yr β€” BPMN 2.0, image export
@zenode/runtime100 executions/dayRuntime $49/mo β€” unlimited executions
@zenode/core (bundle)All free tiersBundle $199/yr β€” all pro features
@zenode/bpmn-presetsAlways freeβ€”
Embed Licenseβ€”$200/mo β€” OEM/white-label embed
Enterprise Runtimeβ€”$500/mo β€” production SLA + support

Runtime pricing scales with value extracted. A startup running 50 workflows/day pays nothing. An enterprise running 50,000/day pays appropriately.


πŸ†š Competitive Map

React FlowBPMN.jsJointJS+n8nZenode
Framework agnosticβŒβœ…βœ…N/Aβœ…
Open source coreβœ…βœ…βŒβœ…βœ…
Custom shapes APIβš οΈβŒβœ…βŒβœ…
BPMN exportβŒβœ…βŒβŒβœ… Pro
JSON exportβœ…βŒβœ…βœ…βœ…
JS workflow executionβŒβŒβŒβœ…βœ…
Live node statusβŒβŒβŒβœ…βœ…
Plugin context pad❌❌ hardcodedβŒβŒβœ…
Visual effectsβŒβŒβŒβŒβœ…
Modular packagesβŒβŒβŒβŒβœ…
Embeddable UMDβš οΈβŒβš οΈβŒβœ…
PriceFree/ProFree$1200+/yrFree/CloudFree/$149/yr

The unique moat: No JavaScript library executes BPMN diagrams at runtime. Camunda has a Java runtime. Flowable has a Java runtime. @zenode/runtime that executes BPMN XML from any source β€” including BPMN.js diagrams β€” is a completely unclaimed space.


πŸš€ Immediate Focus β€” Ship Layer 1 First

Current task: Get @zenode/designer live and production-ready. Everything else is future. The designer is the entry point for every other package.

Why Layer 1 First

Layer 1 live β†’ GitHub stars β†’ developer adoption
             β†’ NPM downloads β†’ credibility
             β†’ Real user feedback β†’ better Layer 2 API design
             β†’ Potential acqui-hire interest starts here

Layer 3 alone β†’ No distribution channel, no one finds it
Layer 2 alone β†’ No demo, no one understands what it serializes

The designer is your marketing, your demo, your distribution channel, and your feedback loop all in one. Everything else depends on it having real users.

Layer 1 Ship Checklist

⬜ Context Action Pad complete (Phase L1·3)
⬜ Connection markers/arrowheads (Phase L1·2+)
⬜ Basic undo/redo (10 steps free β€” enough for launch)
⬜ README with working GIF and 10-line quick start
⬜ Live demo deployed (Vercel/Netlify β€” link in README)
⬜ NPM publish @zenode/designer v0.1.0
⬜ Show HN post

That is the complete v0.1.0 launch scope. Everything else comes after users.


🧩 Config Management β€” Keep It Small

// βœ… CORRECT β€” small config, API-first
const engine = new ZenodeEngine('#canvas', {
  canvas:     { width: 800, height: 600, backgroundColor: '#fff' },
  grid:       { type: 'dotted', size: 20, color: '#ccc' },
  theme:      'dark',
  contextPad: { trigger: 'select', position: 'top-right' }
});
engine.addNode({ preset: 'bpmn-task', x: 100, y: 200, meta: { label: 'Review' } });
engine.addEdge({ from: 'node-1', fromPort: 'right', to: 'node-2', toPort: 'left' });

// ❌ WRONG β€” never declare node instances inside config
const engine = new ZenodeEngine('#canvas', {
  shapes: { default: { rectangle: [{ id: 'task0', width: 120 ... }] } }
});

Config = canvas environment only. Nodes/edges added via API. Styles via presets.


🧩 Shape Renderer Contract β€” Core Plugin Architecture

⚠️ This is the foundation everything else is built on. Set up before Phase 1.3. Never bypass.

// src/types/index.ts
export interface ShapeRenderer {
  draw:      (g: D3Selection, config: ResolvedShapeConfig, theme: ThemeConfig) => void;
  getPath:   (config: ResolvedShapeConfig) => string;
  getBounds: (config: ResolvedShapeConfig) => BoundingBox;
  getPorts:  (config: ResolvedShapeConfig) => PortMap;
}
export interface BoundingBox { x: number; y: number; width: number; height: number; }
export interface PortMap {
  top: Point; bottom: Point; left: Point; right: Point; center: Point;
  [key: string]: Point; // custom ports
}
type Point = { x: number; y: number };

Overlay β€” Always Use getPath()

// ❌ WRONG β€” ignores actual geometry
g.append('rect').attr('width', node.width).attr('height', node.height);

// βœ… CORRECT β€” matches exact shape regardless of renderer
g.append('path')
  .attr('d', registry.get(node.type).getPath(node))
  .attr('fill', 'none').attr('stroke', overlayColor)
  .attr('pointer-events', 'none'); // ← always none on overlays

Rectangle Renderer β€” Reference (per-corner borderRadius)

export const RectangleRenderer: ShapeRenderer = {
  draw(g, config) {
    g.append('path').attr('d', this.getPath(config))
      .attr('fill', config.color).attr('stroke', config.stroke.color)
      .attr('stroke-width', config.stroke.width)
      .attr('stroke-dasharray', config.stroke.strokeDasharray.join(','));
  },
  getPath(config) {
    const { x, y, width: w, height: h, borderRadius: r } = config;
    const tl=r?.leftTop??0, tr=r?.rightTop??0, bl=r?.leftBottom??0, br=r?.rightBottom??0;
    return [`M ${x+tl} ${y}`,`H ${x+w-tr}`,`Q ${x+w} ${y} ${x+w} ${y+tr}`,
            `V ${y+h-br}`,`Q ${x+w} ${y+h} ${x+w-br} ${y+h}`,
            `H ${x+bl}`,`Q ${x} ${y+h} ${x} ${y+h-bl}`,
            `V ${y+tl}`,`Q ${x} ${y} ${x+tl} ${y}`,'Z'].join(' ');
  },
  getBounds(c) { return { x:c.x, y:c.y, width:c.width, height:c.height }; },
  getPorts(c) {
    return {
      top:{x:c.x+c.width/2,y:c.y}, bottom:{x:c.x+c.width/2,y:c.y+c.height},
      left:{x:c.x,y:c.y+c.height/2}, right:{x:c.x+c.width,y:c.y+c.height/2},
      center:{x:c.x+c.width/2,y:c.y+c.height/2}
    };
  }
};

Plugin System

export interface ZenodePlugin { name: string; install(engine: ZenodeEngine): void; }
engine.use(MyPlugin);
// engine.registerShape() + engine.use() being public IS the plugin system.
// Not a future feature β€” the natural result of correct Phase 1 architecture.

βœ… What Is Already Built β€” Do Not Rebuild

Core

  • D3.js SVG canvas: zoom, pan, scroll, double-click reset, configurable extents
  • Grid: dotted/sheet/line/cross, color, transparency, size
  • ZenodeEngine class: canvas init, element management, state
  • Shape preview with real-time grid snapping
  • Shape placement: mouseClick β†’ placedNodes[] β†’ g.placed-nodes
  • Config merger with typed defaults

Shape System

  • ShapeRenderer contract: draw, getPath, getBounds, getPorts
  • ShapeRegistry β€” src/nodes/registry.ts
  • RectangleRenderer, CircleRenderer, RhombusRenderer all implemented
  • engine.registerShape() exposed
  • Overlay via renderer.getPath() β€” src/nodes/overlay.ts
  • roundedRectPath() β€” src/nodes/geometry/rectanglePath.ts

Interaction

  • Selection: single, lasso, Escape/Delete, configurable keyboard shortcuts
  • Visual Effects: VisualState, applyEffects(), Glow + Stroke Flow + Gradient
  • Alignment guides: renderer-bounds-aware, RAF-throttled, config-driven
  • g.lasso layer in canvas

Connections

  • Ports via renderer.getPorts(), hover circles, crosshair cursor
  • Drawing: ghost line, drop to finalise, stored in connections[]
  • Types: straight, curved, s-shaped, l-bent
  • Smart routing (Pro, gated via LicenseManager)
  • Labels: pill-styled, double-click inline edit
  • Navigation controls: floating zoom/focus buttons

πŸ— Build Plan β€” Layer by Layer


LAYER 1 β€” @zenode/designer

The D3.js visual canvas. The entry point for the entire ecosystem. ⚑ Ship this first. Everything else depends on it having real users.


L1 Β· Phase 1 β€” Core Shape Lifecycle βœ… COMPLETE

  • Shape placement, drag & drop, grid snapping
  • Selection, lasso, keyboard shortcuts
  • Visual Effects System (Glow, Stroke Flow, Gradient)
  • Alignment guides

L1 Β· Phase 2 β€” Connections βœ… COMPLETE

  • Ports via renderer.getPorts()
  • Drawing connections, ghost line
  • Types: straight, curved, s-shaped, l-bent
  • Smart routing (Pro)
  • Connection labels, inline edit
  • Navigation controls

L1 Β· Phase 2+ β€” Connection Markers ⬜

  • Arrowheads via SVG <marker>: none, arrow, filled-arrow, diamond, circle
  • Per-connection color, width, dash pattern
  • Animated flow direction via Visual Effects strokeAnimation

L1 Β· Phase 3 β€” Context Action Pad πŸ”§ IN PROGRESS

The floating toolbar near selected nodes/edges. Plugin-extensible from day one. Modelled after BPMN.js's context pad. One of the most visible UX differentiators.

  β”Œβ”€β”€β”¬β”€β”€β”¬β”€β”€β”¬β”€β”€β”¬β”€β”€β”
  β”‚πŸ—‘β”‚βš‘β”‚βœοΈβ”‚βš™β”‚βž•β”‚   ← HTML overlay. Never inside SVG.
  β””β”€β”€β”΄β”€β”€β”΄β”€β”€β”΄β”€β”€β”΄β”€β”€β”˜

Types β€” src/types/index.ts

export type ContextPadTarget =
  | { kind: 'node'; id: string; data: NodeData }
  | { kind: 'edge'; id: string; data: EdgeData };

export interface ContextPadAction {
  id:          string;
  icon:        string;           // inline SVG string
  tooltip:     string;
  targets?:    Array<'node' | 'edge'>;   // omit = both
  appliesTo?:  string[];                 // specific node/edge types
  group?:      'primary' | 'secondary' | 'danger' | string;
  handler:     (target: ContextPadTarget, engine: ZenodeEngine) => void;
  isVisible?:  (target: ContextPadTarget, engine: ZenodeEngine) => boolean;
  isDisabled?: (target: ContextPadTarget, engine: ZenodeEngine) => boolean;
}

export interface ContextPadConfig {
  enabled:          boolean;
  trigger:          'hover' | 'select';
  offset:           { x: number; y: number };
  position:         'top-right' | 'top-left' | 'bottom-right' | 'bottom-left' | 'top-center' | 'bottom-center';
  showDefaults:     boolean;
  suppressDefaults?: string[];
}

Registry β€” src/contextpad/registry.ts

export class ContextPadRegistry {
  private actions = new Map<string, ContextPadAction>();
  register(action: ContextPadAction): void { this.actions.set(action.id, action); }
  unregister(id: string): void { this.actions.delete(id); }
  getActionsFor(target: ContextPadTarget, engine: ZenodeEngine): ContextPadAction[] {
    return [...this.actions.values()]
      .filter(a => {
        if (a.targets && !a.targets.includes(target.kind)) return false;
        if (a.appliesTo && !a.appliesTo.includes(target.data.type)) return false;
        if (a.isVisible && !a.isVisible(target, engine)) return false;
        return true;
      })
      .sort((a, b) => (a.group ?? 'primary').localeCompare(b.group ?? 'primary'));
  }
}

Default Actions β€” src/contextpad/defaults.ts

delete (both), connect (nodes), edit-label (both), properties (both), duplicate (nodes), change-color (both) β€” all suppressible.

Renderer β€” src/contextpad/renderer.ts

HTML <div> overlay. Position = canvas bbox β†’ D3 zoom transform β†’ SVG clientRect offset.

show(target, actions, config): void   // render buttons
hide(): void
reposition(transform: ZoomTransform): void  // RAF-throttled β€” called on every zoom/pan

Critical Rules

  1. HTML overlay, not SVG β€” position: absolute div over the SVG wrapper parent
  2. Position always from D3 zoom transform β€” never cache screen coordinates
  3. Hides on: drag start, canvas click, node:deleted, edge:deleted, Escape
  4. isVisible/isDisabled called every render β€” live engine state, never memoized
  5. Actions are stateless β€” (target, engine) args only, never close over stale data
  6. Plugins can add AND remove β€” engine.unregisterContextAction() is first-class API

Engine Methods

engine.registerContextAction(action: ContextPadAction): void
engine.unregisterContextAction(id: string): void
engine.showContextPad(targetId: string, kind: 'node' | 'edge'): void
engine.hideContextPad(): void
engine.startConnectionFrom(nodeId: string): void
engine.beginLabelEdit(id: string, kind: 'node' | 'edge'): void
engine.duplicateNode(id: string): string

engine.on('contextpad:open',       ({ target }) => {})
engine.on('contextpad:close',      () => {})
engine.on('contextpad:action',     ({ actionId, target }) => {})
engine.on('contextpad:properties', ({ target }) => {})

Plugin Pattern

engine.use({
  name: 'my-workflow-plugin',
  install(engine) {
    engine.registerContextAction({
      id: 'run-node', icon: `<svg>...</svg>`, tooltip: 'Run this step',
      targets: ['node'], appliesTo: ['http-request', 'script'],
      group: 'primary',
      handler:    (t, e) => e.setNodeStatus(t.id, 'running'),
      isDisabled: (t, e) => e.getNode(t.id)?.meta?.locked === true
    });
    engine.unregisterContextAction('duplicate');
  }
});

L1 Β· Phase 4 β€” Swimlanes / Pools / Lanes ⬜

engine.addNode({ type: 'pool', label: 'Order Process', orientation: 'horizontal' });
engine.addNode({ type: 'lane', label: 'Sales Team', parentId: 'pool-1' });
  • g.swimlanes layer β€” below g.placed-nodes
  • Child positions clamped to parent lane bounds during drag
  • SwimLaneRenderer implements ShapeRenderer
  • Cross-lane connection ports on swimlane boundary
  • Validation rule: no-orphan-swimlane

L1 Β· Phase 5 β€” BPMN Feature Parity ⬜

Bringing Zenode to full BPMN.js feature parity:

  • Copy/paste β€” engine.copyNodes() / engine.pasteNodes() β€” Ctrl+C/V
  • Resize handles β€” 8-point handles, snap to grid, ports recompute after resize
  • Node search β€” engine.search(query) β€” searches label, type, meta
  • Node groups β€” engine.groupNodes() / engine.ungroupNodes()
  • Color picker β€” change-color context pad action, persisted in state
  • BPMN presets β€” @zenode/bpmn-presets free package (events, tasks, gateways, containers)
  • Toon theme β€” themes/toon.css β€” 2-hour effort, high marketing value

L1 Β· Phase 6 β€” Full Public Designer API ⬜

Node API

engine.addNode(config: NodeConfig): string
engine.removeNode(id: string): void
engine.updateNode(id: string, patch: Partial<NodeConfig>): void
engine.getNode(id: string): NodeData | null
engine.getAllNodes(): NodeData[]
engine.getNodesInViewport(): NodeData[]    // viewport culling for performance
engine.focusNode(id: string): void
engine.highlight(id: string, durationMs?: number): void
engine.duplicateNode(id: string): string
engine.copyNodes(ids: string[]): void
engine.pasteNodes(offset?): string[]
engine.groupNodes(ids: string[]): string
engine.ungroupNodes(groupId: string): void
engine.lock(): void
engine.unlock(): void
engine.setReadOnly(bool: boolean): void

Edge API

engine.addEdge(config: EdgeConfig): string
engine.removeEdge(id: string): void
engine.getEdge(id: string): EdgeData | null
engine.getAllEdges(): EdgeData[]
engine.getPath(fromNodeId: string, toNodeId: string): string[]

Live Node Status (Free β€” nobody else has this)

engine.setNodeStatus(id: string, status: 'idle'|'running'|'success'|'error'|'warning'): void
// running β†’ animated pulse ring
// success β†’ green checkmark badge
// error   β†’ red border + shake
// warning β†’ amber badge

Validation (Free)

engine.addRule(rule: BuiltInRule | CustomRule): void
// built-ins: 'no-cycles', 'all-nodes-connected', 'single-entry-point',
//            'single-exit-point', 'max-depth', 'no-orphan-swimlane'
engine.validate(): ValidationResult  // { valid, errors, warnings }

Canvas API

engine.clear(): void          engine.reset(): void
engine.zoomTo(level): void    engine.panTo(x, y): void
engine.fitToScreen(): void    engine.getViewport(): ViewportState
engine.search(query): NodeData[]
engine.setTheme(theme): void  // ← Pro
engine.enableMinimap(): void  // ← Pro

Undo/Redo

  • Command pattern. Ctrl+Z / Ctrl+Shift+Z
  • Free: 10 steps. Pro: up to 200.

Events

engine.on('node:placed'|'node:moved'|'node:deleted'|'node:selected'|
          'node:click'|'node:doubleclick'|'node:config:change'|
          'edge:created'|'edge:deleted'|'workflow:change'|
          'validation:error'|'export:complete'|
          'contextpad:open'|'contextpad:close'|
          'contextpad:action'|'contextpad:properties'|
          'runtime:step'|'runtime:complete'|'runtime:error', handler)
engine.off(event, handler)
engine.once(event, handler)

LAYER 2 β€” @zenode/serializer

Converting designer state to every format that matters. Usable without the designer.


L2 Β· Phase 7 β€” Export / Import ⬜

// JSON β€” round-trip perfect, versioned
engine.toJSON(): ZenodeState     // version: "1.0"
engine.fromJSON(state): void

// Formats (Free)
engine.toXML(): string           // generic workflow XML
engine.toMermaid(): string       // GitHub README ready
engine.toDOT(): string           // Graphviz
engine.toDSL(): string           // Zenode DSL, version-controllable

// BPMN 2.0 (Pro) β€” the enterprise unlock
engine.toBPMN(): string          // Camunda / Activiti compatible
engine.fromBPMN(xml): void       // import from any BPMN tool

// Image (Pro)
engine.toImage('png'|'svg'): Promise<Blob>

Read-only viewer embed β€” free, drives distribution:

<script src="https://cdn.zenode.dev/zenode.min.js"></script>
<div id="diagram"></div>
<script>Zenode.render('#diagram', workflowJSON, { readOnly: true });</script>

The round-trip:

Editor ──toJSON()──▢ DB ──fromJSON()──▢ Viewer (read-only)
                       ──toBPMN()──────▢ Camunda/Activiti
                       ──toMermaid()───▢ GitHub README
                       ──createRuntime()β–Ά Server execution

LAYER 3 β€” @zenode/runtime

The feature that separates Zenode from every diagramming library. The diagram IS the workflow logic. Change the diagram, change the behavior.


L3 Β· Phase 8 β€” Execution Runtime ⬜

Diagram JSON
    ↓ createRuntime({ resolvers, outputTransformer })
ExecutionGraph  (topological sort, port routing)
    ↓ runtime.execute(inputData)
StepRunner      (calls resolver per node, routes via port)
    ↓
ExecutionResult (per-node outputs, history, timing)
    ↓ outputTransformer(result)
Your output object

Runtime API

const runtime = engine.createRuntime({
  resolvers: {
    'http-request': async (node, input) => {
      const res = await fetch(node.meta.url, { method: node.meta.method, body: JSON.stringify(input.data) });
      return { data: await res.json() };
    },
    'condition': (node, input) => ({
      port: evaluateExpression(node.meta.expression, input.data) ? 'true' : 'false',
      data: input.data
    }),
    'transform':  (node, input) => ({ data: applyMapping(node.meta.mapping, input.data) }),
    'send-email': async (node, input) => {
      await emailService.send({ to: node.meta.recipient, body: template(node.meta.template, input.data) });
      return { sent: true };
    },
  },
  hooks: {
    onStepStart:    (nodeId) => engine.setNodeStatus(nodeId, 'running'),
    onStepComplete: (nodeId) => engine.setNodeStatus(nodeId, 'success'),
    onStepError:    (nodeId) => engine.setNodeStatus(nodeId, 'error'),
  },
  timeout:  30000,
  maxSteps: 1000,   // infinite loop guard
});

const output = await runtime.execute({ userId: '123' });

Diagram-as-Config (the core value proposition)

// Store diagram in DB. Load and run server-side without code changes.
// Business user changes the diagram β†’ behavior changes. No deployments.
const def     = await db.workflows.findById(id);
const engine  = ZenodeRuntime.fromJSON(def.definition);
const runtime = engine.createRuntime({ resolvers: myResolvers });
const result  = await runtime.execute(triggerData);

Runtime Types β€” src/runtime/types.ts

export interface RuntimeResolver {
  (node: NodeData, input: ResolverInput): Promise<ResolverOutput> | ResolverOutput;
}
export interface ResolverInput  { data: unknown; meta: Record<string, unknown>; context: ExecutionContext; }
export interface ResolverOutput { data: unknown; port?: string; error?: string; }
export interface ExecutionResult {
  finalStatus:  'success' | 'error' | 'timeout';
  finalOutput:  ResolverOutput | null;
  nodeHistory:  Array<{ id: string; type: string; input: unknown; output: unknown; ms: number }>;
  errors:       Array<{ nodeId: string; message: string }>;
  totalMs:      number;
}
export interface RuntimeConfig {
  resolvers:          Record<string, RuntimeResolver>;
  outputTransformer?: (result: ExecutionResult) => unknown;
  hooks?:             { onStepStart?(...): void; onStepComplete?(...): void; onStepError?(...): void; };
  timeout?:           number;
  maxSteps?:          number;
}

Workflow Simulation / Step Runner (Pro)

const ctrl = engine.simulate(inputData?);
ctrl.step(); ctrl.play(delayMs); ctrl.pause(); ctrl.reset();
ctrl.on('step', ({ nodeId, inputData, outputData, port }) => {});
// Visual: active node β†’ 'running', completed β†’ 'success'

LAYER 4 β€” @zenode/transformer

User defines exactly what comes out. Zenode never imposes a schema.


L4 Β· Phase 9 β€” Output Transformation ⬜

// The outputTransformer in RuntimeConfig IS Layer 4.
// It lives in @zenode/transformer as a standalone utility.

// Pattern 1: REST API response
outputTransformer: (r) => ({ ok: r.finalStatus === 'success', payload: r.finalOutput?.data, trace: r.nodeHistory.map(n=>n.id) })

// Pattern 2: Event sourcing
outputTransformer: (r) => r.nodeHistory.map(n => ({ event: `workflow.${n.type}.completed`, data: n.output }))

// Pattern 3: BPMN audit log
outputTransformer: (r) => ({ processId: workflowId, activities: r.nodeHistory.map(n => ({ activityId: n.id, status: 'completed' })) })

// Pattern 4: Pass-through
outputTransformer: (r) => r.finalOutput?.data

Schema-Driven Property Panels (connects Layer 1 to Layer 4)

// Node declares what data it needs. Zenode auto-renders the form.
// Values β†’ node.meta β†’ available to resolvers at runtime.
{
  type: 'http-request',
  schema: {
    url:     { type: 'string',   required: true,  label: 'Endpoint URL' },
    method:  { type: 'select',   options: ['GET','POST','PUT','DELETE'] },
    headers: { type: 'keyvalue', label: 'Headers' },
    body:    { type: 'textarea', label: 'Request Body' },
    timeout: { type: 'number',   default: 5000 }
  }
}
// Opens via contextpad:properties event β†’ auto-rendered panel.
// Emits node:config:change on every field change.

πŸ— Architecture Rules

TypeScript

  • Strict mode. No any. JSDoc on all public methods. Barrel export from src/types/index.ts.
  • ZenodeEngine = designer API. ZenodeRuntime = execution. Never mix them.

D3.js Layer Order (strict)

1. g.swimlanes     (below everything)
2. g.grid
3. g.connections
4. g.placed-nodes
5. g.preview       (pointer-events: none always)
6. g.guides        (pointer-events: none always)
7. g.lasso

Context pad: HTML overlay, never in SVG stack.

State

  • ZenodeEngine owns designer state. ZenodeRuntime owns execution state. Spread never mutate.
  • Emit typed events on every mutation.

CSS Variables

/* Canvas */      --zenode-canvas-bg  --zenode-grid-color
/* Nodes */       --zenode-node-bg  --zenode-node-border  --zenode-node-shadow
/* Interaction */ --zenode-selection-color  --zenode-guide-color  --zenode-port-color
/* Status */      --zenode-status-running  --zenode-status-success
                  --zenode-status-error    --zenode-status-warning
/* Edges */       --zenode-edge-color
/* Context Pad */ --zenode-contextpad-bg     --zenode-contextpad-border
                  --zenode-contextpad-radius  --zenode-contextpad-action-size
                  --zenode-contextpad-action-hover  --zenode-contextpad-action-active
                  --zenode-contextpad-danger  --zenode-contextpad-disabled
                  --zenode-contextpad-divider
/* Swimlanes */   --zenode-swimlane-bg  --zenode-swimlane-header-bg
                  --zenode-swimlane-border  --zenode-lane-divider

Performance

  • 500 nodes: getNodesInViewport() culling

  • 1000 nodes: OffscreenCanvas for static background

  • Guides + context pad: RAF-throttled
  • Connection recalc: only affected connections on node move
  • Runtime: async/await with timeout + maxSteps guard

πŸ“ File Structure

zenode/
β”œβ”€β”€ packages/
β”‚   β”œβ”€β”€ designer/               ← @zenode/designer (Layer 1)
β”‚   β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”‚   β”œβ”€β”€ core/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ engine.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ canvas.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ grid.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ history.ts
β”‚   β”‚   β”‚   β”‚   └── license.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ nodes/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ registry.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ placement.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ selection.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ overlay.ts        # Uses renderer.getPath β€” NEVER plain rect
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ resize.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ status.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ geometry/
β”‚   β”‚   β”‚   β”‚   β”‚   └── rectanglePath.ts
β”‚   β”‚   β”‚   β”‚   └── shapes/
β”‚   β”‚   β”‚   β”‚       β”œβ”€β”€ rectangle.ts
β”‚   β”‚   β”‚   β”‚       β”œβ”€β”€ circle.ts
β”‚   β”‚   β”‚   β”‚       β”œβ”€β”€ rhombus.ts
β”‚   β”‚   β”‚   β”‚       β”œβ”€β”€ swimlane.ts
β”‚   β”‚   β”‚   β”‚       └── group.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ contextpad/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ registry.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ renderer.ts      # HTML overlay β€” position from D3 zoom transform
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ defaults.ts
β”‚   β”‚   β”‚   β”‚   └── index.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ connections/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ manager.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ ports.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ drawing.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ markers.ts
β”‚   β”‚   β”‚   β”‚   └── paths/
β”‚   β”‚   β”‚   β”‚       β”œβ”€β”€ straight.ts
β”‚   β”‚   β”‚   β”‚       β”œβ”€β”€ curved.ts
β”‚   β”‚   β”‚   β”‚       β”œβ”€β”€ s-shaped.ts
β”‚   β”‚   β”‚   β”‚       └── l-bent.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ effects/
β”‚   β”‚   β”‚   β”‚   └── engine.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ validation/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ engine.ts
β”‚   β”‚   β”‚   β”‚   └── rules/
β”‚   β”‚   β”‚   β”œβ”€β”€ plugins/
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ manager.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ auto-layout.ts
β”‚   β”‚   β”‚   β”‚   β”œβ”€β”€ minimap.ts
β”‚   β”‚   β”‚   β”‚   └── collaboration.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ types/
β”‚   β”‚   β”‚   β”‚   └── index.ts         # ALL designer interfaces
β”‚   β”‚   β”‚   └── themes/
β”‚   β”‚   β”‚       β”œβ”€β”€ dark.css
β”‚   β”‚   β”‚       β”œβ”€β”€ light.css
β”‚   β”‚   β”‚       └── toon.css
β”‚   β”‚   └── package.json
β”‚   β”‚
β”‚   β”œβ”€β”€ serializer/             ← @zenode/serializer (Layer 2)
β”‚   β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”‚   β”œβ”€β”€ json.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ xml.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ bpmn.ts        # Pro
β”‚   β”‚   β”‚   β”œβ”€β”€ mermaid.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ dot.ts
β”‚   β”‚   β”‚   β”œβ”€β”€ dsl.ts
β”‚   β”‚   β”‚   └── image.ts       # Pro
β”‚   β”‚   └── package.json
β”‚   β”‚
β”‚   β”œβ”€β”€ runtime/                ← @zenode/runtime (Layer 3)
β”‚   β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”‚   β”œβ”€β”€ engine.ts      # ZenodeRuntime class
β”‚   β”‚   β”‚   β”œβ”€β”€ executor.ts    # Step-by-step execution
β”‚   β”‚   β”‚   β”œβ”€β”€ graph.ts       # Topological sort, port routing
β”‚   β”‚   β”‚   β”œβ”€β”€ simulation.ts  # Pro β€” visual step-through
β”‚   β”‚   β”‚   └── types.ts
β”‚   β”‚   └── package.json
β”‚   β”‚
β”‚   β”œβ”€β”€ transformer/            ← @zenode/transformer (Layer 4)
β”‚   β”‚   β”œβ”€β”€ src/
β”‚   β”‚   β”‚   β”œβ”€β”€ transformer.ts # outputTransformer factory
β”‚   β”‚   β”‚   β”œβ”€β”€ schema.ts      # Schema-driven property panels
β”‚   β”‚   β”‚   └── patterns.ts    # REST, event-sourcing, BPMN audit presets
β”‚   β”‚   └── package.json
β”‚   β”‚
β”‚   β”œβ”€β”€ core/                   ← @zenode/core (all four layers)
β”‚   β”‚   β”œβ”€β”€ src/index.ts       # re-exports all four packages, pre-wired
β”‚   β”‚   └── package.json
β”‚   β”‚
β”‚   └── bpmn-presets/           ← @zenode/bpmn-presets (free, always)
β”‚       β”œβ”€β”€ src/
β”‚       β”‚   └── presets.ts     # start-event, task, gateway, pool etc.
β”‚       └── package.json
β”‚
β”œβ”€β”€ examples/
β”‚   β”œβ”€β”€ custom-shapes/
β”‚   β”‚   β”œβ”€β”€ hexagon.ts
β”‚   β”‚   └── star.ts
β”‚   └── plugins/
β”‚       β”œβ”€β”€ automation-plugin.ts     # adds 'run-node' context pad action
β”‚       └── runtime/
β”‚           β”œβ”€β”€ approval-workflow.ts # designer β†’ execute β†’ output
β”‚           └── http-pipeline.ts
β”œβ”€β”€ demo/                           # Layer 1 demo app (ship first)
β”œβ”€β”€ tests/
β”œβ”€β”€ pnpm-workspace.yaml             # monorepo
└── package.json

πŸ§ͺ Before Writing Any Code β€” Ask These First

  1. Does this mutate engine state? β†’ Command pattern + emit event.
  2. Does this render something? β†’ D3 data join on the correct layer group.
  3. Does this need to be undoable? β†’ Yes if it changes nodes, connections, viewport, swimlane.
  4. Is this a premium feature? β†’ Gate via LicenseManager.check('feature-name').
  5. Does this need a config option? β†’ Add to src/types/index.ts with typed default.
  6. Does this change appearance? β†’ --zenode-* CSS variables β€” never hardcoded.
  7. Does this involve coordinates? β†’ d3.pointer(event, svgRoot), account for zoom transform.
  8. Does this draw an overlay, ring, or port? β†’ registry.get(node.type).getPath(node) β€” never plain rect.
  9. Is this a new shape? β†’ Implement full ShapeRenderer: draw, getPath, getBounds, getPorts.
  10. Does this add a button near a node/edge? β†’ engine.registerContextAction() β€” never hardcode.
  11. Does this involve context pad position? β†’ Always from D3 zoom transform β€” never cached screen coords.
  12. Does this involve workflow execution? β†’ ZenodeRuntime in packages/runtime/ β€” never in engine.ts.
  13. Does this transform execution output? β†’ outputTransformer in RuntimeConfig β€” never in runtime core.
  14. Is this a child of a swimlane? β†’ Position clamped to parent bounds, ports recomputed after move.
  15. Does this belong in a specific package? β†’ Designer UI = @zenode/designer. Formats = @zenode/serializer. Execution = @zenode/runtime. Output = @zenode/transformer.

πŸ—Ί Master Build Order

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  LAYER 1 β€” @zenode/designer  ← SHIP THIS FIRST
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
βœ… L1Β·P1  Core shapes (placement, drag, selection, effects, guides)
βœ… L1Β·P2  Connections (ports, drawing, types, routing, labels, nav)
⬜ L1·P2+ Connection markers (arrowheads, animated flow)
πŸ”§ L1Β·P3  Context Action Pad  ← IN PROGRESS
           └─ ContextPadRegistry + ContextPadRenderer (HTML overlay)
           └─ Default: delete, connect, edit-label, properties, duplicate, change-color
           └─ registerContextAction() / unregisterContextAction()
           └─ Position from D3 zoom transform, hides on drag/deselect/delete/Escape
           └─ Works for nodes AND edges
           └─ --zenode-contextpad-* CSS vars
⬜ L1·P4  Swimlanes (g.swimlanes layer, child clamping, cross-lane ports)
⬜ L1·P5  BPMN parity (copy/paste, resize handles, search, groups, presets, toon theme)
⬜ L1·P6  Full public API (status, validation, undo/redo, schema panels, DSL)

  ── @zenode/designer v0.1.0 LAUNCH ──
  └─ README + GIF + quick start
  └─ Live demo deployed
  └─ NPM publish
  └─ Show HN post        ← devto article #1: "Visual workflow designer, no framework needed"

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  LAYER 2 β€” @zenode/serializer
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⬜ L2·P7  JSON, XML, Mermaid, DOT, DSL (Free)
           BPMN 2.0 (Pro)              ← devto article #2: "Export any diagram to BPMN 2.0"
           Read-only viewer embed (Free)
           Image export (Pro)

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  LAYER 3 β€” @zenode/runtime
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⬜ L3·P8  ZenodeRuntime (topo sort, step executor, resolver API, port routing)
           Execution hooks β†’ setNodeStatus() visual sync
           Diagram-as-Config server-side pattern
           Simulation/step runner (Pro)
           BPMN XML execution from any source  ← devto article #3: "JS BPMN runtime"
           This article + HN post = acquisition interest starts

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  LAYER 4 β€” @zenode/transformer
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⬜ L4·P9  outputTransformer factory + reference patterns
           Schema-driven property panels (node.meta β†’ resolver input)
           Full round-trip demo: design β†’ serialize β†’ execute β†’ transform

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
  SHIP @zenode/core
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
⬜ @zenode/core bundle (all four layers, pre-wired)
⬜ Full demo app: designer + runtime demo + theme switcher (light/dark/toon)
⬜ zenode.min.js UMD (<120kb gzipped, iframe-safe)
⬜ Product Hunt launch  ← "The complete visual workflow ecosystem for JavaScript"
                          This is the moment. Four separate launch events built to this.

🧾 Context Snapshot β€” Source of Truth

βœ… Confirmed implemented

  • ShapeRenderer contract (draw, getPath, getBounds, getPorts) β€” src/types/index.ts
  • ShapeRegistry β€” src/nodes/registry.ts
  • Built-in renderers: rectangle.ts, circle.ts, rhombus.ts
  • engine.registerShape() exposed
  • Overlay via renderer.getPath() β€” src/nodes/overlay.ts
  • Selection: single, lasso, Escape/Delete, keyboard shortcut callbacks
  • Visual Effects: VisualState, src/effects/engine.ts, Glow + Stroke Flow + Gradient
  • Alignment guides: getBounds()-aware, RAF-throttled
  • roundedRectPath() β€” src/nodes/geometry/rectanglePath.ts
  • g.lasso layer
  • Connections: ports, drawing, straight/curved/s-shaped/l-bent, smart routing (Pro)
  • Connection labels, inline edit
  • Navigation controls: floating zoom/focus buttons

πŸ“Œ Permanent Rules

  1. All shape visuals through ShapeRegistry + ShapeRenderer.
  2. Overlay/selection/lasso/ports always use getPath/getBounds/getPorts.
  3. Geometry helpers in src/nodes/geometry/ β€” src/components/shapeTypes/ is deleted, never recreate.
  4. engine.registerShape() is the only shape extension point.
  5. engine.registerContextAction() is the only context pad extension point.
  6. Context pad = HTML overlay only β€” position always from D3 zoom transform.
  7. Effects via effectsEngine.applyEffects() after draw β€” never inside ShapeRenderer.draw().
  8. Execution in packages/runtime/ β€” never in packages/designer/.
  9. Output transformation in RuntimeConfig.outputTransformer β€” never in runtime core.
  10. Config = canvas environment only. Nodes via engine.addNode(). Styles via presets.
  11. Each package has its own package.json, README, and NPM page. They are independently installable.

End of prompt. Immediate focus: Complete L1Β·Phase 3 β€” Context Action Pad, then ship @zenode/designer v0.1.0.

Related Documents