Skip to main content
What you’ll learnThe 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": {
      "id": "text_input",
      "name": "Text Input",
      "type": "simple",
      "category": "inputs"
    },
    "nodeId": "text_input.1"
  }
}
  • 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 connected
  • sourceHandle / 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:
Tutorial — Step 5 of 5 · Complete! ← Nodes & categories