Skip to content

navid-kianfar/kerdar

Repository files navigation

KERDAR - Modern React Workflow Designer

npm version npm version License: MIT

A production-ready, embeddable React workflow designer library inspired by n8n. Build powerful workflow automation into your application with a beautiful, modern UI.

GitHub | npm: @kerdar/core | npm: @kerdar/nodes-standard

Features

  • Visual Workflow Designer - Drag & drop nodes, connect them visually
  • 34+ Built-in Nodes - Triggers, actions, logic, data transformations, integrations
  • Expression Editor - Monaco-based editor with autocomplete for data references
  • Schema System - Type-safe data flow with intelligent autocomplete
  • Credential Management - n8n-style credential selector with secure storage
  • Inline Editing - Click on node names to edit directly on canvas
  • Client-side Execution - Run workflows directly in the browser
  • TypeScript First - Full type safety with comprehensive type definitions
  • Themeable - Light/dark mode with customizable colors
  • Extensible - Add custom nodes, categories, and credential types

Installation

# Using pnpm (recommended)
pnpm add @kerdar/core @kerdar/nodes-standard

# Using npm
npm install @kerdar/core @kerdar/nodes-standard

# Using yarn
yarn add @kerdar/core @kerdar/nodes-standard

Quick Start

import { useState } from 'react';
import {
  WorkflowDesigner,
  registerNodes,
  useWorkflowStore,
  type Workflow,
} from '@kerdar/core';
import { standardNodes } from '@kerdar/nodes-standard';
import '@kerdar/core/dist/style.css';

// Register standard nodes
registerNodes(standardNodes);

function App() {
  const [workflow, setWorkflow] = useState<Workflow>({
    id: 'workflow-1',
    name: 'My Workflow',
    nodes: [],
    edges: [],
  });

  return (
    <div style={{ height: '100vh' }}>
      <WorkflowDesigner
        workflow={workflow}
        onChange={setWorkflow}
      />
    </div>
  );
}

Creating Custom Nodes

Basic Custom Node

import { registerNode, NodeCategory, PropertyType, type NodeTypeDefinition } from '@kerdar/core';

const MyCustomNode: NodeTypeDefinition = {
  type: 'my-custom-node',
  version: 1,
  name: 'myCustomNode',
  displayName: 'My Custom Node',
  description: 'A custom node that does something',
  icon: 'Zap', // Lucide icon name
  iconColor: '#FF6B6B',
  category: NodeCategory.Action, // or 'action' string
  group: ['custom', 'transform'],

  // Define inputs
  inputs: [
    { type: 'main', displayName: 'Input' }
  ],

  // Define outputs
  outputs: [
    { type: 'main', displayName: 'Output' }
  ],

  // Define configurable parameters
  properties: [
    {
      name: 'operation',
      displayName: 'Operation',
      type: PropertyType.Options,
      options: [
        { name: 'Add', value: 'add' },
        { name: 'Multiply', value: 'multiply' },
      ],
      default: 'add',
      required: true,
    },
    {
      name: 'value',
      displayName: 'Value',
      type: PropertyType.Number,
      default: 0,
      description: 'The value to use in the operation',
    },
    {
      name: 'advanced',
      displayName: 'Advanced Options',
      type: PropertyType.Collection,
      default: {},
      values: [
        {
          name: 'precision',
          displayName: 'Decimal Precision',
          type: PropertyType.Number,
          default: 2,
        },
      ],
      // Only show when operation is 'multiply'
      displayOptions: {
        show: {
          operation: ['multiply'],
        },
      },
    },
  ],

  // Execution function
  async execute(context) {
    const inputData = context.getInputData();
    const operation = context.getNodeParameter<string>('operation');
    const value = context.getNodeParameter<number>('value');

    const results = inputData.map(item => ({
      json: {
        ...item.json,
        result: operation === 'add'
          ? (item.json.value as number) + value
          : (item.json.value as number) * value,
      },
    }));

    return { outputData: [results] };
  },
};

// Register the node
registerNode(MyCustomNode);

Custom Category

Categories are automatically created when you register nodes. Just use a new category value:

import { NodeCategory } from '@kerdar/core';

// Use built-in categories
const node1 = {
  // ...
  category: NodeCategory.Action, // 'action'
};

// Or create a custom category by using a string
const node2 = {
  // ...
  category: 'my-custom-category' as any, // Will create a new category
};

To customize how categories appear in the sidebar, you can filter or group nodes:

import { useNodeTypesByCategory, useNodeCategories } from '@kerdar/core';

function CustomSidebar() {
  const categories = useNodeCategories();

  // Get nodes for a specific category
  const actionNodes = useNodeTypesByCategory('action');

  // Custom ordering
  const orderedCategories = ['trigger', 'action', 'logic', 'data', 'my-custom-category'];

  return (
    <div>
      {orderedCategories.map(category => (
        <CategorySection key={category} category={category} />
      ))}
    </div>
  );
}

Working with Workflows

Workflow Data Structure

interface Workflow {
  id: string;
  name: string;
  description?: string;
  nodes: WorkflowNode[];
  edges: WorkflowEdge[];
  settings?: WorkflowSettings;
  metadata?: {
    createdAt: string;
    updatedAt: string;
    author?: string;
    tags?: string[];
  };
}

interface WorkflowNode {
  id: string;
  type: string;           // References NodeTypeDefinition.type
  name: string;           // Display name
  position: { x: number; y: number };
  parameters: Record<string, any>;
  disabled?: boolean;
  credentials?: Record<string, { id: string; name: string }>;
}

interface WorkflowEdge {
  id: string;
  source: string;         // Source node ID
  target: string;         // Target node ID
  sourceHandle?: string;  // Output handle (e.g., "output-0")
  targetHandle?: string;  // Input handle (e.g., "input-0")
}

Fetching Workflows

import { useState, useEffect } from 'react';
import { WorkflowDesigner, type Workflow } from '@kerdar/core';

function WorkflowEditor({ workflowId }: { workflowId: string }) {
  const [workflow, setWorkflow] = useState<Workflow | null>(null);
  const [loading, setLoading] = useState(true);

  // Fetch workflow from your API
  useEffect(() => {
    async function fetchWorkflow() {
      try {
        const response = await fetch(`/api/workflows/${workflowId}`);
        const data = await response.json();
        setWorkflow(data);
      } catch (error) {
        console.error('Failed to fetch workflow:', error);
      } finally {
        setLoading(false);
      }
    }
    fetchWorkflow();
  }, [workflowId]);

  if (loading) return <div>Loading...</div>;
  if (!workflow) return <div>Workflow not found</div>;

  return (
    <WorkflowDesigner
      workflow={workflow}
      onChange={setWorkflow}
    />
  );
}

Saving Workflows

import { useWorkflowStore, useIsDirty } from '@kerdar/core';

function SaveButton() {
  const workflow = useWorkflowStore(state => state.workflow);
  const isDirty = useIsDirty();
  const [saving, setSaving] = useState(false);

  const handleSave = async () => {
    setSaving(true);
    try {
      await fetch(`/api/workflows/${workflow.id}`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify(workflow),
      });

      // Mark as saved (clears dirty state)
      useWorkflowStore.getState().markAsSaved();
    } catch (error) {
      console.error('Failed to save:', error);
    } finally {
      setSaving(false);
    }
  };

  return (
    <button onClick={handleSave} disabled={!isDirty || saving}>
      {saving ? 'Saving...' : isDirty ? 'Save Changes' : 'Saved'}
    </button>
  );
}

Export/Import Workflows

function ExportImportButtons() {
  const workflow = useWorkflowStore(state => state.workflow);
  const setWorkflow = useWorkflowStore(state => state.setWorkflow);

  // Export to JSON file
  const handleExport = () => {
    const json = JSON.stringify(workflow, null, 2);
    const blob = new Blob([json], { type: 'application/json' });
    const url = URL.createObjectURL(blob);

    const a = document.createElement('a');
    a.href = url;
    a.download = `${workflow.name || 'workflow'}.json`;
    a.click();

    URL.revokeObjectURL(url);
  };

  // Import from JSON file
  const handleImport = (event: React.ChangeEvent<HTMLInputElement>) => {
    const file = event.target.files?.[0];
    if (!file) return;

    const reader = new FileReader();
    reader.onload = (e) => {
      try {
        const imported = JSON.parse(e.target?.result as string);
        setWorkflow(imported);
      } catch (error) {
        console.error('Invalid workflow file:', error);
      }
    };
    reader.readAsText(file);
  };

  return (
    <>
      <button onClick={handleExport}>Export</button>
      <input type="file" accept=".json" onChange={handleImport} />
    </>
  );
}

Executing Workflows

Client-side Execution

import { executeWorkflow, useExecutionStore } from '@kerdar/core';

function ExecuteButton() {
  const workflow = useWorkflowStore(state => state.workflow);
  const isExecuting = useExecutionStore(state => state.isExecuting);

  const handleExecute = async () => {
    try {
      const result = await executeWorkflow(workflow, {
        mode: 'manual',
        onProgress: (nodeId, status) => {
          console.log(`Node ${nodeId}: ${status}`);
        },
      });
      console.log('Execution result:', result);
    } catch (error) {
      console.error('Execution failed:', error);
    }
  };

  return (
    <button onClick={handleExecute} disabled={isExecuting}>
      {isExecuting ? 'Running...' : 'Execute'}
    </button>
  );
}

Accessing Execution Data

import { useExecutionStore, useNodeStatus, useAllNodeStatus } from '@kerdar/core';

function ExecutionStatus() {
  const isExecuting = useExecutionStore(state => state.isExecuting);
  const nodeStatuses = useAllNodeStatus();
  const executionLog = useExecutionStore(state => state.executionLog);

  // Get output data for a specific node
  const nodeOutputData = useExecutionStore(state => state.nodeOutputData);
  const httpRequestOutput = nodeOutputData['http-request-node-id'];

  return (
    <div>
      <p>Status: {isExecuting ? 'Running' : 'Idle'}</p>

      <h3>Node Statuses:</h3>
      {Object.entries(nodeStatuses).map(([nodeId, status]) => (
        <div key={nodeId}>{nodeId}: {status}</div>
      ))}

      <h3>Execution Log:</h3>
      {executionLog.map((entry, i) => (
        <div key={i}>{entry.message}</div>
      ))}
    </div>
  );
}

Theming

Light/Dark Mode

import { WorkflowDesigner, useThemeActions, ThemeMode } from '@kerdar/core';

function App() {
  const { setMode } = useThemeActions();

  return (
    <>
      <button onClick={() => setMode(ThemeMode.Light)}>Light</button>
      <button onClick={() => setMode(ThemeMode.Dark)}>Dark</button>
      <button onClick={() => setMode(ThemeMode.System)}>System</button>

      <WorkflowDesigner workflow={workflow} onChange={setWorkflow} />
    </>
  );
}

Custom Theme

import { WorkflowDesigner, type ThemeConfig } from '@kerdar/core';

const customTheme: Partial<ThemeConfig> = {
  mode: 'dark',
  primaryColor: '#3B82F6',
  accentColor: '#8B5CF6',
  nodeColors: {
    trigger: '#8B5CF6',
    action: '#3B82F6',
    logic: '#F59E0B',
    data: '#10B981',
  },
};

function App() {
  return (
    <WorkflowDesigner
      workflow={workflow}
      onChange={setWorkflow}
      theme={customTheme}
    />
  );
}

Schema System

KERDAR includes a powerful schema system that enables type-safe data flow between nodes with intelligent autocomplete in the expression editor.

Defining Output Schemas

Add output schemas to your custom nodes for intelligent autocomplete:

import {
  registerNode,
  createSchema,
  stringProperty,
  numberProperty,
  objectProperty,
  type DataSchema,
  type NodeTypeDefinition,
} from '@kerdar/core';

// Static schema
const outputSchema: DataSchema = createSchema({
  userId: stringProperty({ description: 'User identifier' }),
  name: stringProperty({ description: 'User full name' }),
  email: stringProperty({ format: 'email' }),
  age: numberProperty({ description: 'User age' }),
  metadata: objectProperty({
    createdAt: stringProperty({ format: 'date-time' }),
    updatedAt: stringProperty({ format: 'date-time' }),
  }),
});

const UserLookupNode: NodeTypeDefinition = {
  type: 'user-lookup',
  // ... other properties
  outputSchema, // Static schema
  async execute(context) {
    // Node implementation
  },
};

Dynamic Schemas

For nodes whose output depends on parameters (like HTTP Request):

import { type DynamicSchemaFn } from '@kerdar/core';

const dynamicOutputSchema: DynamicSchemaFn = (params, node) => {
  const includeMetadata = params.includeMetadata as boolean;

  if (includeMetadata) {
    return createSchema({
      data: anyProperty(),
      metadata: objectProperty({
        statusCode: numberProperty(),
        headers: objectProperty({}, { additionalProperties: stringProperty() }),
      }),
    });
  }

  return createSchema({
    data: anyProperty(),
  });
};

const MyNode: NodeTypeDefinition = {
  // ...
  outputSchema: dynamicOutputSchema,
};

Using Schema Context

Access schema-based suggestions programmatically:

import {
  useSchemaContext,
  useSchemaSuggestions,
  useMockData,
  getSchemaExpressionVariables,
} from '@kerdar/core';

function MyComponent({ nodeId }: { nodeId: string }) {
  // Get full schema context (all upstream schemas)
  const context = useSchemaContext(nodeId);

  // Get autocomplete suggestions
  const suggestions = useSchemaSuggestions(nodeId);

  // Get mock data for simulation
  const mockData = useMockData(nodeId);

  // Get organized expression variables
  const variables = getSchemaExpressionVariables(nodeId);
  // Returns: { json: [...], input: [...], nodes: {...}, builtIn: [...] }
}

Workflow Simulation

Test workflows without making real API calls:

import { simulateWorkflow, previewSimulationDataFlow } from '@kerdar/core';

// Simulate full workflow execution with mock data
const result = await simulateWorkflow(workflow, {
  nodeDelay: 500, // Delay between nodes for visualization
  mockDataOverrides: {
    'node-1': { customData: 'override' },
  },
  onProgress: (nodeId, status, data) => {
    console.log(`${nodeId}: ${status}`, data);
  },
  onDataFlow: (source, target, data) => {
    console.log(`Data flowing from ${source} to ${target}`);
  },
});

// Preview data flow without execution
const dataFlow = previewSimulationDataFlow(workflow);
// Returns Map<nodeId, { input: [...], output: [...] }>

Property Types

KERDAR supports all standard n8n property types for node parameters:

Type Description
string Text input with optional password mode
number Numeric input with min/max validation
boolean Toggle switch
options Dropdown select
multiOptions Multi-select dropdown
json JSON editor
code Monaco code editor
collection Collapsible group of nested properties
fixedCollection Array of key-value pairs (headers, query params)
dateTime Date/time picker
color Color picker
notice Info message display

FixedCollection Example (Custom Headers)

{
  name: 'headerParameters',
  displayName: 'Header Parameters',
  type: PropertyType.FixedCollection,
  default: {},
  typeOptions: {
    multipleValues: true,
    multipleValueButtonText: 'Add Header',
  },
  values: [
    { name: 'name', displayName: 'Name', type: PropertyType.String, default: '' },
    { name: 'value', displayName: 'Value', type: PropertyType.String, default: '' },
  ],
}

Collection Example (Options Group)

{
  name: 'options',
  displayName: 'Options',
  type: PropertyType.Collection,
  default: {},
  values: [
    { name: 'timeout', displayName: 'Timeout (ms)', type: PropertyType.Number, default: 30000 },
    { name: 'followRedirects', displayName: 'Follow Redirects', type: PropertyType.Boolean, default: true },
    { name: 'ignoreSSL', displayName: 'Ignore SSL Issues', type: PropertyType.Boolean, default: false },
  ],
}

Credential System

Nodes can require credentials for authentication. The credential selector UI automatically appears when a node defines credentials:

const HttpRequestNode: NodeTypeDefinition = {
  // ...
  credentials: [
    {
      name: 'httpBasicAuth',
      displayName: 'HTTP Basic Auth',
      displayOptions: {
        show: { authentication: ['basicAuth'] },
      },
    },
    {
      name: 'apiKey',
      displayName: 'API Key',
      displayOptions: {
        show: { authentication: ['apiKey'] },
      },
    },
  ],
  // ...
};

HTTP Request Authentication Options

The built-in HTTP Request node supports multiple authentication types:

  • None - No authentication
  • Basic Auth - Username/password with Base64 encoding
  • API Key - Configurable key name, value, and location (header/query)
  • Header Auth - Custom header via credential
  • Bearer Token - OAuth-style bearer token

API Reference

Main Components

Component Description
WorkflowDesigner Main workflow editor component
NodeSidebar Node palette sidebar
NodeDetailsView Node configuration panel (NDV)
ExecutionHistory Execution history panel

Hooks

Hook Description
useWorkflowStore Access workflow state
useWorkflow Get current workflow
useNodes Get workflow nodes
useEdges Get workflow edges
useSelectedNode Get selected node
useIsDirty Check if workflow has unsaved changes
useExecutionStore Access execution state
useIsExecuting Check if workflow is executing
useNodeStatus Get status for a specific node
useNodeRegistryStore Access node registry
useNodeTypes Get all registered node types
useNodeCategories Get all categories
useThemeStore Access theme state
useThemeMode Get current theme mode

Functions

Function Description
registerNode(node) Register a single node type
registerNodes(nodes) Register multiple node types
getNodeType(type) Get node type definition
executeWorkflow(workflow, options) Execute a workflow

Standard Nodes

Trigger Nodes

  • Manual Trigger - Start workflow manually
  • Schedule Trigger - Cron-based scheduling
  • Webhook Trigger - HTTP endpoint trigger

Action Nodes

  • HTTP Request - Make HTTP requests
  • Code - Execute JavaScript code
  • Execute Command - Run shell commands
  • Send Email - Send emails via SMTP
  • Slack - Slack API integration

Logic Nodes

  • If - Conditional branching
  • Switch - Multi-way routing
  • Merge - Combine data from multiple inputs
  • Loop - Iterate over items
  • Split In Batches - Process items in batches

Data Nodes

  • Set Variable - Set/modify variables
  • Filter - Filter items by conditions
  • Sort - Sort items
  • Limit - Limit number of items
  • Transform - Map/transform data

Integration Nodes

  • Redis - Redis operations
  • RabbitMQ - Message queue operations
  • MinIO - Object storage operations

TypeScript Support

All types are exported from @kerdar/core:

import type {
  Workflow,
  WorkflowNode,
  WorkflowEdge,
  NodeTypeDefinition,
  NodeProperty,
  NodeExecutionContext,
  NodeExecutionResult,
  ThemeConfig,
} from '@kerdar/core';

License

MIT

About

A production-ready React workflow designer library inspired by n8n. Build visual workflow automation into any app with 34+ nodes, Monaco expression editor, drag-drop canvas, and full TypeScript support. Zero backend required.

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages