# How to Add a Custom Plugin to the Kaneo Project: A Complete Developer's Guide

> Learn how to add a custom plugin to the Kaneo project. This guide covers implementing the IntegrationPlugin interface, defining event handlers, and registering your plugin. Enhance Kaneo with your own integrations today.

- Repository: [kaneo.app/kaneo](https://github.com/usekaneo/kaneo)
- Tags: how-to-guide
- Published: 2026-08-06

---

**To add a custom plugin to Kaneo, implement the `IntegrationPlugin` interface in `apps/api/src/plugins/`, define event handlers for task lifecycle events, and register your plugin via `registerPlugin()` in [`apps/api/src/plugins/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/index.ts).**

Kaneo is an open-source project management platform with an extensible backend architecture. The plugin system allows developers to integrate external services by reacting to task events through a type-safe TypeScript interface defined in the `apps/api` package.

## Understanding the Kaneo Plugin Architecture

The Kaneo backend uses an event-driven plugin system centered around the `IntegrationPlugin` interface. According to the source code in [`apps/api/src/plugins/types.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/types.ts), each plugin must export an object containing a unique type identifier, display name, optional configuration schema, and event handler functions.

The system uses a central registry pattern ([`apps/api/src/plugins/registry.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/registry.ts)) to maintain a map of all available plugins. When tasks are created, updated, or deleted, the system dispatches events to registered plugins that implement the corresponding handlers.

## Step-by-Step Guide to Creating a Custom Plugin

### Step 1: Scaffold Your Plugin Directory

Create a new directory under `apps/api/src/plugins/` using your plugin name as the folder name. Follow the structure used by existing integrations like `github` or `slack`:

```text
apps/api/src/plugins/my-integration/
├── index.ts          # Main plugin export

├── config.ts         # Configuration schema (optional)

└── events/           # Event handler implementations

    └── task-created.ts

```

### Step 2: Define Configuration Validation

If your integration requires API keys or webhook URLs, define a validation schema using Valibot or Zod. Create [`apps/api/src/plugins/my-integration/config.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/my-integration/config.ts):

```typescript
import { v } from "valibot";

export const myPluginConfigSchema = v.object({
  webhookUrl: v.string([v.url()]),
  apiKey: v.string(),
});

export type MyPluginConfig = v.InferOutput<typeof myPluginConfigSchema>;

```

### Step 3: Create Event Handlers

Implement handler functions that react to Kaneo events. Each handler receives the event payload and an `IntegrationContext` object containing the database connection and plugin configuration. Create [`apps/api/src/plugins/my-integration/events/task-created.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/my-integration/events/task-created.ts):

```typescript
import type { IntegrationContext } from "../../types";
import type { TaskCreatedEvent } from "@kaneo/api/events";
import type { MyPluginConfig } from "../config";

export async function onTaskCreated(
  event: TaskCreatedEvent,
  ctx: IntegrationContext,
) {
  const cfg = ctx.config as MyPluginConfig;
  
  await fetch(cfg.webhookUrl, {
    method: "POST",
    headers: { 
      "Authorization": `Bearer ${cfg.apiKey}`, 
      "Content-Type": "application/json" 
    },
    body: JSON.stringify(event),
  });
}

```

### Step 4: Implement the IntegrationPlugin Interface

Export the main plugin object in [`apps/api/src/plugins/my-integration/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/my-integration/index.ts):

```typescript
import { IntegrationPlugin } from "../types";
import { myPluginConfigSchema } from "./config";
import { onTaskCreated } from "./events/task-created";

export const myIntegration: IntegrationPlugin = {
  type: "my-integration",
  name: "My Custom Integration",
  configSchema: myPluginConfigSchema,
  onTaskCreated,
  // Add other handlers: onTaskStatusChanged, onTaskDeleted, etc.
};

```

### Step 5: Register Your Plugin

Import and register your plugin in [`apps/api/src/plugins/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/index.ts):

```typescript
import { myIntegration } from "./my-integration";

// Existing registrations...
registerPlugin(myIntegration);

```

## Complete Example: Building a Webhook Plugin

Below is a complete working example for a webhook notification plugin that posts task status changes to an external endpoint.

**Configuration schema** ([`apps/api/src/plugins/example-webhook/config.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/example-webhook/config.ts)):

```typescript
import { v } from "valibot";

export const exampleWebhookConfigSchema = v.object({
  endpoint: v.string([v.url()]),
  secret: v.string(),
});

export type ExampleWebhookConfig = v.InferOutput<typeof exampleWebhookConfigSchema>;

```

**Event handler** ([`apps/api/src/plugins/example-webhook/events/task-status-changed.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/example-webhook/events/task-status-changed.ts)):

```typescript
import type { IntegrationContext } from "../../types";
import type { TaskStatusChangedEvent } from "@kaneo/api/events";
import type { ExampleWebhookConfig } from "../config";

export async function onTaskStatusChanged(
  event: TaskStatusChangedEvent,
  ctx: IntegrationContext,
) {
  const cfg = ctx.config as ExampleWebhookConfig;
  
  await fetch(cfg.endpoint, {
    method: "POST",
    headers: {
      "X-Webhook-Secret": cfg.secret,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      taskId: event.taskId,
      newStatus: event.status,
      timestamp: event.timestamp,
    }),
  });
}

```

**Plugin registration** ([`apps/api/src/plugins/example-webhook/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/example-webhook/index.ts)):

```typescript
import { IntegrationPlugin } from "../types";
import { exampleWebhookConfigSchema } from "./config";
import { onTaskStatusChanged } from "./events/task-status-changed";

export const exampleWebhook: IntegrationPlugin = {
  type: "example-webhook",
  name: "Example Webhook",
  configSchema: exampleWebhookConfigSchema,
  onTaskStatusChanged,
};

```

## Handling Database Schema Changes

If your plugin requires persistent storage for its own data, modify [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts) to add your tables. After updating the schema, generate a migration using:

```bash
pnpm --filter @kaneo/api db:generate

```

Migrations apply automatically when the API server starts.

## Testing Your Custom Plugin

Place unit tests under `tests/api/my-integration/` following the patterns in existing plugin tests. For integration testing, add test cases in `tests/api-integration/` that create tasks and verify your plugin's side effects using mock HTTP servers or test database assertions.

Run the full validation suite before submitting:

```bash
pnpm lint
pnpm typecheck
pnpm build

```

## Summary

- **Create** a new directory under `apps/api/src/plugins/` with your plugin name.
- **Define** an `IntegrationPlugin` object implementing the interface from [`apps/api/src/plugins/types.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/types.ts).
- **Implement** event handlers that receive event payloads and `IntegrationContext` containing database and config access.
- **Register** your plugin by importing it into [`apps/api/src/plugins/index.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/index.ts) and calling `registerPlugin()`.
- **Validate** your code using Valibot or Zod schemas for type-safe configuration handling.
- **Migrate** any new database tables via [`apps/api/src/database/schema.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/database/schema.ts) if your plugin requires custom storage.

## Frequently Asked Questions

### What events can my plugin listen to?

Your plugin can implement handlers for task lifecycle events including `onTaskCreated`, `onTaskStatusChanged`, `onTaskDeleted`, and `onTaskCommentAdded`. Each handler receives a strongly typed event payload and an `IntegrationContext` object. Check [`apps/api/src/plugins/types.ts`](https://github.com/usekaneo/kaneo/blob/main/apps/api/src/plugins/types.ts) for the complete list of available event interfaces.

### Can I write plugins in JavaScript instead of TypeScript?

While the Kaneo codebase is written in TypeScript, you can write plugins in JavaScript. However, you will lose type safety and IntelliSense for the `IntegrationContext` and event payloads. The build system will transpile JavaScript files, but maintaining TypeScript definitions ensures compatibility with the registry system.

### How do I access the database within my plugin handlers?

The `IntegrationContext` object passed to every event handler contains a database connection property. Use this context to query or modify data using the ORM patterns established in the Kaneo codebase. The context also provides access to the validated plugin configuration specific to your integration instance.

### Where should I store plugin configuration?

Store configuration schemas in a [`config.ts`](https://github.com/usekaneo/kaneo/blob/main/config.ts) file within your plugin directory. When administrators configure your plugin through the Kaneo UI, the system validates inputs against your schema before persisting them to the database. Your event handlers receive the validated configuration through the `IntegrationContext` object.