Define once. Surface everywhere.
Trails is a contract-first TypeScript framework. Define a trail — typed input, Result output, examples, intent — and the framework projects it onto CLI, MCP, HTTP, or WebSocket. One definition, every surface, zero drift.
Trails ships CLI, MCP, and HTTP surfaces today. WebSocket is part of the architecture and roadmap, but not yet built.
Claude Code — add the marketplace, then install the plugin:
claude plugin marketplace add outfitter-dev/trails
claude plugin install trails@trailsCodex, Cursor, and others — install the skill:
npx skills outfitter-dev/trailsThe skill gives your agent the full Trails reference: lexicon, patterns, error taxonomy, surface wiring, testing, and before/after migration examples.
bunx @ontrails/trails createFollow the prompts — pick a name, choose a starter, select your surfaces. The scaffolder generates a working project with trails, a topo, surface wiring, and tests.
Or install manually:
bun add @ontrails/core @ontrails/cli @ontrails/commander zod
bun add -d @ontrails/testingapp.get('/api/projects/:id', async (req, res) => {
try {
const project = await db.projects.findById(req.params.id);
if (!project) {
return res.status(404).json({ error: 'Project not found' });
}
res.json(project);
} catch (err) {
console.error('Failed to fetch project:', err);
res.status(500).json({ error: 'Internal server error' });
}
});const show = trail('project.show', {
input: z.object({ id: z.string().describe('Project ID') }),
output: projectSchema,
intent: 'read',
examples: [
{ name: 'Found', input: { id: 'p_1' }, expected: { id: 'p_1', name: 'Acme' } },
{ name: 'Missing', input: { id: 'p_0' }, error: 'NotFoundError' },
],
blaze: async (input) => {
const project = await db.projects.findById(input.id);
if (!project) return Result.err(new NotFoundError(`Project ${input.id} not found`));
return Result.ok(project);
},
});Same logic. But now the framework derives:
- CLI:
myapp project show --id p_1with--helptext, exit code 2 for not-found - MCP: tool
myapp_project_showwith JSON Schema input,readOnlyHintannotation - Tests: both examples run as assertions —
testAll(graph)validates the happy path and the error path - Governance: warden checks for throws, surface-specific imports in trail code, missing output schemas
You authored the contract. The framework did the rest.
Each declaration you add to a trail unlocks derived behavior across every surface:
| You add | You get for free |
|---|---|
input (Zod schema) |
CLI flags + --help text, MCP JSON Schema, input validation |
output (Zod schema) |
Contract tests, MCP response typing, surface map entries |
intent: 'read' |
MCP readOnlyHint, CLI skips confirmation, HTTP GET |
intent: 'destroy' |
MCP destructiveHint, CLI auto-adds --dry-run, HTTP DELETE |
examples |
Tests (happy + error path), agent guidance, documentation |
crosses |
Composition graph, cycle detection, cross coverage in tests |
resources: [db] |
Singleton lifecycle, test mock auto-resolution, warden governance |
detours |
Recovery paths, detour contract validation, shadowing checks |
The value isn't any single feature. It's that they multiply — each declaration makes every surface smarter without additional wiring.
import { trail, topo, Result } from '@ontrails/core';
import { surface as cliSurface } from '@ontrails/commander';
import { surface as mcpSurface } from '@ontrails/mcp';
import { z } from 'zod';
// 1. Define trails
const greet = trail('greet', {
input: z.object({ name: z.string().describe('Who to greet') }),
output: z.object({ message: z.string() }),
intent: 'read',
blaze: (input) => Result.ok({ message: `Hello, ${input.name}!` }),
});
// 2. Collect into topo
const graph = topo('myapp', { greet });
// 3. Open surfaces with any adapter
await cliSurface(graph); // CLI
// await mcpSurface(graph); // MCP — same trails, same run functionThe same topo can be opened on HTTP today with @ontrails/hono. WebSocket follows the same peer-surface model, but is still planned.
$ myapp greet --name World
{ "message": "Hello, World!" }| Package | What it does |
|---|---|
@ontrails/core |
Result, errors, trail/signal/contour/topo, validation, schema derivation |
@ontrails/cli |
CLI command model - flag derivation, output formatting |
@ontrails/commander |
Commander adapter for the CLI surface |
@ontrails/mcp |
MCP surface — tool generation, annotations, progress bridge |
@ontrails/http |
HTTP surface model — route derivation, verb mapping, error responses |
@ontrails/hono |
Hono adapter that opens a topo on the HTTP surface |
@ontrails/store |
Backend-agnostic store definitions, typed accessors, adapter-support helpers |
@ontrails/testing |
testAll(), testTrail(), testCrosses(), contract testing, surface harnesses |
@ontrails/topographer |
Surface maps, semantic diffing, lock files for CI governance |
@ontrails/observe |
Log and trace sink contracts, sink composition, built-in sinks, trace rendering |
@ontrails/tracing |
Tracing compatibility, query/status trails, trails.db dev-state storage, sampling helpers, OTel adapter |
@ontrails/logtape |
Adapter that forwards Trails log records to a LogTape-shaped logger |
@ontrails/warden |
AST-based convention rules, drift detection, CI formatters |
See docs/index.md for the full guide, organized by what you're trying to do.
bun run build # Build all packages
bun run test # Run all tests
bun run lint # Lint with oxlint
bun run typecheck # TypeScript strict modev1 beta. The contract layer, CLI/MCP/HTTP surfaces, trails topo and trails dev workflows, shared trails.db, tracing-backed developer state, schema-derived stores, and the Drizzle runtime are implemented and shipping. The WebSocket surface is designed but not yet built. See Horizons for what's next.