npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@kerdar/core

v0.1.1

Published

Modern React Workflow Designer Library - Core Package

Readme

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
  • 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}
    />
  );
}

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