Skip to content

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).

parse.tsi
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 type can also be written as a type argument after the closing backtick:

angle.tsi
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` : fallback an ordinary conditional over an untyped ask, while cond ? ask `p`: number : fallback asks for a number. A glued colon with nothing after it on its line raises NOLA1018: 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) reads User.withRetry as a qualified type name. To chain intent methods, parenthesize the extractor (ask (..`p`: User).withRetry(2)), use a delimited type such as string[], Array<User>, { … } or (User), or write <User>.
  • < after a type name starts type arguments, with either spelling: ask `p`: User < 5 is not a comparison. Parenthesize the ask to compare.

${expr} inserts a JavaScript value into the instruction when you construct the intent. Strings are inserted as written; other values are serialized as JSON.

lookup.tsi
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).

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:

spellings.tsi
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:

intents.tsi
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.

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.

types.tsi
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):

models.ts
export interface Person {
name: string;
age: number;
}
report.tsi
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:

tally.tsi
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]));
}
  • Describe the answer you need. For example, the person described in the text tells 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