Workflow Data Schema
Workflows are stored as JSON in the workflowData field of the Workflow model. This document describes the complete schema specification.
Overview
The workflow data structure uses a normalized format that separates node definitions, connectors, and flow structure:
- Nodes — Each node defined once in a flat collection
- Connectors — Link/edge data with full presentation (routing points)
- Workflow — Tree structure using references, not inline data
- Presentation — Canvas viewport state
This design enables:
- Node reuse across multiple branches
- Preservation of connector routing/layout
- Clean separation of flow logic from presentation
- Efficient updates without data duplication
Top-Level Structure
interface WorkflowData {
nodes: Record<string, WorkflowNodeDef>; // Node definitions by ID
connectors: Record<string, WorkflowConnector>; // Connector definitions by ID
workflow: WorkflowRef; // Flow structure with refs
presentation?: {
viewport: {
x: number; // Canvas pan X offset
y: number; // Canvas pan Y offset
zoom: number; // Zoom level (100 = 100%)
};
};
}
| Field | Type | Description |
|---|---|---|
nodes | Record<string, WorkflowNodeDef> | Map of node ID → node definition |
connectors | Record<string, WorkflowConnector> | Map of connector ID → connector definition |
workflow | WorkflowRef | Root of the workflow tree (using refs) |
presentation | object | Canvas viewport state for UI restoration |
Node Definition (WorkflowNodeDef)
interface WorkflowNodeDef {
entityId?: string; // UUID of the WorkflowEntity
name: string; // Display name
entityType: string; // 'event' | 'prompt' | 'action' | 'result' | 'logic-branch'
presentation: {
type: string; // Node shape: 'circle', 'star', 'octagon', 'brain'
position: {
x: number; // X coordinate on canvas
y: number; // Y coordinate on canvas
};
};
// Event-specific fields
tfCondition?: string; // 'Single Path' | 'True/False' | 'Multi' | 'Iterable'
logicField?: string; // Variable name for Multi/Iterable mode
// Logic branch fields
branchValue?: string; // Branch value (e.g., "true", "false", "admin")
}
Node IDs
Node IDs are generated during serialization (e.g., n1, n2, n3). They're internal references, not persisted entity IDs.
Connector Definition (WorkflowConnector)
interface WorkflowConnector {
source: string; // Source node ID (e.g., "n1")
target: string; // Target node ID (e.g., "n2")
sourcePort: string; // Port name: 'out', 'true', 'false'
targetPort: string; // Port name: 'in'
presentation?: {
points?: Array<{ // Routing points for curved/segmented lines
x: number;
y: number;
}>;
};
}
Connector Features
- Full routing preservation — Points array stores exact line path
- Port identification — Source/target ports enable branch routing
- Layout persistence — User-adjusted link paths are saved
Workflow Reference (WorkflowRef)
interface WorkflowRef {
ref: string; // Node ID reference (e.g., "n1")
value?: string; // Branch value (for logic branches)
children: WorkflowRef[]; // Child references
}
The workflow field contains a tree of references, not inline node data:
{
"ref": "n1",
"children": [
{ "ref": "n2", "children": [] }
]
}
Complete Example
A workflow that checks if a user is premium and routes accordingly:
{
"nodes": {
"n1": {
"entityId": "550e8400-e29b-41d4-a716-446655440001",
"name": "Check Premium Status",
"entityType": "event",
"tfCondition": "True/False",
"presentation": {
"type": "circle",
"position": { "x": 100, "y": 200 }
}
},
"n2": {
"name": "true",
"entityType": "logic-branch",
"branchValue": "true",
"presentation": {
"type": "logic-branch",
"position": { "x": 300, "y": 100 }
}
},
"n3": {
"name": "false",
"entityType": "logic-branch",
"branchValue": "false",
"presentation": {
"type": "logic-branch",
"position": { "x": 300, "y": 300 }
}
},
"n4": {
"entityId": "550e8400-e29b-41d4-a716-446655440010",
"name": "Generate Premium Welcome",
"entityType": "prompt",
"presentation": {
"type": "brain",
"position": { "x": 500, "y": 100 }
}
},
"n5": {
"entityId": "550e8400-e29b-41d4-a716-446655440020",
"name": "Log Basic User",
"entityType": "action",
"presentation": {
"type": "star",
"position": { "x": 500, "y": 300 }
}
},
"n6": {
"entityId": "550e8400-e29b-41d4-a716-446655440030",
"name": "Success Result",
"entityType": "result",
"presentation": {
"type": "octagon",
"position": { "x": 700, "y": 100 }
}
},
"n7": {
"entityId": "550e8400-e29b-41d4-a716-446655440031",
"name": "Basic Success",
"entityType": "result",
"presentation": {
"type": "octagon",
"position": { "x": 700, "y": 300 }
}
}
},
"connectors": {
"c1": {
"source": "n1",
"target": "n2",
"sourcePort": "out",
"targetPort": "in",
"presentation": {
"points": [
{ "x": 160, "y": 200 },
{ "x": 230, "y": 200 },
{ "x": 230, "y": 100 },
{ "x": 300, "y": 100 }
]
}
},
"c2": {
"source": "n1",
"target": "n3",
"sourcePort": "out",
"targetPort": "in",
"presentation": {
"points": [
{ "x": 160, "y": 200 },
{ "x": 230, "y": 200 },
{ "x": 230, "y": 300 },
{ "x": 300, "y": 300 }
]
}
},
"c3": {
"source": "n2",
"target": "n4",
"sourcePort": "out",
"targetPort": "in"
},
"c4": {
"source": "n3",
"target": "n5",
"sourcePort": "out",
"targetPort": "in"
},
"c5": {
"source": "n4",
"target": "n6",
"sourcePort": "out",
"targetPort": "in"
},
"c6": {
"source": "n5",
"target": "n7",
"sourcePort": "out",
"targetPort": "in"
}
},
"workflow": {
"ref": "n1",
"children": [
{
"ref": "n2",
"value": "true",
"children": [
{
"ref": "n4",
"children": [
{ "ref": "n6", "children": [] }
]
}
]
},
{
"ref": "n3",
"value": "false",
"children": [
{
"ref": "n5",
"children": [
{ "ref": "n7", "children": [] }
]
}
]
}
]
},
"presentation": {
"viewport": {
"x": 50,
"y": 100,
"zoom": 100
}
}
}
Entity Types
Event Node
Entry point with condition evaluation:
{
"n1": {
"entityId": "uuid",
"name": "Check User Type",
"entityType": "event",
"tfCondition": "Single Path",
"presentation": {
"type": "circle",
"position": { "x": 100, "y": 200 }
}
}
}
Event with True/False Branching
{
"n1": {
"entityId": "uuid",
"name": "Is Premium?",
"entityType": "event",
"tfCondition": "True/False",
"presentation": { "type": "circle", "position": { "x": 100, "y": 200 } }
},
"n2": {
"name": "true",
"entityType": "logic-branch",
"branchValue": "true",
"presentation": { "type": "logic-branch", "position": { "x": 300, "y": 100 } }
},
"n3": {
"name": "false",
"entityType": "logic-branch",
"branchValue": "false",
"presentation": { "type": "logic-branch", "position": { "x": 300, "y": 300 } }
}
}
Event with Multi Branching
{
"n1": {
"entityId": "uuid",
"name": "Route by Role",
"entityType": "event",
"tfCondition": "Multi",
"logicField": "userRole",
"presentation": { "type": "circle", "position": { "x": 100, "y": 200 } }
},
"n2": {
"name": "Admin",
"entityType": "logic-branch",
"branchValue": "admin",
"presentation": { "type": "logic-branch", "position": { "x": 300, "y": 100 } }
},
"n3": {
"name": "User",
"entityType": "logic-branch",
"branchValue": "user",
"presentation": { "type": "logic-branch", "position": { "x": 300, "y": 250 } }
},
"n4": {
"name": "Guest",
"entityType": "logic-branch",
"branchValue": "guest",
"presentation": { "type": "logic-branch", "position": { "x": 300, "y": 400 } }
}
}
Event with Iterable Mode
Iterable mode allows the workflow to repeat for each item in an array. This is useful for processing lists of items like batch operations, multiple recipients, or array data.
{
"n1": {
"entityId": "uuid",
"name": "Process Each Recipient",
"entityType": "event",
"tfCondition": "Iterable",
"logicField": "message.recipients",
"presentation": { "type": "circle", "position": { "x": 100, "y": 200 } }
},
"n2": {
"entityId": "uuid",
"name": "Send Notification",
"entityType": "action",
"presentation": { "type": "star", "position": { "x": 350, "y": 200 } }
}
}
How Iterable Mode Works:
- The
logicFieldpoints to an array in the message (e.g.,message.recipients) - The workflow executes its children once for each item in the array
- Each iteration receives the current item as
___iterableItem___in the context - If the field is not an array, the workflow proceeds normally (single execution)
Prompt Node
{
"n5": {
"entityId": "uuid",
"name": "Generate Welcome",
"entityType": "prompt",
"presentation": {
"type": "brain",
"position": { "x": 500, "y": 200 }
}
}
}
Action Node
{
"n6": {
"entityId": "uuid",
"name": "Post to Mastodon",
"entityType": "action",
"presentation": {
"type": "star",
"position": { "x": 700, "y": 200 }
}
}
}
Result Node
{
"n7": {
"entityId": "uuid",
"name": "Success",
"entityType": "result",
"presentation": {
"type": "octagon",
"position": { "x": 900, "y": 200 }
}
}
}
Data Flow
flowchart TB
subgraph Admin[Admin Project]
Canvas[React Diagrams Canvas]
Serialize[serializeWorkflow]
Deserialize[deserializeWorkflow]
API[Workflow API]
end
subgraph DB[Database]
WorkflowTable[Workflow.workflowData JSON]
end
subgraph Consumer[Consumer Project]
Cache[WorkflowCache]
Evaluator[Evaluator.traverseNode]
NodeResolver[resolveNodeRef]
end
Canvas --> Serialize
Serialize --> API
API --> WorkflowTable
WorkflowTable --> Cache
Cache --> Evaluator
Evaluator --> NodeResolver
NodeResolver --> Evaluator
Deserialize --> Canvas
API --> Deserialize
Serialization (Canvas → JSON)
The Admin app's serializeWorkflow() function:
- Build nodes map — Extract all diagram nodes with generated IDs (
n1,n2, ...) - Build connectors map — Extract all diagram links with generated IDs (
c1,c2, ...) - Build workflow tree — Derive tree structure from connector relationships using refs
- Handle logic branches — Add
valueproperty on branch refs
// Pseudocode
function serializeWorkflow(): WorkflowData {
const nodes: Record<string, WorkflowNodeDef> = {};
const connectors: Record<string, WorkflowConnector> = {};
const nodeIdMap = new Map<DiagramNode, string>();
// 1. Build nodes map
let nodeCounter = 1;
for (const diagramNode of model.getNodes()) {
const nodeId = `n${nodeCounter++}`;
nodeIdMap.set(diagramNode, nodeId);
nodes[nodeId] = {
entityId: diagramNode.entityId,
name: diagramNode.name,
entityType: diagramNode.entityType,
presentation: {
type: diagramNode.type,
position: diagramNode.getPosition()
},
// ... other fields
};
}
// 2. Build connectors map
let connectorCounter = 1;
for (const link of model.getLinks()) {
const connectorId = `c${connectorCounter++}`;
connectors[connectorId] = {
source: nodeIdMap.get(link.getSourceNode()),
target: nodeIdMap.get(link.getTargetNode()),
sourcePort: link.getSourcePort().getName(),
targetPort: link.getTargetPort().getName(),
presentation: {
points: link.getPoints().map(p => ({ x: p.getX(), y: p.getY() }))
}
};
}
// 3. Build workflow tree from connectors
const workflow = buildWorkflowTree(nodes, connectors);
return { nodes, connectors, workflow, presentation: { viewport: {...} } };
}
Deserialization (JSON → Canvas)
The Admin app's deserializeWorkflow() function:
- Create diagram nodes — Instantiate from
nodescollection - Position nodes — Apply
presentation.positioncoordinates - Create diagram links — Instantiate from
connectorscollection - Restore routing — Apply connector
presentation.pointsfor exact layout - Validate structure — Use
workflowtree for orphan detection
// Pseudocode
function deserializeWorkflow(data: WorkflowData): void {
const nodeMap = new Map<string, DiagramNode>();
// 1. Create nodes
for (const [nodeId, nodeDef] of Object.entries(data.nodes)) {
const node = createNodeByType(nodeDef.entityType, nodeDef);
node.setPosition(nodeDef.presentation.position.x, nodeDef.presentation.position.y);
model.addNode(node);
nodeMap.set(nodeId, node);
}
// 2. Create connectors with routing
for (const [connectorId, connector] of Object.entries(data.connectors)) {
const sourceNode = nodeMap.get(connector.source);
const targetNode = nodeMap.get(connector.target);
const link = createLink(sourceNode, targetNode, connector.sourcePort, connector.targetPort);
// Restore routing points
if (connector.presentation?.points) {
link.setPoints(connector.presentation.points);
}
model.addLink(link);
}
// 3. Restore viewport
if (data.presentation?.viewport) {
model.setOffset(data.presentation.viewport.x, data.presentation.viewport.y);
model.setZoomLevel(data.presentation.viewport.zoom);
}
}
Consumer Execution
The Consumer's Evaluator.traverseNode() resolves refs at runtime:
// In Evaluator.js
async traverseNode(nodeRef, context, workflowMetadata) {
// Resolve ref to node definition
const nodeDef = this.nodesMap[nodeRef.ref];
if (!nodeDef || !nodeDef.entityId) {
return false;
}
// Get entity from cache
const entity = this.getEntity(nodeDef.entityId);
// Evaluate with appropriate evaluator
const result = await evaluator.evaluate(context, entity, nodeRef, ...);
// Process children refs
if (result.children) {
for (const childRef of result.children) {
await this.traverseNode(childRef, context, workflowMetadata);
}
}
}
Branch Matching
For True/False and Multi mode events, the value property on workflow refs determines branch selection:
// In EventEvaluator.js
findMatchingBranch(nodeRef, branchValue) {
// Children have 'value' property from workflow structure
return nodeRef.children.find(child =>
String(child.value) === String(branchValue)
);
}
Schema Validation
The Consumer validates workflows before execution:
| Check | Behavior on Failure |
|---|---|
Missing workflow | Skip workflow (log warning) |
Missing ref in workflow | Skip node (log warning) |
ref not found in nodes | Skip node (log warning) |
entityId not in cache | Skip node (log warning) |
| Circular reference | Detected via visited set, skip |
Database Storage
model Workflow {
id String @id @default(uuid())
organizationId String
environmentId String
name String
description String?
workflowData Json? // The JSON structure documented here
isActive Boolean @default(true)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
PostgreSQL's JSONB type enables:
- Efficient storage with compression
- Indexing on JSON paths (if needed)
- Querying workflows containing specific entity IDs
Benefits of This Format
| Benefit | Description |
|---|---|
| No duplication | Each node defined once, referenced multiple times |
| Full layout preservation | Connector routing points stored |
| Clean separation | Flow logic separate from presentation |
| Easier updates | Change node once, reflected everywhere |
| Simpler debugging | Flat structures easier to inspect |
Related Topics
- Workflow Canvas — Visual editor documentation
- Consumer Evaluator — How workflows are executed
- Workflow Entities — Entity configuration