Nola extends TypeScript with native syntax for LLM inference, much as async / await made asynchronous code feel native.
// 1) A TS type is the contract. Nola derives its JSON schema; JSDoc goes in too.
interface Person {
name: string;
age: number;
/** the employer, or "unknown" */
employer: string;
job: string;
}
// 2) `infer` declares an inference function: it returns Intent<T>, like async returns Promise<T>.
function extractPerson(text: string) {
return __nola.intents.Intent(async (__frame) => { void text;
// 3) ..`prompt`<T> builds the extraction Intent<T> and validates the reply against T.
// 4) `ask` resolves it, like `await` resolves a Promise<T>.
const person = await __nola.ask(__nola.intents.ExtractIntent<Person>({ instruction: `the person described in the text`, type: __nola_type_$2(), loc: "14:22", def: "b7b6d537457a6fbc9988b6a91ef6a1bf11dbbd5efd493ee82f50ffeb2dace4ba" }), __frame);
return person;
}, __nola_file_ctx().func({ fn: "extractPerson", instruction: "", args: [{ name: "text", type: __nola_type_$1(), contextual: true, value: text }] }));
}
// 5) An Intent<T> is thenable: from plain TS you would import the function and await it, here we await it in place.
const person = await extractPerson(
"Ada Lovelace, 36, worked with Charles Babbage on the Analytical Engine as a mathematician.",
);
console.log(person);
import { __nola } from "@nola-lang/runtime";
__nola.useRuntime(16);
function __nola_file_ctx() { return __nola.context.file("playground.tsi"); }
function __nola_type_$1(): import("@nola-lang/runtime").InferType<unknown> | undefined { return __nola.types.string(); }
function __nola_type_$2(): import("@nola-lang/runtime").InferType<unknown> { return __nola.types.ref("Person", __nola_type_Person); }
function __nola_type_Person(): import("@nola-lang/runtime").InferType<unknown> { return __nola.types.object({ name: __nola.types.string(), age: __nola.types.number(), employer: __nola.types.string().describe("the employer, or \"unknown\""), job: __nola.types.string() }); }
// 1) A TS type is the contract. Nola derives its JSON schema; JSDoc goes in too.
interface Person {
name: string;
age: number;
/** the employer, or "unknown" */
employer: string;
job: string;
}
// 2) `infer` declares an inference function: it returns Intent<T>, like async returns Promise<T>.
infer function extractPerson(.text: string) {
// 3) ..`prompt`<T> builds the extraction Intent<T> and validates the reply against T.
// 4) `ask` resolves it, like `await` resolves a Promise<T>.
const person = ask ..`the person described in the text`<Person>;
return person;
}
// 5) An Intent<T> is thenable: from plain TS you would import the function and await it, here we await it in place.
const person = await extractPerson(
"Ada Lovelace, 36, worked with Charles Babbage on the Analytical Engine as a mathematician.",
);
console.log(person);
The playground uses hosted inference. No account or API key required.
Run it in the playground →You already know this grammar. Read the two functions side by side: infer stands where async stood, ask where await stood, Intent where Promise stood. Everything else keeps its shape.
Same file, one letter apart — the i in .tsi is inference.
Node ≥ 22. The starter replays a committed ledger, so no API key is needed until you switch to a live provider or the Nola Platform. Already have a project? npm create nola@latest -- --add wires Nola into it.
An SDK call spreads one decision across a schema, a prompt, and a type that must stay in sync. Nola makes it a single typed expression that the compiler, editor, and runtime all understand.
Same task, same model, on every tab. Samples follow each SDK’s current documentation, checked August 2026. The snippets are trimmed to the decision; the repo has each stack as a complete installable project.
Full runnable projects for every tab →Six ideas, and you’ve seen the whole language — tap through, none of them needs more than a dozen lines.
infer function declares an LLM-backed function the way async declares a concurrent one. Dot-prefixed params (.bio) are context the model sees; plain params stay ordinary values. Import it from plain TS like anything else.
The dot is the whole API. .bio’s value is composed into the prompt of every ask in the call; team stays an ordinary JavaScript argument — its name and type reach the model, its value never does.
One dot in. Context is a parameter, not a string you assemble.
infer function declares an LLM-backed function the way async declares a concurrent one. Dot-prefixed params (.bio) are context the model sees; plain params stay ordinary values. Import it from plain TS like anything else.
The dot is the whole API. .bio’s value is composed into the prompt of every ask in the call; team stays an ordinary JavaScript argument — its name and type reach the model, its value never does.
One dot in. Context is a parameter, not a string you assemble.
nola console is a local trace viewer. Run it once, point an app at it, and every ask shows up as it resolves — grouped by project, by call, and by the line of .tsi that authored it.

The runtime already emits an event for each step of an ask and a receipt when it finishes. The console stores that stream in a per-machine SQLite file and lays it out as a tree:
The Asks view turns the tree sideways: every execution of one authored ask — keyed by its text and type, not its line number — with a duration chart, so a prompt you are tuning is one page.
The same events reach your own code: add a hook with any of onAskStart … onInvocationEnd to nola.config.ts, or set NOLA_LOG=debug for the built-in logger. Hooks observe only — one that throws can never break an ask.
JSX made markup a language feature. Nola does the same for inference. A .tsi file reads like TypeScript — but tsc never sees it: Nola owns the parse, lowers every ask to plain TS, and a provider fills in the type at run time.
Three phases for every .tsi file, and only the first is yours. You write TypeScript with two new constructs; the toolchain and runtime do the rest. No schema DSL, no graph builder, no prompt library.
Everything you know about TypeScript still applies. Add infer functions, dot-prefixed context params (.message) the model can see, and ask ..`prompt`<T> wherever you need a value from the LLM. That’s the whole job.
Before tsc, the bundler, Node, or the editor sees it, the toolchain rewrites .tsi to ordinary TypeScript with a source map — the JSX model. JSON Schemas for every <T> are derived from your types at compile time. Nothing for you to run or configure.
Calling an infer function returns a lazy Intent<T>. Awaiting it composes the context, asks the configured provider, validates the reply against the schema (retrying if it drifts), and hands back a real T — plus a receipt for every ask.
.tsi lowers to TypeScript with a source map, so every editor is a thin client over one language server. VS Code ships today; Zed and JetBrains are on the way. Your coding agent reads it too.
nola.nola-vscodeThe full editor story today: language server, tsserver plugin and debugger, all aware of .tsi positions.
zedExtension in progress — same language server, packaged for Zed.
webstorm · ideaPlugin in progress — WebStorm and IntelliJ IDEA first.
One command writes the real .tsi grammar into your repo — .claude/skills/, .cursor/rules/, AGENTS.md — so your agent reads the actual rules, not whatever it guessed. The files are self-contained and version-stamped: they work for your whole team, and a re-run tells you what has gone stale.
Run it bare and it detects the agents your project already uses; nola init offers the same step.
Any editor: nola check reports errors at .tsi positions from the terminal, and the bundled tsserver plugin types .tsi imports from plain TS.
“Just plain TypeScript, as far as I can tell.”
“I bundled it. Didn’t notice a thing.”
“Thenable. I awaited it. No further questions.”
“Breakpoints bound on the first try.”
Scaffold a project in one command — it runs offline out of the box, so you can read, edit and re-run .tsi before you ever paste an API key.