FlowDrop is a frontend editor that calls your backend REST API. This guide explains what endpoints to implement, what request/response formats FlowDrop expects, and how to get a working backend running.
Endpoint Tiers
Not all endpoints are required. Here they are organized by priority:
Tier 1: Minimum Viable Backend
These 5 endpoints are the bare minimum to get FlowDrop working:
| Method | Path | Purpose |
|---|
GET | /health | Health check (FlowDrop checks this on mount) |
GET | /nodes | List available node types |
GET | /workflows/:id | Load a workflow |
POST | /workflows | Create a new workflow |
PUT | /workflows/:id | Update an existing workflow |
Tier 2: Full Editor Experience
These endpoints enable the complete sidebar, categories, and port validation:
| Method | Path | Purpose |
|---|
GET | /categories | Node category definitions (sidebar groups) |
GET | /port-config | Port data types and compatibility rules |
GET | /nodes/:id | Get a single node’s metadata |
GET | /workflows | List all workflows |
DELETE | /workflows/:id | Delete a workflow |
Tier 3: Advanced Features
These enable playground, execution, and interrupts:
| Method | Path | Purpose |
|---|
POST | /workflows/:id/execute | Execute a workflow |
GET | /executions/:id | Get execution status |
POST | /workflows/:id/playground/sessions | Create playground session |
GET | /playground/sessions/:sid/messages | Poll for messages |
POST | /playground/sessions/:sid/messages | Send user message |
GET | /interrupts/:id | Get pending interrupt |
POST | /interrupts/:id | Resolve an interrupt |
GET | /system/config | Runtime configuration |
Base URL Configuration
All paths above are relative to a base URL you configure:
import { createEndpointConfig } from '@flowdrop/flowdrop/core';
const endpointConfig = createEndpointConfig('/api/flowdrop');
// Nodes endpoint becomes: GET /api/flowdrop/nodes
GET /health
FlowDrop calls this to verify the backend is reachable.
Response:
{
"status": "ok",
"version": "1.0.0"
}
GET /nodes
Returns all available node types. FlowDrop uses this to populate the sidebar.
Query parameters:
category (optional) — filter by category
search (optional) — search name/description
limit (optional, default: 100)
offset (optional, default: 0)
Response:
{
"success": true,
"data": [
{
"id": "text_input",
"name": "Text Input",
"description": "Accepts text from the user",
"type": "simple",
"category": "inputs",
"icon": "mdi:text-box-outline",
"inputs": [],
"outputs": [
{
"id": "output",
"name": "Text",
"type": "output",
"dataType": "string"
}
],
"configSchema": {
"type": "object",
"properties": {
"placeholder": {
"type": "string",
"title": "Placeholder",
"default": "Enter text..."
}
}
}
}
]
}
Key fields in NodeMetadata:
id (required) — unique identifier
name (required) — display name
type — node visual type: workflowNode, simple, square, tool, gateway, terminal, idea, note
category — sidebar group: inputs, outputs, models, processing, logic, tools, etc.
icon — Iconify icon ID (e.g., mdi:text-box-outline)
inputs / outputs — port definitions with id, name, type, dataType
configSchema — JSON Schema defining the configuration form
POST /workflows
Creates a new workflow. FlowDrop sends the full workflow JSON.
Request body:
{
"name": "My Workflow",
"description": "A simple workflow",
"nodes": [
{
"id": "node-1",
"type": "simple",
"position": { "x": 100, "y": 200 },
"data": {
"label": "Text Input",
"config": { "placeholder": "Enter text..." },
"metadata": { "id": "text_input", "name": "Text Input", "...": "..." }
}
}
],
"edges": [
{
"id": "edge-1",
"source": "node-1",
"sourceHandle": "output",
"target": "node-2",
"targetHandle": "input"
}
]
}
Response:
{
"success": true,
"data": {
"id": "wf-abc123",
"name": "My Workflow",
"nodes": [],
"edges": [],
"metadata": {
"schemaVersion": "1.0.0",
"createdAt": "2025-01-01T00:00:00Z",
"updatedAt": "2025-01-01T00:00:00Z"
}
}
}
PUT /workflows/:id
Updates an existing workflow. Same request body format as POST.
GET /workflows/:id
Returns a single workflow by ID. Same response format as POST response.
GET /categories
Returns category definitions for the node sidebar.
Response:
{
"success": true,
"data": [
{
"id": "inputs",
"name": "Inputs",
"description": "Data input nodes",
"icon": "mdi:import",
"color": "var(--fd-node-emerald)",
"weight": 10
},
{
"id": "processing",
"name": "Processing",
"description": "Data transformation nodes",
"icon": "mdi:cog",
"color": "var(--fd-node-blue)",
"weight": 30
}
]
}
GET /port-config
Returns data type definitions and compatibility rules for port connections.
Response:
{
"success": true,
"data": {
"version": "1.0.0",
"defaultDataType": "string",
"dataTypes": [
{
"id": "string",
"name": "String",
"description": "Text data",
"color": "#10b981",
"category": "basic"
},
{
"id": "json",
"name": "JSON",
"description": "Structured data",
"color": "#f59e0b",
"category": "complex"
}
],
"compatibilityRules": [
{ "from": "string", "to": "json" },
{ "from": "json", "to": "string" }
]
}
}
These power execution, the interactive playground, and human-in-the-loop
interrupts. They use the same { "success": true, "data": ... } envelope as
Tier 1–2 unless noted.
POST /workflows/:id/execute
Starts a run. The body is optional.
Request:
{
"inputs": { "text_input": "Hello" },
"options": { "timeout": 30000, "maxSteps": 50 }
}
Response (202 Accepted):
{
"success": true,
"data": {
"execution_id": "exec-abc123",
"status": "running",
"started_at": "2025-01-01T00:00:00Z",
"estimated_completion": "2025-01-01T00:00:05Z"
}
}
GET /executions/:id
Poll for execution status using the execution_id returned above.
This endpoint returns the status object directly — no success/data
envelope. It is the one exception to the response wrapper.
Response:
{
"status": "completed",
"jobs": [],
"node_statuses": {
"node-1": { "status": "completed" }
},
"job_status_summary": {}
}
status is one of pending, running, completed, failed, cancelled,
paused, interrupted. The playground’s isTerminalStatus and
shouldStopPolling callbacks key off these values.
POST /workflows/:id/playground/sessions
Create an isolated test session for a workflow. The body is optional.
Request:
{ "name": "Test Session 1" }
Response (201):
{
"success": true,
"data": {
"id": "sess-abc123",
"workflowId": "wf-abc123",
"name": "Test Session 1",
"status": "idle",
"createdAt": "2025-01-01T00:00:00Z",
"updatedAt": "2025-01-01T00:00:00Z"
}
}
status is one of idle, running, awaiting_input, completed, failed.
POST /playground/sessions/:sid/messages
Send a user message — this triggers a run. The message is created with status
pending and processed asynchronously; poll the messages endpoint to track it.
Request:
{
"content": "Process this file",
"inputs": { "file_path": "/data/input.csv" }
}
Response (200):
{
"success": true,
"data": {
"id": "msg-1",
"sessionId": "sess-abc123",
"role": "user",
"content": "Process this file",
"status": "pending",
"sequenceNumber": 1,
"timestamp": "2025-01-01T00:00:00Z"
}
}
Returns 409 if the previous message in the session is still processing —
messages are handled in sequence.
GET /playground/sessions/:sid/messages
Poll for new messages. Supports since, latest, and before query parameters
for pagination.
Response:
{
"success": true,
"data": [
{
"id": "msg-2",
"sessionId": "sess-abc123",
"role": "assistant",
"content": "Done — processed 42 rows.",
"status": "completed",
"sequenceNumber": 2,
"timestamp": "2025-01-01T00:00:03Z"
}
],
"hasMore": false,
"sessionStatus": "completed"
}
role is user, assistant, system, or log. sessionStatus tells the
poller when to stop.
GET /interrupts/:id
Fetch a pending human-in-the-loop interrupt.
Response:
{
"success": true,
"data": {
"id": "int-abc123",
"type": "confirmation",
"status": "pending",
"nodeId": "node-3",
"executionId": "exec-abc123",
"allowCancel": true,
"config": {
"message": "Do you approve this action?",
"confirm_label": "Approve",
"cancel_label": "Reject"
},
"createdAt": "2025-01-01T00:00:00Z"
}
}
type is confirmation, choice, text, form, or review; status is
pending, resolved, or cancelled.
POST /interrupts/:id
Resolve an interrupt by submitting the user’s response. The value type depends
on the interrupt type.
Request:
Interrupt type | value shape |
|---|
confirmation | boolean |
choice | string or string[] |
text | string |
form | object matching the form schema |
review | decisions map + summary |
Response: the updated interrupt (same shape as GET /interrupts/:id, now
with status: "resolved").
GET /system/config
Public runtime configuration the editor reads on mount.
Response:
{
"success": true,
"data": {
"version": "1.0.0",
"features": { "playground": true, "interrupts": true },
"limits": { "maxWorkflowNodes": 100, "maxConcurrentExecutions": 5 }
}
}
CORS Configuration
FlowDrop runs in the browser, so your backend must allow cross-origin requests if served from a different domain:
// Express example
import cors from 'cors';
app.use(
cors({
origin: 'http://localhost:5173', // your frontend URL
methods: ['GET', 'POST', 'PUT', 'DELETE'],
allowedHeaders: ['Content-Type', 'Authorization']
})
);
When an operation fails, return a consistent error format:
{
"success": false,
"error": "Workflow not found",
"code": "NOT_FOUND",
"message": "No workflow exists with ID 'wf-xyz'"
}
FlowDrop’s API client expects standard HTTP status codes:
200 — success
201 — created
400 — bad request (validation error)
401 — unauthorized (triggers onApiError and auth provider’s onUnauthorized)
404 — not found
500 — server error
Static vs. Dynamic Node Serving
For simple use cases, you can serve node metadata as static JSON:
// nodes.json — serve as a static file
const nodes = [
{ id: 'text_input', name: 'Text Input', ... },
{ id: 'http_request', name: 'HTTP Request', ... }
];
app.get('/api/flowdrop/nodes', (req, res) => {
res.json({ success: true, data: nodes });
});
For dynamic use cases, load from a database:
app.get('/api/flowdrop/nodes', async (req, res) => {
const nodes = await db
.collection('nodes')
.find({
...(req.query.category && { category: req.query.category })
})
.toArray();
res.json({ success: true, data: nodes });
});
FlowDrop exercises your API in a predictable order on mount. Run these requests
against your base URL to confirm the contract before wiring up the editor —
they mirror exactly what the editor does. Replace the BASE value with your own.
BASE=http://localhost:3001/api/flowdrop
# 1. Health — FlowDrop checks this first on mount
curl -s $BASE/health
# 2. Nodes — populates the sidebar
curl -s $BASE/nodes
# 3. Categories — sidebar groups
curl -s $BASE/categories
# 4. Port config — connection compatibility rules
curl -s $BASE/port-config
# 5. Create a workflow (note the returned id)
curl -s -X POST $BASE/workflows \
-H 'Content-Type: application/json' \
-d '{"name":"Smoke test","nodes":[],"edges":[]}'
# 6. Load it back (use the id from step 5)
curl -s $BASE/workflows/<id>
# 7. Update it
curl -s -X PUT $BASE/workflows/<id> \
-H 'Content-Type: application/json' \
-d '{"name":"Smoke test (edited)","nodes":[],"edges":[]}'
Your backend conforms when:
If all seven calls succeed and the checklist passes, mounting the editor against
this base URL will load nodes into the sidebar and let you create, edit, and save
workflows.
Next Steps