π§ 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.
π§ 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
| Package | Free | Paid |
|---|---|---|
@zenode/designer | Core canvas, shapes, basic connections | Pro $149/yr β minimap, themes, undo+, PNG export |
@zenode/serializer | JSON, Mermaid, DOT, DSL | Pro $99/yr β BPMN 2.0, image export |
@zenode/runtime | 100 executions/day | Runtime $49/mo β unlimited executions |
@zenode/core (bundle) | All free tiers | Bundle $199/yr β all pro features |
@zenode/bpmn-presets | Always 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 Flow | BPMN.js | JointJS+ | n8n | Zenode | |
|---|---|---|---|---|---|
| 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 | β οΈ | β | β οΈ | β | β |
| Price | Free/Pro | Free | $1200+/yr | Free/Cloud | Free/$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/designerlive 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
ZenodeEngineclass: 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
ShapeRenderercontract:draw,getPath,getBounds,getPortsShapeRegistryβsrc/nodes/registry.tsRectangleRenderer,CircleRenderer,RhombusRendererall implementedengine.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.lassolayer 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
- HTML overlay, not SVG β
position: absolutediv over the SVG wrapper parent - Position always from D3 zoom transform β never cache screen coordinates
- Hides on: drag start, canvas click,
node:deleted,edge:deleted, Escape isVisible/isDisabledcalled every render β live engine state, never memoized- Actions are stateless β
(target, engine)args only, never close over stale data - 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.swimlaneslayer β belowg.placed-nodes- Child positions clamped to parent lane bounds during drag
SwimLaneRendererimplementsShapeRenderer- 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-colorcontext pad action, persisted in state - BPMN presets β
@zenode/bpmn-presetsfree 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 fromsrc/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
ZenodeEngineowns designer state.ZenodeRuntimeowns 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
- Does this mutate engine state? β Command pattern + emit event.
- Does this render something? β D3 data join on the correct layer group.
- Does this need to be undoable? β Yes if it changes nodes, connections, viewport, swimlane.
- Is this a premium feature? β Gate via
LicenseManager.check('feature-name'). - Does this need a config option? β Add to
src/types/index.tswith typed default. - Does this change appearance? β
--zenode-*CSS variables β never hardcoded. - Does this involve coordinates? β
d3.pointer(event, svgRoot), account for zoom transform. - Does this draw an overlay, ring, or port? β
registry.get(node.type).getPath(node)β never plain rect. - Is this a new shape? β Implement full
ShapeRenderer:draw,getPath,getBounds,getPorts. - Does this add a button near a node/edge? β
engine.registerContextAction()β never hardcode. - Does this involve context pad position? β Always from D3 zoom transform β never cached screen coords.
- Does this involve workflow execution? β
ZenodeRuntimeinpackages/runtime/β never inengine.ts. - Does this transform execution output? β
outputTransformerinRuntimeConfigβ never in runtime core. - Is this a child of a swimlane? β Position clamped to parent bounds, ports recomputed after move.
- 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
ShapeRenderercontract (draw,getPath,getBounds,getPorts) βsrc/types/index.tsShapeRegistryβ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.tsg.lassolayer- Connections: ports, drawing, straight/curved/s-shaped/l-bent, smart routing (Pro)
- Connection labels, inline edit
- Navigation controls: floating zoom/focus buttons
π Permanent Rules
- All shape visuals through
ShapeRegistry+ShapeRenderer. - Overlay/selection/lasso/ports always use
getPath/getBounds/getPorts. - Geometry helpers in
src/nodes/geometry/βsrc/components/shapeTypes/is deleted, never recreate. engine.registerShape()is the only shape extension point.engine.registerContextAction()is the only context pad extension point.- Context pad = HTML overlay only β position always from D3 zoom transform.
- Effects via
effectsEngine.applyEffects()after draw β never insideShapeRenderer.draw(). - Execution in
packages/runtime/β never inpackages/designer/. - Output transformation in
RuntimeConfig.outputTransformerβ never in runtime core. - Config = canvas environment only. Nodes via
engine.addNode(). Styles via presets. - 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
Journey from Concept to Code: Transforming Cursor into a Devin-like AI Assistant
square: https://daily.borninsea.com/assets/image_1739763605490_yobtaj.png
grapeot/devin.cursorrules
date: 2025-07-03T22:11:02.060257
dbt Core
A powerful open-source data transformation tool that enables analytics engineers to transform data in their warehouses by writing modular SQL enhanced with Jinja templating.
Cursor Development Environment
docker build -f Dockerfile.dev -t clawdrive-dev .