Saving workflows
Persist workflow data with save callbacks and understand the workflow data model.
What you'll learn
The workflow data structure (nodes and edges), how to implement save callbacks, and how to handle workflow lifecycle events.
With a pre-built workflow loaded, clicking Save in the toolbar runs the save flow. Users can modify the workflow and save their changes.
The workflow data structure
When you save or export a workflow, FlowDrop produces a JSON object with two main arrays: nodes and edges.
Nodes
Each node on the canvas is represented as:
{
"id": "text_input.1",
"type": "universalNode",
"position": { "x": 0, "y": 100 },
"data": {
"label": "Text Input",
"config": {
"placeholder": "Enter text..."
},
"metadata": {
"node_type_id": "text_input",
"name": "Text Input",
"type": "simple",
"category": "inputs"
}
}
}position— where the node sits on the canvas (x, y coordinates)data.config— the user's configuration values (from the config form)data.metadata— the full node definition (type, ports, schema)
Edges
Each connection between nodes is an edge:
{
"id": "e-text_input-ai_analyzer",
"source": "text_input.1",
"target": "ai_content_analyzer.1",
"sourceHandle": "text_input.1-output-text",
"targetHandle": "ai_content_analyzer.1-input-content"
}source/target— the node IDs being connectedsourceHandle/targetHandle— the specific port IDs (format:{nodeId}-{direction}-{portId})
Event handlers
FlowDrop provides lifecycle hooks to respond to workflow changes and saves:
const app = await mountFlowDropApp(container, {
nodes,
categories,
endpointConfig: createEndpointConfig('/api/flowdrop'),
showNavbar: true,
eventHandlers: {
// Called before save — return false to cancel
onBeforeSave: async (workflow) => {
console.log('Saving workflow:', workflow.name);
const isValid = workflow.nodes.length > 0;
return isValid;
},
// Called after successful save
onAfterSave: async (workflow) => {
console.log('Workflow saved!', workflow.id);
},
// Called when save fails
onSaveError: async (error, workflow) => {
console.error('Save failed:', error.message);
},
// Called on any workflow change
onWorkflowChange: (workflow, changeType) => {
// changeType: 'node_add', 'node_remove', 'node_move',
// 'node_config', 'edge_add', 'edge_remove',
// 'metadata', 'name', 'description'
console.log(`Change: ${changeType}`);
},
// Called when dirty state changes
onDirtyStateChange: (isDirty) => {
// Update your UI (e.g., show unsaved indicator)
document.title = isDirty ? '* My Editor' : 'My Editor';
}
}
});Implementing a save endpoint
FlowDrop sends the workflow data to your API when the user clicks Save. Here's a minimal backend example:
// Express.js example
app.put('/api/flowdrop/workflows/:id', (req, res) => {
const { id } = req.params;
const { nodes, edges, name, description } = req.body;
// Save to your database
db.workflows.update(id, { nodes, edges, name, description });
res.json({
success: true,
data: { id, nodes, edges, name, description },
message: 'Workflow saved'
});
});The API response should follow the pattern { success: boolean, data: Workflow, message: string }.
Complete setup
Here's everything from the tutorial combined into a single setup:
import { mountFlowDropApp } from '@flowdrop/flowdrop/editor';
import { createEndpointConfig } from '@flowdrop/flowdrop/core';
import '@flowdrop/flowdrop/styles';
const nodes = [
{ id: 'text_input', name: 'Text Input', type: 'simple', category: 'inputs' /* ... */ },
{ id: 'text_output', name: 'Text Output', type: 'simple', category: 'outputs' /* ... */ },
{ id: 'ai_analyzer', name: 'AI Analyzer', type: 'tool', category: 'ai' /* ... */ }
// ...more nodes
];
const categories = [
{ id: 'inputs', name: 'Inputs', icon: 'mdi:import', color: '#22c55e' },
{ id: 'outputs', name: 'Outputs', icon: 'mdi:export', color: '#ef4444' },
{ id: 'ai', name: 'AI & ML', icon: 'mdi:brain', color: '#9C27B0' }
// ...more categories
];
const app = await mountFlowDropApp(document.getElementById('editor'), {
nodes,
categories,
endpointConfig: createEndpointConfig('/api/flowdrop'),
height: '100vh',
showNavbar: true,
eventHandlers: {
onAfterSave: async (wf) => console.log('Saved:', wf.id),
onDirtyStateChange: (dirty) => {
document.title = dirty ? '* Editor' : 'Editor';
}
}
});What's next
You've completed the tutorial! Here are some areas to explore next:
- Node Types — deep dive into all built-in node types and custom nodes
- Configuration Forms — advanced JSON Schema forms with UI schema layouts
- Framework Integration — use FlowDrop with React, Vue, Angular, or vanilla JS
- Theming — customize colors, fonts, and dark mode with CSS tokens
- Interactive Playground — add a chat-based testing interface to your editor
Tutorial — Step 5 of 5 · Complete!