The ask operator
ask resolves an askable to its value. It is a unary prefix operator with the same precedence as await. A method chain applies to the operand before it is resolved: ask (..`x`: string).withRetry(2) retries the extractor, then asks. With the angle-bracket spelling the parentheses can go, ask `x`<string>.withRetry(2) means the same; after a colon type they are needed, because a . there would continue the type name (see Extractors). Parentheses can make longer chains easier to read.
What ask accepts
Section titled “What ask accepts”An askable is one of three things: an extractor, a call intent, or the Intent returned by calling an infer function — all three are intents.
export infer function getUserById(.name: string) { return ask `the user record for the named person`: { name: string; id: string };}import { getUserById } from "./users.tsi";
type User = { name: string };
export infer function report(.text: string) { const user = ask `the user named in the text`: User; // extractor const record = ask getUserById(user.name); // another infer function return record;}Call intents are the third kind — see Call intents.
ask vs await
Section titled “ask vs await”| You are in… | Use | On |
|---|---|---|
| an infer function body | ask |
any askable |
| an infer function body | await |
ordinary promises (fetch, a database, any library) |
| plain TypeScript | await |
the intent an infer function returned — this opens an invocation |
Use ask to resolve a raw extractor or call intent. It needs the context of an invocation; awaiting it directly throws NOLA3010 at runtime:
// not-checked — WRONG: nothing supplies the inference contextimport { nameIntent } from "./person.tsi";const name = await nameIntent;export const nameIntent = ..`the user's full name`: string;
// RIGHT — resolve it with `ask` inside an infer functionexport infer function whoIsIt(.text: string) { return ask nameIntent;}// RIGHT — from plain TS, await the INFER FUNCTION's resultimport { whoIsIt } from "./person.tsi";
const name = await whoIsIt("Alice Smith, 32, works at Acme Corp.");console.log(name);ask with <name>
Section titled “ask with <name>”ask with <name> selects a named model from nola.config.ts for one ask:
export infer function summarize(.text: string) { const draft = ask with fast `a rough summary`: string; const final = ask with careful `a polished summary of: ${draft}`: string; return final;}import { anthropic, openai } from "@nola-lang/providers";import { defineConfig } from "@nola-lang/runtime";
export default defineConfig({ model: { default: openai({ model: "gpt-5-mini" }), fast: openai({ model: "gpt-5-nano" }), careful: anthropic({ model: "claude-sonnet-4-5" }), },});<name>must be a static identifier. A string literal, a number or a parenthesized expression afterwithisNOLA1009; a dotted name (provider.fast) is read as the identifierproviderplus a stray.fastand fails as a plain syntax error instead.- Names are resolved at ask time. A name that matches a key in the
modelmap selects that model. Withmodel: "nola", an unmatched name is sent to the platform as an inference profile, which the platform uses for routing. With any other default model, an unmatched name raisesNOLA3004and lists the configured names. - For a dynamic choice use the intent method instead:
ask (..`a rough summary`: string).withModel(useFast ? "fast" : "careful")— see Intent methods. ask withoutis a plain ask of an identifier namedwithout, not a pin.
To use the platform for both asks, configure a single model. The names fast and careful become inference profiles:
import { defineConfig } from "@nola-lang/runtime";
export default defineConfig({ model: "nola" }); // `ask with fast` / `ask with careful` become inference profilesRouting uses forceModel first, then the selection from ask with or .withModel, then model.default (or the single configured model). See Providers.
Where ask is legal
Section titled “Where ask is legal”ask is allowed directly inside an infer function or at the top level of a .tsi file, including in blocks, loops and try statements. It is not allowed inside a plain function or a nested closure, even one declared inside an infer function. Class field initializers and static blocks also reject it (NOLA2001).
ask is a reserved word in .tsi files. It is allowed as a member or property name, such as obj.ask, but using it as an identifier raises NOLA1003.
At module level
Section titled “At module level”A top-level ask runs in an implicit invocation named <module>. This lets you write a script without wrapping it in an infer function.
export infer function label(.mail: string) { return ask `a one-word label`: string;}
const mail = "my invoice is wrong";export const kind = ask `the kind of request in ${mail}`: string;export const tag = ask label(mail);The module body supports context statements and contextual bindings, just like an infer function. A template literal on its own line is a context statement: it applies to every ask below it and to every infer function declared below it, and it is read at each ask, so a value in it is seen as it is when the ask runs. Use const .x or let .x to add contextual bindings:
`Support inbox for ${brand}. Answer with one word.`const brand = "Acme";const .audience = "support agents";export const kind = ask `the kind of request`: string;A few details matter when using top-level asks:
- Each top-level
askhas its own root invocation, events, trace and timeout..withTimeout(ms)on its operand sets that invocation’s timeout and can overrideask.timeoutMs. ask label(mail)chainslabelunder the<module>invocation, exactly as an infer body chains its callees;await label(mail)on the next line would be a detached root instead.- A module context statement reaches the asks below it and the asks inside every infer function declared below it: they render the module’s
<context module="<file>">block directly before their own, whoever calls them, andask label(mail)from the module body renders it once, with what the call site saw and what the function’s position sees. Contextual bindings do not travel that way; they reach a function only throughask. - The module has no parameters: with no visible context statement and no contextual binding in scope, the prompt is the
<task>block alone. A visible context statement or binding gives the scope a<context module>block. See The prompt. - The lowered output contains a top-level
await, so the project must be an ES module ("type": "module"), which every scaffold is. - Importing the module runs its asks. For a library, keep asks inside infer functions so callers control when they run.
Next: Call intents