From baml
Use when composing multiple BAML functions into a typed pipeline — `let stage1 = ...; let stage2 = ...; Result { ... }`. Covers function-to-function composition with typed values flowing between stages, dispatch via `match` on enums / literal unions / `let <name>: <Type> =>` bindings, error propagation with `throws T` / `catch (e) { T => ... }` type-only arms, and fan-out patterns (sequential in BAML, parallel via host). Prerequisite: baml:core. Often paired with baml:llm-functions.
How this skill is triggered — by the user, by Claude, or both
Slash command
/baml:pipelinesThe summary Claude sees in its skill listing — used to decide when to auto-load this skill
Load this when the task chains multiple BAML functions. Composition stays in BAML so the workflow is typed end-to-end and testable without host code.
Load this when the task chains multiple BAML functions. Composition stays in BAML so the workflow is typed end-to-end and testable without host code.
For a single-function task, you don't need this skill.
class TriageResult {
email: Email,
intent: Intent,
draft: ReplyDraft,
score: int,
}
function judge_draft(email: Email, draft: ReplyDraft) -> int {
client: FastOpenAI
prompt #"
Score this reply from 1 to 5.
Email:
{{ email.body }}
Reply:
{{ draft.body }}
{{ ctx.output_format }}
"#
}
function triage_one(email: Email) -> TriageResult {
let intent = classify_email(email);
let draft = draft_reply(email, intent);
let score = judge_draft(email, draft);
TriageResult {
email: email,
intent: intent,
draft: draft,
score: score,
}
}
function triage_batch(raw_json: string) -> TriageResult[] {
let emails = load_emails(raw_json);
let results: TriageResult[] = [];
for (let email in emails) {
results.push(triage_one(email));
}
results
}
This shape works well in real agent runs: typed load at the boundary, small LLM stages, pure orchestration, typed results.
Things to notice:
triage_one, triage_batch) has no client: directive — it's not itself an LLM call.let is a stage you can inspect, test, and rewire.// Good — typed
class ParsedTicket { subject: string, body: string, priority: int, }
class Routed { ticket: ParsedTicket, queue: string, }
function parse_ticket(raw: string) -> ParsedTicket {
client: "openai/gpt-4o-mini"
prompt #"parse: {{ raw }} {{ ctx.output_format }}"#
}
function route_ticket(t: ParsedTicket) -> Routed {
client: "openai/gpt-4o-mini"
prompt #"route: {{ t }} {{ ctx.output_format }}"#
}
function handle(raw: string) -> Routed {
route_ticket(parse_ticket(raw))
}
Keep client: and prompt #"..."# on their own lines — the parser rejects one-liner LLM function bodies that combine them.
Don't pass JSON strings between stages. Typed values give compile-time guarantees and skip a redundant encode/decode.
matchenum Intent { Cancel, Refund, Question, Spam, }
class Response { message: string, }
function classify_intent(text: string) -> Intent {
client: "openai/gpt-4o-mini"
prompt #"classify the intent of: {{ text }} {{ ctx.output_format }}"#
}
function handle_cancel(text: string) -> Response {
client: "openai/gpt-4o-mini"
prompt #"cancel: {{ text }} {{ ctx.output_format }}"#
}
function handle_refund(text: string) -> Response {
client: "openai/gpt-4o-mini"
prompt #"refund: {{ text }} {{ ctx.output_format }}"#
}
function handle_question(text: string) -> Response {
client: "openai/gpt-4o-mini"
prompt #"question: {{ text }} {{ ctx.output_format }}"#
}
function handle_message(text: string) -> Response {
match (classify_intent(text)) {
Intent.Cancel => handle_cancel(text),
Intent.Refund => handle_refund(text),
Intent.Question => handle_question(text),
Intent.Spam => Response { message: "ignored" },
}
}
match (x) has parens around the scrutinee."openai/gpt-4o-mini", Haiku) for the classifier and the expensive one for the branch that actually does the work.let bindingsfunction json_to_string(value: json) -> string {
match (value) {
null => "null",
let s: string => s,
let n: int => baml.unstable.string(n),
let items: json[] => baml.json.stringify(items.to_json()),
let obj: map<string, json> => baml.json.stringify(obj.to_json()),
_ => "other",
}
}
let <name>: <Type> => binds the narrowed value inside the arm. Use it whenever you need to reach into the narrowed value, like serializing a sub-structure or pulling a field. _: <Type> => matches without binding when you don't need the value.
class StageError { stage: string, message: string, }
class Parsed { value: string, }
class Routed { destination: string, }
function parse_stage(s: string) -> Parsed throws StageError {
if (s.length() == 0) {
throw StageError { stage: "parse", message: "empty input" };
};
Parsed { value: s.trim() }
}
function route_stage(p: Parsed) -> Routed throws StageError {
Routed { destination: "queue:" + p.value }
}
function pipeline(s: string) -> Routed throws StageError {
route_stage(parse_stage(s))
}
function pipeline_safe(s: string) -> Routed? {
pipeline(s) catch (e) {
StageError => null,
}
}
throws T is part of the signature. The compiler enforces that callers either catch the error or re-throw (or propagate by also declaring throws T). Throw classes give callers a typed shape to match on; you can also throw strings or ints if the failure is simple.
catch is an expression — each arm produces a value compatible with the success-path type.
BAML's for / in is sequential. For real concurrency, fan out at the host layer (asyncio.gather, Promise.all — see baml:bridges).
When the parallelism is over a typed array and sequential is fine, write the loop in BAML:
function summarize_all(docs: string[]) -> string[] {
let out: string[] = [];
for (let d in docs) {
out.push(summarize(d)); // sequential
}
out
}
catch (e) { _ => ... } swallows the error type. Use a specific arm (catch (e) { StageError => ... }) so unexpected failures still propagate.baml.json.from_string<T>(raw) to exercise the parsing/orchestration paths deterministically. See baml:testing.for / in is sequential — for true concurrency, fan out at the host layer.{{ ctx.output_format }} on stages — every LLM stage with a typed return needs it.match and catch are expressions — each arm must produce a value compatible with the surrounding type. Don't put a ; after the closing }.Provides a checklist for code reviews covering functionality, security, performance, maintainability, tests, and quality. Use for pull requests, audits, team standards, and developer training.
npx claudepluginhub boundaryml/baml-skill --plugin baml