Project anatomy
The starter templates put inference code in src/ and provider settings in nola.config.ts. The default feature-extraction template runs directly from src/main.tsi. function-calling adds a TypeScript helper, while typescript-interop separates the infer function from a plain TypeScript entry point.
The tree below shows the files used by these templates; the files under src/ depend on which one you choose.
my-app/ nola.config.ts # provider and runtime options nola.replay.jsonl # offline scaffold only: the template's recorded answers package.json # runtime + providers as dependencies, nola-lang as a devDependency tsconfig.json # NodeNext, allowArbitraryExtensions, include: ["src"] .gitignore # node_modules, dist, *.tsbuildinfo, *.d.tsi.ts, .env, .env.* README.md # the template's own notes src/ main.tsi # feature-extraction / function-calling: the program — `.` bindings and top-level asks; feature-extraction opens with a context statement tickets.ts # function-calling: the plain async function the top-level ask calls person.tsi # typescript-interop: types + infer functions main.ts # typescript-interop: plain TypeScript entry point .env # NOLA_API_KEY when you chose Nola as the provider; otherwise yours to create (ignored by git) .env.example # vendor scaffolds only: the slot for that vendor's key (OPENAI_API_KEY=), to copy to .envnola.config.ts
Section titled “nola.config.ts”Choosing Nola as the provider creates this config:
import { defineConfig } from "@nola-lang/runtime";
// Nola serves inference: reads NOLA_API_KEY from .env (your free trial key)// and picks the model. Out of runs? `npx nola-lang account` opens your Nola// account, where prepaid balance is added — or bring your own model and keep// everything else:// import { openai } from "@nola-lang/providers";// model: openai("gpt-5-mini"), // reads OPENAI_API_KEYexport default defineConfig({ model: "nola",});"nola" is shorthand for nola.infer(), which uses Nola’s hosted inference service. The offline versions of feature-extraction, function-calling and typescript-interop use model: replay("./nola.replay.jsonl") to read recorded answers without an API key.
The config lives at the project root and default-exports a defineConfig call. model accepts one model or a map with a required default entry. Other entries can be selected with ask with <name>.
Import defineConfig, nola and terminalTrace from @nola-lang/runtime. Vendor models, mocks and provider helpers come from @nola-lang/providers. See nola.config.ts for all options.
src/main.tsi (feature-extraction)
Section titled “src/main.tsi (feature-extraction)”`You read short professional bios. Answer from the text alone; never invent facts.`
interface Person { name: string; age: number; employer: string; job: string;}
const .message = "Alice Smith, 32, is a staff engineer at Acme Corp working on distributed systems.";
const person = ask `the person described in the text`: Person;
const .role = person.job;const seniority = ask `the seniority level the role implies`: "junior" | "mid" | "senior" | "staff";
console.log(JSON.stringify({ ...person, seniority }));Run this file with nola run src/main.tsi. The command loads the config and executes the top-level asks.
The first template literal is a context statement: every ask below it sees its text. const .message and const .role are contextual bindings: subsequent asks receive their values. Here, the first ask’s answer supplies .role for the second ask. Each ask `…`: T resolves an extractor; see module-level asks for the rules.
The function-calling template also starts in src/main.tsi. It uses a call intent to fill the arguments of createTicket, an async function imported from src/tickets.ts with the NodeNext ./tickets.js specifier.
To reuse inference from another part of your application, put the asks in an infer function, as the next example shows.
src/person.tsi (typescript-interop)
Section titled “src/person.tsi (typescript-interop)”export interface Person { name: string; age: number; employer: string; job: string;}
export infer function extractPerson(.message: string) { const person = ask `the person described in the text`: Person; return person;}Person defines the result type and supplies the schema for the extractor. .message gives the model the input text. The function returns the validated result of the ask.
src/main.ts (typescript-interop)
Section titled “src/main.ts (typescript-interop)”import { extractPerson } from "./person.tsi";
const person = await extractPerson( "Alice Smith, 32, is a staff engineer at Acme Corp working on distributed systems.",);console.log(JSON.stringify(person));The import keeps the .tsi extension. Calling extractPerson creates an intent; awaiting it runs the function. Use nola run src/main.ts to execute this entry with the loader and config.
tsconfig.json
Section titled “tsconfig.json”{ "compilerOptions": { "strict": true, "target": "ES2022", "module": "NodeNext", // hence ./x.js specifiers for plain-TS imports "moduleResolution": "NodeNext", "allowArbitraryExtensions": true, // required for the .tsi declaration pairs "noEmit": true, // nola build emits; tsc only checks "skipLibCheck": true }, "include": ["src"] // directory-style, so the editor can admit .tsi files}package.json
Section titled “package.json”{ "name": "my-app", "version": "0.0.0", "private": true, "type": "module", "scripts": { "start": "nola run src/main.tsi", "build": "nola build", "check": "nola check" }, "dependencies": { "@nola-lang/providers": "^0.1.14", "@nola-lang/runtime": "^0.1.14" }, "devDependencies": { "nola-lang": "^0.1.14", "typescript": "^5.6.0" }, "engines": { "node": ">=22.18" }}start runs the template’s entry, which is src/main.ts for typescript-interop. The runtime and providers are application dependencies. nola-lang supplies the compiler, CLI and loader, and is a devDependency for applications that deploy built output.
Keep the three Nola packages on the same version; the scaffold sets matching ranges. "type": "module" enables ESM, which both the loader and nola build use.
NOLA_API_KEY=nola_sk_...The scaffold writes .env when it creates a Nola key. For a vendor provider, copy .env.example to .env and fill in the named key, such as OPENAI_API_KEY. This also applies to templates that come with their own provider, such as triage-ticket.
nola run and node --import nola-lang/register load the .env next to nola.config.ts before evaluating the config. Existing process environment values take precedence. Built applications read the process environment and do not load .env automatically.
The generated .gitignore excludes .env, .env.*, node_modules/, dist/, *.tsbuildinfo and generated *.d.tsi.ts files. It makes an exception for .env.example, which can be committed with empty key placeholders. When adding a Nola key to an existing project, the setup flow adds the environment-file rules there too.
nola.replay.jsonl
Section titled “nola.replay.jsonl”The replay ledger contains one JSON line per recorded exchange, keyed by a fingerprint of the request. replay("./nola.replay.jsonl") reads answers from it.
If you change a prompt, context or type, the new request may have no matching entry and fail with NOLA3008. Switch to a live provider or record a new ledger after such changes. See Record and replay. Projects scaffolded with the Nola trial do not include a ledger.
Built output
Section titled “Built output”nola build mirrors the source tree in dist/. It writes dist/src/main.tsi.js or person.tsi.js, a source map and a declaration file for each module, and the bundled dist/nola.config.js. Each built module imports the config. For feature-extraction, run the result with node dist/src/main.tsi.js.
The build compiles .tsi files only. Plain .ts files, including typescript-interop’s src/main.ts, need a separate build step. Deploying explains how to combine the outputs.
Next: The mental model