Reference

Routecraft cheat sheet.

The whole fluent API on one page. Searchable, copyable, and print-to-PDF ready.

v0.6.0TypeScript

Or press Cmd/Ctrl + P

Setup

Installation

# Core library
bun add @routecraft/routecraft

# AI / MCP integration
bun add @routecraft/ai

# CLI (run routes from the terminal)
bun add -g @routecraft/cli

CLI runs TypeScript directly via Bun (Bun >= 1.1.0). Node 22+ also works.

Concept

Route + Context

import { craft, ContextBuilder } from '@routecraft/routecraft'

const route = craft()
  .id('my-route')
  .from(source)
  .transform(x => x.body)
  .to(destination)
  .build()

const ctx = await new ContextBuilder()
  .routes(route)
  .build()

await ctx.start()
await ctx.stop()

DSL

Builder DSL (fluent API)

Chain operations to build a type-safe pipeline. Types flow through each step.

craft()
  .id('pipeline')                  // unique route name
  .title('Pipeline')               // display name (agents, docs)
  .description('...')              // doc string used in errors
  .tag('idempotent')               // route classification
  .input({ body: schema })         // input validation
  .output({ body: schema })        // output validation
  .authorize({ roles: ['admin'] }) // route guard (auth)
  .from(source)                    // source adapter
  .authenticate(resolver)          // mint trusted principal
  .transform(body => body)         // pure body function
  .process(ex => ex)               // full exchange access
  .filter(ex => ex.body.age > 18)  // drop if false
  .validate(schema(zodSchema))     // any Standard Schema
  .header('key', 'val')            // set a header
  .enrich(other)                   // fetch and merge
  .tap(log())                      // side effect, non-blocking
  .split()                         // fan-out per array item
  .aggregate()                     // collect back into one
  .choice(c => c.when(...))        // conditional branching
  .to(destination)                 // destination adapter
  .error((e, ex, fwd) => ...)      // step-scope handler
  .build()

Inputs

Sources

// Static value or async function
.from(simple({ hello: 'world' }))
.from(simple(() => fetch('/api')))

// Emit on an interval
.from(timer({ intervalMs: 5000 }))

// Cron schedule
.from(cron('0 9 * * *'))
.from(cron('0 9 * * *',
  { timezone: 'America/New_York' }))

// In-process direct endpoint
.from(direct('my-endpoint'))

// IMAP mail (push via IDLE)
.from(mail('INBOX',
  { unseen: true, markSeen: true }))

// In-process EventEmitter
.from(event({ eventName: 'order' }))

// File and file-format sources
.from(file({ path: './data.txt' }))
.from(json({ file: './data.json' }))
.from(jsonl({ file: './events.jsonl' }))
.from(csv({ file: './rows.csv' }))
.from(html({ html: '<table>...</table>' }))

Outputs

Destinations

// Log to console
.to(log())
.to(debug(ex => ex.body))

// Discard the exchange
.to(noop())

// HTTP request
.to(http({
  method: 'POST',
  url: 'https://example.com',
  body: ex => ex.body,
}))

// Dynamic URL
.to(http({ url: ex => `/${ex.body.id}` }))

// In-process direct endpoint
.to(direct('my-endpoint'))

// Write file or file format
.to(file({ path: './out.txt' }))
.to(json({ file: './out.json' }))
.to(jsonl({ file: './out.jsonl' }))
.to(csv({ file: './out.csv' }))

// Send email via SMTP
.to(mail({
  to: ex => ex.body.email,
  subject: 'Hello',
  text: ex => ex.body.text,
}))

Envelope

Exchanges

type Exchange<T> = {
  id: string
  body: T
  headers: ExchangeHeaders
  logger: Logger
}

Access patterns

// .transform() gets the body
.transform(body => body.toUpperCase())

// .process() gets the full exchange
.process(ex => ({
  ...ex,
  body: { ...ex.body, ts: Date.now() },
}))

// .filter() gets the full exchange
.filter(ex => ex.body.age > 18)

Flow

Split & aggregate

Fan out an array, process each item, collect back.

craft()
  .from(simple([1, 2, 3]))
  .split()                  // 3 exchanges
  .transform(n => n * 2)    // each runs once
  .aggregate()              // [2, 4, 6]
  .to(log())

Flow

Choice & branching

First matching when wins. halt() short-circuits the branch.

craft()
  .from(source)
  .choice(c => c
    .when(ex => ex.body.priority === 'urgent',
      b => b.to(urgentQueue))
    .when(ex => ex.body.amount > 1000,
      b => b.to(reviewQueue))
    .otherwise(
      b => b.to(errorSink).halt()))
  .to(defaultDestination)

Lookup

Enrich (fetch & merge)

// Default: deep merge
.enrich(http({ url: '/api/user' }))

// Custom merge strategy
.enrich(dest, (orig, fetched) => ({
  ...orig.body,
  meta: fetched.body,
}))

// Merge helpers
.enrich(dest, only('meta'))
.enrich(dest, replace())

Safety

Validation (Standard Schema)

Works with Zod, Valibot, ArkType, or any Standard Schema library.

import { z } from 'zod'
import { schema } from '@routecraft/routecraft'

craft()
  .from(direct('input'))
  .validate(schema(z.object({
    email: z.string().email(),
    age: z.number().min(18),
  })))
  .to(log())
// body type is inferred from the schema

Observability

Events system

// Context lifecycle
ctx.on('context:started', () => {})
ctx.on('context:error', ({ details }) => {
  // { error, route?, exchange? }
})

// Route lifecycle (route id required)
ctx.on('route:my-route:started', () => {})

// Exchange tracking (use * for all routes)
ctx.on('route:*:exchange:completed',
  ({ details }) => {
    // { exchange, duration }
  })

// Step-level tracing
ctx.on('route:*:step:completed',
  ({ details }) => {
    // { operation, adapter, duration }
  })

// Wildcards
ctx.on('route:*:exchange:*', () => {})
ctx.on('plugin:*:started', () => {})

Recovery

Error handling

craft()
  .error((error, exchange, forward) => {
    // Return a recovery value
    return { recovered: true }

    // Or forward to a dead-letter route
    return forward('dlq', {
      source: error.message,
    })
  })
  .from(source)
  .to(destination)

Error code ranges

RC1xxx  Definition
RC2xxx  DSL
RC3xxx  Lifecycle
RC5xxx  Adapter (incl. auth)
RC9xxx  Testing

Runtime

Context & plugins

const ctx = await new ContextBuilder()
  .add({
    crm: { timezone: 'UTC' },
    direct: { channelType: 'memory' },
    mail: { accounts: { /* ... */ } },
    plugins: [myPlugin],
  })
  .on('context:started', () => {})
  .store('custom-key', value)
  .routes(route1, route2)
  .build()

const myPlugin: CraftPlugin = {
  async apply(ctx) {
    ctx.store('key', new Map())
  },
  async teardown(ctx) { /* cleanup */ },
}

AI

LLM destination

Model id is provider:model. Providers (Anthropic, OpenAI, Ollama, Gemini, OpenRouter) are registered via llmPlugin.

import { llm } from '@routecraft/ai'

// Basic call (user prompt defaults to body)
craft()
  .from(direct('text-in'))
  .to(llm('anthropic:claude-sonnet-4-6', {
    system: 'Summarize concisely',
    user: ex => ex.body.text,
    temperature: 0.2,
  }))
  .to(log())

// Structured output (body.output is typed)
.to(llm('openai:gpt-4o', {
  system: 'Extract contact info',
  output: z.object({
    email: z.string().email(),
    name: z.string(),
  }),
}))

AI

Agents & tools

agent() runs a multi-turn tool-calling loop. tools() selects from MCP tools, registered functions, and direct routes.

import { agent, tools } from '@routecraft/ai'

craft()
  .id('assistant')
  .from(direct('chat-in'))
  .to(agent({
    model: 'anthropic:claude-sonnet-4-6',
    system: 'You are a helpful assistant.',
    user: ex => ex.body.message,
    tools: tools([
      'CurrentTime',              // registered fn
      'Direct(greet-user)',       // direct route as tool
      'MCP(github:create_issue)', // single MCP tool
      { tagged: 'read-only' },    // by tag
    ]),
  }))
  .to(log())

Register functions and direct-route tools once via agentPlugin({ functions: { CurrentTime: currentTime(), greetUser: directTool('greet-user') } }).

AI

MCP integration

Expose a route as an MCP tool

import { mcp } from '@routecraft/ai'

craft()
  .id('fetch-page')
  .title('Fetch page')
  .description('Fetch webpage content')
  .tag('read-only')
  .input({ body: z.object({
    url: z.string().url(),
  }) })
  .from(mcp())
  .enrich(http({ url: ex => ex.body.url }))
  .to(log())

Call an MCP server

// As a destination (invoke a remote tool)
.to(mcp('github', 'search'))

Protect with auth

import { jwt } from '@routecraft/routecraft'

craft()
  .id('private-tool')
  .authorize({ scopes: ['tools:read'] })
  .from(mcp({
    auth: jwt({ jwksUri: '...' }),
  }))
  .to(log())
// Use WorkOS / Clerk presets via mcpPlugin

Claude Desktop config

{
  "mcpServers": {
    "routecraft": {
      "command": "bunx",
      "args": ["@routecraft/cli", "run",
        "./capabilities/index.ts"]
    }
  }
}

Terminal

CLI & TUI

Run routes

# Run a route file
craft run ./my-route.ts

# With debug logging
craft run ./my-route.ts \
  --log-level debug \
  --log-file ./craft.log

Inspect with the TUI

Live event and exchange inspector. Requires the telemetry plugin enabled on the context.

# Launch the TUI (reads the telemetry DB)
craft tui

# Or point it at a specific DB
craft tui --db ./app/telemetry.db