Beyond the defaults

Plugins

Extend the Routecraft runtime with cross-cutting behaviour.

What is a plugin?

A plugin is code that runs once when the context starts, before any capabilities are registered. It has access to the full CraftContext and can:

  • Subscribe to lifecycle events (capability started, error occurred, context stopped)
  • Write shared state to the context store for adapters to read
  • Register additional capabilities dynamically

Plugins vs capabilities: a capability defines what your system does. A plugin extends how the runtime behaves. Logging, metrics, tracing, auth headers, and connection pooling are all plugin concerns, not capability concerns.

Writing a plugin

A plugin is an object with an apply(context) method. An optional name identifies it in logs and plugin:* events; without one, the runtime derives an id:

// plugins/logger.ts
import { type CraftContext, type CraftPlugin } from '@routecraft/routecraft'

export default {
  name: 'logger',
  apply(context: CraftContext) {
    context.on('route:started', ({ details: { route } }) => {
      context.logger.info(`Started: ${route.definition.id}`)
    })

    context.on('context:error', ({ details: { error } }) => {
      context.logger.error(error, 'Context error')
    })
  },
} satisfies CraftPlugin

Two more phases are optional: start runs once every route has started, and teardown releases what the plugin acquired when the context stops. A plugin with a lifetime, such as a timer, uses them. This one only needs apply, and keeps its state in the context store:

// plugins/metrics.ts
import { type CraftContext, type CraftPlugin } from '@routecraft/routecraft'

// Declaring the key types for what goes in and what comes back out, so
// readers do not have to cast.
declare module '@routecraft/routecraft' {
  interface StoreRegistry {
    'metrics.counters': { started: number; errors: number }
  }
}

export default {
  name: 'metrics',

  async apply(context: CraftContext) {
    context.setStore('metrics.counters', { started: 0, errors: 0 })

    context.on('route:started', () => {
      const counters = context.getStore('metrics.counters')
      if (counters) counters.started += 1
    })
  },
} satisfies CraftPlugin

Registering a plugin

Pass plugins in craft.config.ts:

// craft.config.ts
import type { CraftConfig } from '@routecraft/routecraft'
import logger from './plugins/logger'
import metrics from './plugins/metrics'

export const craftConfig: CraftConfig = {
  plugins: [logger, metrics],
}

Setting global adapter defaults

The most common use of plugins and context configuration is setting default options for adapters so you do not repeat them in every capability.

Core adapters have dedicated fields on CraftConfig:

// craft.config.ts
export const craftConfig: CraftConfig = {
  cron: { timezone: 'UTC', maxJitter: 2000 },
  direct: { channelType: KafkaChannel },
}

External adapters (from @routecraft/ai, etc.) use companion plugins:

import { llmPlugin } from '@routecraft/ai'

export const craftConfig: CraftConfig = {
  cron: { timezone: 'UTC' },
  plugins: [
    llmPlugin({
      providers: { anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! } },
      defaultOptions: { temperature: 0.7 },
    }),
  ],
}

Every cron() source and llm() destination in the context inherits those defaults unless overridden per-adapter. This keeps shared configuration out of every capability file.

For the full pattern (how merged options work, which adapters support them, and how to add support to a custom adapter), see Merged options.

Managing external services

Plugins can manage long-lived external processes. The built-in mcpPlugin demonstrates this pattern: it spawns stdio MCP server subprocesses, monitors their health, and restarts them with exponential backoff when they crash.

import { mcpPlugin } from '@routecraft/ai'

export const craftConfig: CraftConfig = {
  plugins: [
    mcpPlugin({
      clients: {
        filesystem: {
          transport: 'stdio',
          command: 'npx',
          args: ['-y', '@modelcontextprotocol/server-filesystem', '/tmp'],
        },
      },
      maxRestarts: 5,
    }),
  ],
}

The plugin starts each subprocess when the context starts and tears them down when it stops. Tools from all sources (local routes, stdio clients, HTTP clients) are collected into a unified registry accessible from the context store.

Lifecycle events

Plugins subscribe to events using context.on(eventName, handler). Common events include route:started, route:stopped, context:started, context:stopped, and context:error. See the Events reference for the full list.

Dynamically registering capabilities

Because plugins run before capabilities are registered, they can add capabilities to the context at startup:

// plugins/admin.ts
import {
  craft,
  log,
  simple,
  type CraftContext,
  type CraftPlugin,
} from '@routecraft/routecraft'

export default {
  name: 'admin',
  apply(context: CraftContext) {
    if (process.env.ENABLE_ADMIN === 'true') {
      context.registerRoutes(
        craft()
          .id('admin-health')
          .from(simple({ ok: true }))
          .to(log())
          .build()[0],
      )
    }
  },
} satisfies CraftPlugin

Monitoring

Logging, telemetry, and writing a custom monitoring plugin.

Merged options

Set adapter defaults once and share them across the context.

Plugins

The reference catalogue: every built-in plugin and its options.

Previous
Events