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):
- Built-in defaults: hardcoded in the adapter (e.g.
temperature: 0forllm()) - Context defaults: registered in
craft.config.ts - 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
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 }
Related
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.