Skip to content

Why Nola

Nola adds notation for LLM inference to TypeScript. Its name comes from NOtation LAnguage. The aim is to let you write a request where you need its result, using TypeScript types to describe what should come back.

Every new kind of computation gets a notation

Section titled “Every new kind of computation gets a notation”

Syntax can make a recurring task easier to read. With async/await, asynchronous work fits into ordinary control flow. JSX gives UI trees a notation that can live beside the code that uses them. Nola applies a similar idea to inference.

An inference request needs an instruction, an expected result type, context and a model. Nola gives each of those a place: an extractor describes the request and type, contextual parameters supply data, and configuration chooses the model.

The compiler derives a schema from the TypeScript type and connects each ask to its source location. The runtime validates the reply before returning it. This lets you work from your existing types while keeping requests close to the code that uses their results.

Here is a function with two asks:

analyze.tsi
export infer function analyzeUserRequest(userId: string, .message: string) {
const ticketId = ask `ticket id mentioned in the message`: string;
const isFraud = ask `does the message look fraudulent`: boolean;
return { userId, ticketId, isFraud };
}
  • infer marks a function that returns a lazy Intent<T>. Its body runs when you resolve that intent.
  • .message is a contextual parameter. Its value is available to every ask in this invocation.
  • ask resolves an intent. An extractor such as ask `instruction`: T asks the model for a value of type T. Use ..`instruction`: T to create an extractor when it does not appear directly after ask.

Plain TypeScript imports the file directly and awaits the result:

main.ts
import { analyzeUserRequest } from "./analyze.tsi";
const result = await analyzeUserRequest("user-1", "Ticket TCK-4711: customer reports suspicious activity.");
// → { userId: "user-1", ticketId: "TCK-4711", isFraud: true }

The function still uses TypeScript for its arguments, local variables and return value. Nola lowers the added syntax to plain TypeScript for the rest of the toolchain. The mental model explains how intents, context and asks fit together.

Because Nola parses the inference syntax, it can connect the request to your types, context and source locations:

Because the compiler sees… You get
the type argument on an extractor A response schema derived from the TypeScript type. The runtime validates replies against it and converts supported wire values, such as ISO date strings, into their TypeScript representation. Changing an interface changes the derived schema; invalid model replies are caught at runtime.
which parameters carry a dot Shared context within an invocation. A second ask still sees .message, and an infer function invoked with ask inherits the caller’s context.
every ask site Editor support tied to the original source: completion inside ${…} holes, go-to-definition from .ts, diagnostics at .tsi positions and breakpoints inside infer functions.
the boundary where inference happens Receipts, telemetry, retries, timeouts, model routing (ask with fast …) and record/replay. These can be configured for the application or adjusted for individual asks.
how .tsi lowers to plain TS Integration with TypeScript, Node and supported bundlers through Nola’s tooling. Built applications run as JavaScript with the runtime and configured providers.

The comparison pages show the same task in Nola, BAML, the Vercel AI SDK and LangGraph. They are a useful next step if you want to compare how each approach expresses types, prompts and control flow.

Nola focuses on typed inference. A few boundaries help explain where it fits:

  • Workflow orchestration stays in your application. Use if, for, Promise.all and functions, or combine Nola with a workflow framework when you need durable state and checkpointing. See Nola vs LangGraph.
  • .tsi modules are imported directly. Nola tooling handles their compilation. You do not generate a separate client to call an infer function. See Nola vs BAML.
  • Prompt wording is yours to maintain. Nola composes prompts from your code and includes the result in receipts; it does not search for better wording.
  • The runtime runs in your server process. Configure a provider in nola.config.ts, either through Nola’s hosted inference service or with your own provider key. The Node loader requires Node ≥ 22.18.

.tsi is not valid TypeScript, so adopting Nola means adding a compilation step and editor support. Nola uses a vendored Babel parser with a Nola plugin, provides nola check for type-checking, and supplies a loader and bundler plugins. A project that uses only tsc will need these tools to work with .tsi source.

The tradeoff is whether keeping inference beside your TypeScript types and control flow is worth that tooling dependency for your project. The quick start gives you a small example to try before making that decision.

Next: Quick start — scaffold a project and run your first .tsi file offline.