Extractors
An extractor describes a model request using instruction text in backticks and an optional type. Write ask `instruction`: T to request a T; the type can also be written as a type argument, ask `instruction`<T> (the angle-bracket spelling). To store the request, add two leading dots: ..`instruction`: T (details below).
Typed and untyped
Section titled “Typed and untyped”export infer function parse(.doc: string) { const id = ask `the ticket id`: string; // typed const count = ask `how many line items`: number; const free = ask `think step by step about the document`; // untyped return { id, count, free };}With no type the extractor asks for free text: the wire schema is a plain string and the static TypeScript type is any. Give every extractor an explicit type unless you deliberately want unconstrained prose.
The angle-bracket spelling
Section titled “The angle-bracket spelling”The type can also be written as a type argument after the closing backtick:
export infer function parse(.doc: string) { const id = ask `the ticket id`<string>; // same as `: string` const count = ask `how many line items`<number>; return { id, count };}The two spellings are one construct: they compile to the same code and share one definition, so a recorded ledger serves either. Everything that accepts : T accepts <T>: the .. form (..`x`<T>), call-intent slots (fn(`x`<T>)), ask with, and decision types (ask `Which team?`<Choice<{ billing: "Payments"; sales: null }>>). This documentation writes : T throughout; use <T> when you prefer it, or where the colon’s rules get in the way.
Three rules come with the colon:
- The colon touches the backtick, and the type starts on its line. A colon after a space is never the extractor’s, which is what keeps
cond ? ask `p` : fallbackan ordinary conditional over an untyped ask, whilecond ? ask `p`: number : fallbackasks for a number. A glued colon with nothing after it on its line raisesNOLA1018: a type would otherwise continue onto the next line and swallow the next statement. - A type name followed by
.continues the type.ask `p`: User.withRetry(2)readsUser.withRetryas a qualified type name. To chain intent methods, parenthesize the extractor (ask (..`p`: User).withRetry(2)), use a delimited type such asstring[],Array<User>,{ … }or(User), or write<User>. <after a type name starts type arguments, with either spelling:ask `p`: User < 5is not a comparison. Parenthesize the ask to compare.
Interpolation
Section titled “Interpolation”${expr} inserts a JavaScript value into the instruction when you construct the intent. Strings are inserted as written; other values are serialized as JSON.
interface Person { name: string; age: number;}
export infer function lookup(text: string) { return ask `the person described in: ${text}`: Person;}A substitution is ordinary TypeScript, read when the ask runs: a string is spliced verbatim, anything else is written as JSON. It cannot contain another Nola construct (NOLA2010).
Two spellings, one construct
Section titled “Two spellings, one construct”You can omit .. in two places: directly after ask or ask with <name>, and in a call’s argument list, where a typed template makes the call a call intent. Everywhere else, include the dots to distinguish an extractor from an ordinary template string:
declare function createTicket(title: string, priority: number): Promise<string>;
export const nameIntent = ..`the user's full name`: string; // stored: dots required
export infer function whoIsIt(.text: string) { const name = ask `the user's full name`: string; // implied const same = ask ..`the user's full name`: string; // still legal const id = ask createTicket(`a short title`: string, 2); // call-intent slot: implied too const also = ask createTicket(..`a short title`: string, 2); // still legal return { name, same, id, also };}After ask, the template literal must be the first token of the operand. In a call, it must be the first token of an argument, or of a property value or element in an object or array literal written there (api.save({ note: `a one-line note`: string })). Parenthesized templates (ask (`x`)) and tagged templates (ask tag`x`) stay ordinary expressions, f((`x`: T)) is a syntax error, and an untyped template in a call passes a string: f(`x`) is an ordinary call.
A space is required between ask and the backtick. Writing ask`x` raises NOLA1017. A typed template anywhere else needs the two dots: const i = `x`<string> raises NOLA2014, and const i = `x`: string is an ordinary syntax error. With either extractor spelling, < after the closing backtick starts type arguments; use parentheses if you mean to compare values. In a call’s argument list a bare template is an extractor only when its <…> reads as type arguments, so f(`a` < b) stays a comparison.
Construction anywhere, resolution only with ask
Section titled “Construction anywhere, resolution only with ask”You can construct an extractor anywhere in a .tsi file, including at module level. It needs context only when you resolve it:
export const nameIntent = ..`the user's full name`: string; // legal, inert
export infer function whoIsIt(.text: string) { return ask nameIntent; // resolved here}Resolve extractors with ask inside an infer function or at module level. Other locations raise NOLA2001; see Where ask is legal. Awaiting a raw extractor directly raises NOLA3010 because it has no invocation context.
Derivable types
Section titled “Derivable types”The compiler uses the TypeScript checker to derive a JSON Schema from the extractor’s type. It supports the following types:
| TypeScript | Wire schema · notes |
|---|---|
string, number, boolean |
JSON scalars |
Date |
a string with format: "date-time", revived to a real Date on the way back |
T[] |
arrays of any derivable T |
{ a: string; b?: number } |
inline object literals; optional members are not required |
named interface / type aliases |
same file, another file (import type { Person } from "./models.js") or a package |
interface X extends Y, A & B, Partial<T>, Pick, Omit, Required, Box<number> |
the flattened, instantiated members |
recursive types (TreeNode → children?: TreeNode[]) |
via $defs / $ref, validated recursively |
"a" | "b" string-literal unions, string enums |
a JSON Schema enum — closed label sets |
Refund | Chargeback, string | null, 1 | 2 |
anyOf; a shared literal key (kind) makes the union discriminated |
Record<string, T>, [string, number] |
additionalProperties / prefixItems |
| JSDoc comments on members | become schema descriptions the model reads |
The full table, including what stays underivable, is on Schema derivation.
export interface Conclusion { answer: string; /** the collected notes that directly support the answer */ evidence: string[];}
export type CalendarEvent = { title: string; at: Date };
export type TreeNode = { label: string; children?: TreeNode[];};
export infer function conclude(.question: string, .notes: string[]) { return ask `answer the research question using only the collected notes`: Conclusion;}
export infer function nextEvent(.calendar: string) { const event = ask `the next event on the calendar`: CalendarEvent; const when: Date = event.at; // a Date, not a string return when;}
export infer function parseTree(.input: string) { return ask `the tree structure described in the input`: TreeNode;}Types from another file work through a type-only import with the NodeNext .js specifier; the toolchain carries the schema across for you (see TypeScript interop):
export interface Person { name: string; age: number;}import type { Person } from "./models.js";
export infer function extractPerson(.text: string) { return ask `the person described in the text`: Person;}Unsupported types include Map, Set, RegExp, functions, class instances, uninstantiated generics and other ambient library types (NOLA2002). Ask for a supported shape, then convert the result in TypeScript:
export infer function tally(.doc: string) { const counts = ask `counts per label`: { label: string; count: number }[]; return new Map(counts.map((c) => [c.label, c.count]));}Writing the instruction
Section titled “Writing the instruction”- Describe the answer you need. For example,
the person described in the texttells the model what to look for. - Use types for structure. Declare optional fields, label sets and dates in the type, and use JSDoc to describe fields or add constraints.
- Name types you reuse. An exported type gives other functions and TypeScript callers the same contract, including its JSDoc descriptions.
Next: The ask operator