Beyond the defaults

Merged options

Set adapter defaults once and share them across your entire context.

What are merged options?

Many adapters accept options at the call site: timezone for cron(), temperature for llm(), and so on. When the same options repeat across dozens of capabilities, duplication becomes a maintenance problem. Merged options solve this by letting you register context-level defaults that every adapter of that type inherits automatically.

The merge hierarchy (last wins):

  1. Built-in defaults: hardcoded in the adapter (e.g. temperature: 0 for llm())
  2. Context defaults: registered in craft.config.ts
  3. Per-adapter options: passed directly at the call site

Per-adapter options always take precedence over context defaults, which in turn take precedence over built-in defaults.

Setting defaults for core adapters

Core adapters (cron, direct) have dedicated fields on CraftConfig. Set them once and every adapter of that type in the context inherits the values:

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

export const craftConfig: CraftConfig = {
  cron: { timezone: 'UTC', maxJitter: 2000 },
}

Now every cron() source inherits timezone: 'UTC' and maxJitter: 2000 unless overridden:

// Inherits timezone: 'UTC' and maxJitter: 2000 from config
.from(cron('@daily'))

// Overrides timezone but keeps maxJitter: 2000
.from(cron('0 9 * * 1-5', { timezone: 'America/New_York' }))

Setting defaults for external adapters

Adapters from other packages (like @routecraft/ai) use the plugin pattern. Register a companion plugin in craft.config.ts:

import type { CraftConfig } from '@routecraft/routecraft'
import { llmPlugin, embeddingPlugin } from '@routecraft/ai'

export const craftConfig: CraftConfig = {
  plugins: [
    llmPlugin({
      providers: { anthropic: { apiKey: process.env.ANTHROPIC_API_KEY! } },
      defaultOptions: { temperature: 0.7 },
    }),
    embeddingPlugin({
      providers: { openai: { apiKey: process.env.OPENAI_API_KEY! } },
    }),
  ],
}

Plugins that manage additional concerns (like llmPlugin which also registers provider credentials) wrap defaultOptions inside a larger configuration object. See the Plugins reference for the full options of each plugin.

The direct adapter also supports a context-level channelType to swap all endpoints from in-memory to a distributed implementation. See Configuration.

Supported adapters

AdapterHow to set defaultsLocation
cron()CraftConfig.croncraft.config.ts
direct()CraftConfig.direct (channelType only)craft.config.ts
llm()llmPlugin({ defaultOptions })CraftConfig.plugins
embedding()embeddingPlugin({ defaultOptions })CraftConfig.plugins

How it works

Under the hood, merged options travel through a port: a named, typed token one plugin provides and anything else can look up. A config key such as cron installs a small plugin that provides the defaults through the adapter's port. When an adapter needs its options (in subscribe() or send()), it looks the port up on the context it runs in and combines the defaults with its own per-adapter options. Per-adapter values always win.

┌──────────────┐  provide()   ┌─────────────────┐
│ CraftConfig  │─────────────►│ port            │
│ cron: { ... }│              │ CRON_DEFAULTS   │
└──────────────┘              └───────┬─────────┘
                                      │ context.lookup()
                                      ▼
                              ┌─────────────────┐
                              │  CronAdapter    │
                              │  mergedOptions()│
                              │  { ...defaults, │
                              │    ...adapter } │
                              └─────────────────┘

A port's token is its identity, so two copies of the module that declares it in one process are refused at install (RC1103) rather than silently splitting the defaults in two.

Adding merged options to a custom adapter

If you are building a custom adapter and want to support merged options, follow these steps.

1. Define the options type

export interface MyAdapterOptions {
  apiKey?: string
  baseUrl?: string
  timeout?: number
}

2. Declare a port

Call port() once, at module scope, and export the token. The name is owner.capability@version:

import { port } from '@routecraft/routecraft'

export const MY_ADAPTER_DEFAULTS = port<Partial<MyAdapterOptions>>('acme.my-adapter.defaults@1')

3. Implement MergedOptions<T> on your adapter class

import { type MergedOptions, type CraftContext } from '@routecraft/routecraft'

class MyAdapter implements Destination<unknown>, MergedOptions<MyAdapterOptions> {
  readonly adapterId = 'acme.adapter.my-adapter'
  public options: Partial<MyAdapterOptions>

  constructor(options?: Partial<MyAdapterOptions>) {
    this.options = options ?? {}
  }

  mergedOptions(context: CraftContext): MyAdapterOptions {
    return {
      timeout: 5000,                               // built-in default
      ...context.lookup(MY_ADAPTER_DEFAULTS),      // context defaults
      ...this.options,                             // per-adapter overrides
    }
  }

  async send(exchange) {
    const opts = this.mergedOptions(getExchangeContext(exchange))
    // use opts.apiKey, opts.baseUrl, opts.timeout ...
  }
}

4. Create a plugin factory

Ship a companion plugin that provides the port, so users have a typed, discoverable API:

import { definePlugin } from '@routecraft/routecraft'

export function myAdapterPlugin(defaultOptions: Partial<MyAdapterOptions>) {
  return definePlugin({
    id: 'acme.my-adapter',
    provides: [MY_ADAPTER_DEFAULTS],
    bind(c) {
      c.provide(MY_ADAPTER_DEFAULTS, defaultOptions)
    },
  })
}

To offer a config key instead (defineProject({ myAdapter: {...} })), register the same factory with registerConfigApplier('myAdapter', myAdapterPlugin) and augment CraftConfig with the key.

5. Export both

Export the plugin and the port from your package, so another plugin can provide the defaults itself or read them.

export { myAdapterPlugin, MY_ADAPTER_DEFAULTS }

Adapters

How adapters are configured, at the call site and in craft config.

Configuration

Every CraftConfig field, including the cron and direct defaults.

Plugins

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

Previous
Creating adapters