Skip to content

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.

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.

users.tsi
export infer function getUserById(.name: string) {
return ask `the user record for the named person`: { name: string; id: string };
}
report.tsi
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.

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 context
import { nameIntent } from "./person.tsi";
const name = await nameIntent;
person.tsi
export const nameIntent = ..`the user's full name`: string;
// RIGHT — resolve it with `ask` inside an infer function
export infer function whoIsIt(.text: string) {
return ask nameIntent;
}
main.ts
// RIGHT — from plain TS, await the INFER FUNCTION's result
import { whoIsIt } from "./person.tsi";
const name = await whoIsIt("Alice Smith, 32, works at Acme Corp.");
console.log(name);

ask with <name> selects a named model from nola.config.ts for one ask:

summarize.tsi
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;
}
nola.config.ts
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 after with is NOLA1009; a dotted name (provider.fast) is read as the identifier provider plus a stray .fast and fails as a plain syntax error instead.
  • Names are resolved at ask time. A name that matches a key in the model map selects that model. With model: "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 raises NOLA3004 and 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 without is a plain ask of an identifier named without, not a pin.

To use the platform for both asks, configure a single model. The names fast and careful become inference profiles:

nola.config.ts
import { defineConfig } from "@nola-lang/runtime";
export default defineConfig({ model: "nola" }); // `ask with fast` / `ask with careful` become inference profiles

Routing uses forceModel first, then the selection from ask with or .withModel, then model.default (or the single configured model). See Providers.

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.

A top-level ask runs in an implicit invocation named <module>. This lets you write a script without wrapping it in an infer function.

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

script.tsi
`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 ask has its own root invocation, events, trace and timeout. .withTimeout(ms) on its operand sets that invocation’s timeout and can override ask.timeoutMs.
  • ask label(mail) chains label under 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, and ask 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 through ask.
  • 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