Skip to content

Nola vs BAML

BAML is a language for defining LLM functions in .baml files. You declare inputs, output types, and prompts, then generate a client for your application. The client parses responses into the declared types. The ctx.output_format macro adds output-format instructions to a prompt. See BAML’s prompting guide.

Both BAML and Nola return typed values. The main difference in this example is where the output type lives: a BAML class with a generated client, or a TypeScript type used by a Nola extractor.

Both examples classify a support message into a category and order IDs, then write a reply using that category. These snippets focus on the two calls; provider credentials and project setup are omitted.

BAML: define two functions in .baml, configure a TypeScript generator, and call the generated client.

baml_src/triage.baml
class Triage {
category "billing" | "refund" | "fraud" | "other"
orderIds string[]
}
function ClassifyMessage(message: string) -> Triage {
client "openai/gpt-5-mini"
prompt #"
{{ _.role("system") }} You are a support classifier.
{{ ctx.output_format }}
{{ _.role("user") }} {{ message }}
"#
}
function WriteReply(category: string, message: string) -> string {
client "openai/gpt-5-mini"
prompt #"
{{ _.role("system") }} You are a support agent.
{{ _.role("user") }} Reply to this {{ category }} case: {{ message }}
"#
}
// not-checked — triage.ts, after `npx baml-cli generate`
import { b } from "./baml_client";
const triage = await b.ClassifyMessage(message);
const reply = await b.WriteReply(triage.category, message);

Nola: define the type and both asks in a .tsi module.

classify.tsi
export type Triage = {
category: "billing" | "refund" | "fraud" | "other";
orderIds: string[];
};
export infer function classifyMessage(.message: string) {
const triage = ask `triage the customer message`: Triage;
const reply = ask `a reply for a ${triage.category} case`: string;
return { ...triage, reply };
}
  • Clients in several languages. A BAML definition can generate clients for TypeScript, Python, Ruby, and other targets. This is useful when services in different languages share prompts and types. See the generator reference.
  • Prompt testing. BAML’s VS Code extension provides a playground and runs test blocks alongside function definitions. See Testing functions.
  • Reusing TypeScript types. Nola derives the schema from Triage during compilation. If the domain types already belong to a TypeScript application, they can remain there. BAML’s generated types can instead serve as the application’s source of truth.
  • Shared context. Marking .message makes it available to both asks. In the BAML example, the TypeScript caller passes the message to each function explicitly.
  • Keeping steps together. Nola lets you put asks, branches, loops, and ordinary function calls in the same .tsi function and check them with nola check. BAML callers can also use TypeScript control flow around generated functions.

Schema derivation happens at compile time; validation of a model response happens at runtime. Neither approach makes the meaning of a model’s answer a compile-time guarantee.

BAML fits a project that needs clients in several languages or prefers a dedicated prompt language and testing workflow.

Nola fits a TypeScript project that wants to use its existing types for extraction and keep model calls next to application logic. It requires adopting .tsi files and the Nola toolchain.

Next: Nola vs LangGraph