From routecraft-skills
Authors a new Routecraft capability (workflow, automation, MCP tool, webhook handler, or scheduled job). Use when composing adapters into a typed pipeline with sources, operations, and destinations.
How this skill is triggered — by the user, by Claude, or both
Slash command
/routecraft-skills:create-capabilityThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
A capability is the user-facing unit of automation in Routecraft. It is a typed pipeline that starts at a source (`from(...)`), flows through operations (`transform`, `enrich`, `filter`, `validate`, `split`, `aggregate`, `choice`, `process`, `tap`), and lands at one or more destinations (`to`). The codebase calls these "routes" internally because that is what the `craft()` builder returns; in u...
A capability is the user-facing unit of automation in Routecraft. It is a typed pipeline that starts at a source (from(...)), flows through operations (transform, enrich, filter, validate, split, aggregate, choice, process, tap), and lands at one or more destinations (to). The codebase calls these "routes" internally because that is what the craft() builder returns; in user-facing language and in the docs they are capabilities.
You are writing this capability for the user. Treat the linter (bun run lint) as authoritative once you have written the code: if it disagrees, the linter wins.
Use this skill when the user asks to:
If the user only needs a small utility function with no I/O and no orchestration, that does not need to be a capability. If it crosses systems, has retry semantics, or should be discoverable, it does.
Confirm answers to these questions before writing. Ask the user only the ones that are not already obvious from context.
input and output if the user knows what they wantsplit then aggregate)? Branch (choice)? Conditional drop (filter)? Schema check (validate)?.batch({...}) before .from(...).error(...) is built in; others are coming)Read reference/examples-index.md and pick the row that best matches the answers above. The index maps intent to a public doc page and the closest existing capability on GitHub.
Then, in this order:
WebFetch the linked doc page (raw markdown variant on routecraft.dev/raw/docs/...)WebFetch the linked example file on GitHub (use the raw.githubusercontent.com URL)Read examples/src/<closest>.ts end to endDo not write from memory. Capabilities look small but the operator order, schema placement, and direct-call id linking are easy to get wrong without a reference.
The DSL is fluent and mostly self-documenting once you have an example open. Common shape:
import { craft, simple, http, log } from "@routecraft/routecraft";
import { z } from "zod";
const Input = z.object({ /* ... */ });
type Input = z.infer<typeof Input>;
export default craft()
.id("my-capability") // required for direct-call routing
.title("Human-readable title") // surfaced in MCP tools and the TUI
.description("What this does") // surfaced in MCP tools
.input({ body: Input }) // typed and validated at the boundary; retypes the chain
.from(/* source */) // body is already typed as the Input schema output
// operations
.to(/* destination */);
Authoring rules to keep in mind:
route.ts exists so a reader can follow the flow (where data comes from, what happens to it in order, where it lands) without reading the inner workings of every step. A wall of inline logic defeats that. Never inline a large transform, process, enrich, or filter body. Extract anything beyond a couple of trivial lines into a named function in a sibling internal file in the capability folder (e.g. summarise.ts, map-order.ts) and pass the reference: .transform(toOrderLine) instead of .transform((x) => { /* 30 lines */ }). The named step then reads like a verb in the pipeline. Inline lambdas are fine only when they are short and self-evident.id(), .title(), .description(), .input(), .output(), .error(), .batch() come before .from(...). Once you call .from(...), you are in the pipeline and metadata methods no longer apply.input({ body: Schema }) before .from(...); the chain is retyped from the schema's inferred output, so no .from<Input>(...) generic is needed. An explicit .from<T>(...) still overrides the inferred type when you need to.tap(destination).to(dest) -- send and ignore the destination's result body (terminal or pass-through with original body).enrich(dest) -- merge the destination's result into the body.tap(dest) -- fire and forget; do not wait, do not change the body.split() you can chain operations on each item; close with .aggregate() to fan back in.error(handler) at route scope catches anything that escapes the pipeline; at step scope, attach it to a single step@standard-schema/spec). Zod and Valibot both work because both implement Standard Schema. Use @routecraft/routecraft's helpers in shared code, not Zod directlyThis is a documented Routecraft standard, not a suggestion. bunx create-routecraft scaffolds this shape, and the project-structure page is the source of truth: https://routecraft.dev/raw/docs/introduction/project-structure.md (read it if you are setting up a new project or unsure where a file belongs).
Each capability is its own folder under capabilities/, grouped by domain:
capabilities/
<domain>/
<id>/
route.ts # public surface AND the readable main flow (default export + input/output types)
route.test.ts # colocated test (see Step 4)
README.md # short description; mermaid + integrations table for complex ones
<internal>.ts # mappers, transforms, helpers; never imported from outside this folder
Rules:
route.ts is the readable main flow. It is both the public surface and the file a human reads to understand what the capability does. Keep it to the DSL chain plus its schemas: a reader should follow the flow top to bottom without paging through transform internals. The heavy logic lives in the sibling internal files and is pulled in by name (see the readability rule in Step 3). If route.ts is becoming hard to scan, that is the signal to extract, not to add a comment.route.ts is the only file other capabilities may import. Re-export the capability's input/output types from it so callers depend on the contract, not the internals.summarise.ts, map-order.ts, __fixtures__, ...) hold the implementation detail of each step. They are private to the folder; never import them from another capability.direct('<id>') plus the types re-exported from the callee's route.ts. Never reach into another capability's internal files.shared/ folder next to capabilities/. Any capability may import from shared/; keep it pure (validators, parsers, formatters, types), with no side effects and no imports back into a capability's internals. This is the single-project answer, so a one-app repo never needs workspace tooling just to share a date parser. Once the repo grows into multiple apps under apps/, shared helpers graduate from shared/ to a workspace package each app depends on.capabilities/<id>.ts) is acceptable shorthand for a trivial, internal-free capability, but the folder shape is the default the scaffolder produces.In a project that follows the folder-per-capability layout, colocate the test as route.test.ts next to route.ts. When contributing inside this monorepo's packages, tests instead live in the package's test/ directory (packages/<pkg>/test/<name>.test.ts). Every test must have JSDoc with @case, @preconditions, @expectedResult. Use @routecraft/testing and follow the canonical lifecycle:
import { describe, it, expect, afterEach } from "vitest";
import { testContext, type TestContext } from "@routecraft/testing";
import myCapability from "../src/my-capability";
describe("my-capability", () => {
let t: TestContext | undefined;
afterEach(async () => {
if (t) await t.stop();
t = undefined;
});
/**
* @case happy path
* @preconditions a valid input body
* @expectedResult the capability completes without errors
*/
it("transforms the body and sends to destination", async () => {
t = await testContext().routes([myCapability]).build();
await t.test();
expect(t.errors).toHaveLength(0);
});
});
Errors thrown inside handlers are caught at the boundary and surfaced on t.errors; do not expect t.test() to reject. Full test pattern: https://routecraft.dev/raw/docs/introduction/testing.md
Run, in this order, until each is clean:
bun run typecheck
bun run lint
bun run test
Use bun run <script> (not bun <script>) so Bun invokes the package.json script rather than its built-in test runner. If bun run lint complains, fix the capability rather than silencing the rule. The linter encodes Routecraft's authoring rules. If it does not catch something the user expected it to catch, that is a follow-up for the lint package.
npx claudepluginhub routecraftjs/routecraft --plugin routecraft-skillsProvides CDSS development patterns for drug interaction checking, dose validation, clinical scoring (NEWS2, qSOFA), and alert classification integrated into EMR workflows.