Flowlib
Plugins

Build Your Own

Create a custom Flowlib plugin with hooks, actions, database tables, and API endpoints.

An Flowlib plugin is a plain object with an id. Everything else is opt-in — add only what you need.

import type { FlowlibPlugin } from '@flowlib/core';

export function myPlugin(): FlowlibPlugin {
  return {
    id: 'my-plugin',
    name: 'My Plugin',
  };
}

Register it in your config:

const router = await createFlowlibRouter({
  // ...
  plugins: [myPlugin()],
});

Intercept requests with hooks

Hooks let you run logic before or after requests and flow executions. Return { response } to short-circuit, or {} to continue normally.

export function rateLimiter(): FlowlibPlugin {
  const counts = new Map<string, number>();

  return {
    id: 'rate-limiter',
    hooks: {
      onRequest: async ({ request }) => {
        const ip = request.headers['x-forwarded-for'] || 'unknown';
        const count = (counts.get(ip) || 0) + 1;
        counts.set(ip, count);

        if (count > 100) {
          return { response: { status: 429, body: { error: 'Rate limited' } } };
        }
        return {};
      },
    },
  };
}

Available hooks:

HookWhen it firesCan short-circuit?
onRequestBefore every API requestYes — return { response }
onResponseAfter the route handler respondsNo, but can replace the response
beforeFlowRunBefore flow executionYes — return { cancel: true }
afterFlowRunAfter flow executionNo
beforeNodeExecuteBefore each nodeYes — return { skip: true }
afterNodeExecuteAfter each nodeCan override { output }
onAuthorizeDuring permission checksYes — return { allowed: false }

When multiple plugins register the same hook, they run in declaration order.

Add custom flow nodes

Define actions using defineAction(). They automatically appear in the node palette and as agent tools.

import { defineAction } from '@flowlib/core';
import { z } from 'zod/v4';

const myAction = defineAction({
  id: 'my_plugin.transform',
  name: 'My Transform',
  description: 'Custom data transformation',
  provider: { id: 'my_plugin', name: 'My Plugin', icon: 'Blocks' },
  params: {
    schema: z.object({ format: z.string() }),
    fields: [{ name: 'format', label: 'Format', type: 'text', required: true }],
  },
  async execute(params, context) {
    return { success: true, output: { formatted: params.format } };
  },
});

export function myPlugin(): FlowlibPlugin {
  return {
    id: 'my-plugin',
    actions: [myAction],
  };
}

Add database tables

Declare tables in an abstract, dialect-agnostic format. The CLI generates the Drizzle schema files for you.

export function myPlugin(): FlowlibPlugin {
  return {
    id: 'my-plugin',
    schema: {
      my_plugin_logs: {
        fields: {
          id: { type: 'string', primaryKey: true },
          flowRunId: { type: 'string', references: { table: 'flow_runs', field: 'id' } },
          message: { type: 'string', required: true },
          createdAt: { type: 'string', required: true },
        },
      },
    },
    requiredTables: ['my_plugin_logs'],
  };
}

After adding the plugin, run:

npx flowlib-cli generate
npx flowlib-cli migrate --push

Flowlib verifies required tables exist at startup and shows a clear error if they're missing.

You can also extend core tables (additive only):

schema: {
  flows: {
    fields: {
      tenantId: { type: 'string' },  // Adds a column to the core flows table
    },
  },
}

Add API endpoints

Plugin endpoints are mounted at /plugins/{pluginId}/:

export function myPlugin(): FlowlibPlugin {
  return {
    id: 'my-plugin',
    endpoints: [
      {
        method: 'GET',
        path: '/stats',
        handler: async (ctx) => {
          return { body: { requests: 42 } };
        },
      },
    ],
  };
}
// Accessible at: GET /plugins/my-plugin/stats

The handler receives a PluginEndpointContext with body, params, query, headers, identity, database, and core. Return { body } for JSON responses or { stream } for streaming.

endpoints: [
  {
    method: 'POST',
    path: '/items/:id',
    handler: async (ctx) => {
      const { id } = ctx.params;
      const { name } = ctx.body;
      const flowlib = ctx.getFlowlib();
      // Access flows, runs, etc. via the Flowlib instance
      return { status: 201, body: { id, name } };
    },
  },
];

Auth hooks still apply unless the endpoint declares isPublic: true.

Init and shutdown

Use init for setup work and shutdown for cleanup. Init runs during Flowlib.initialize() in declaration order. Shutdown runs in reverse.

export function myPlugin(): FlowlibPlugin {
  let interval: NodeJS.Timeout;

  return {
    id: 'my-plugin',
    async init(ctx) {
      ctx.logger.info('My plugin initializing');
      interval = setInterval(() => {
        /* background work */
      }, 60000);
    },
    async shutdown() {
      clearInterval(interval);
    },
  };
}

Frontend plugins

Frontend plugins contribute UI elements to the Flowlib editor:

ContributionWhat it adds
sidebarNavigation items
routesFull pages
panelTabsTabs in the node editor panel
headerActionsToolbar buttons
providersReact context providers
apiHeadersHeaders injected into every API request

Frontend plugins are passed to the Flowlib component and composed at render time.

Keep backend and frontend code in separate directories (src/backend/, src/frontend/). Shared types in src/shared/ must be browser-safe — no Node.js imports.

On this page