Skip to content

Intents

An intent describes work to run later. Calling an infer function returns an Intent<T>; resolving it produces a T. You can store an intent, pass it to another function or configure it before it runs.

async/await Nola
async function f(): Promise<T> infer function f(): Intent<T>
a Promise<T> — a value standing for a T that is not here yet an Intent<T> — a value standing for a T the model has not produced yet
await p resolves the promise to its T ask i resolves the intent to its T
the body returns T; the caller sees Promise<T> the body returns T; the caller sees Intent<T>
a promise is thenable an Intent<T> is thenable — plain TypeScript awaits it
summarize.tsi
export infer function summarize(.text: string) {
return ask `a one-sentence summary`: string;
}
main.ts
import { summarize } from "./summarize.tsi";
const intent = summarize("The quarterly report shows revenue up 12% on lower costs."); // Intent<string> — nothing has run
const summary = await intent; // now it runs
console.log(summary);

Calling an async function starts its body immediately. Calling an infer function leaves the body unstarted until you resolve its intent. There are two steps:

  • Construction captures the request: an infer function call captures its arguments, while an extractor evaluates ordinary ${…} substitutions and records its target type. This does not contact a provider.
  • Resolution runs the work through ask, or through await for an infer function’s intent. The runtime composes the prompt using the context available then, calls the provider and validates the answer.

You can prepare several intents and resolve only the ones you need:

triage.tsi
export infer function triage(.ticket: string) {
const severity = ..`the severity: low, medium or high`: "low" | "medium" | "high"; // constructed, not run
const owner = ..`the team that should own this ticket`: string; // constructed, not run
const s = ask severity; // one provider call
if (s === "low") return { severity: s, owner: "queue" };
return { severity: s, owner: ask owner }; // a second call, only on this branch
}

The low branch returns without making a provider request for owner.

There are three ways to create an intent:

Expression Produces Tier
calling an infer function — summarize(text) the function’s result Intent<T>
an extractor — ..`instruction`: T (the dots are optional directly after ask and in a call’s argument list) a request to pull a T from context Askable<T>
a call intent — fn`hint`(…) or a plain call with an extractor argument a request to fill the arguments, then call fn Askable<T>
Tier What it can do
Askable<T> resolvable only through ask; carries .withRetry(n), .withModel(…), .withParams({…}), .withTimeout(ms)
Intent<T> extends Askable<T>, PromiseLike<T> everything above, plus .detached() and then — so plain TypeScript can await it

An infer function’s intent includes enough information to start an invocation: its function, instruction, source file and contextual parameters. An extractor or call intent needs an existing invocation and uses the context of the ask that resolves it. Only Intent<T> supports await. See Intent methods for configuration and Intent API for signatures.

Where you are Use Accepts
an infer function body ask any intent — it runs on the asking invocation
plain TypeScript await an Intent<T> — it opens a fresh invocation

A raw extractor or call intent cannot be bare-awaited: with no invocation to borrow, the runtime throws NOLA3010. From plain TypeScript, await the infer function’s result instead — see ask vs await.

Intent<T> works with APIs that accept thenables. For example, Promise.all resolves several infer-function intents concurrently:

batch.ts
import { summarize } from "./summarize.tsi";
const [a, b] = await Promise.all([
summarize("Revenue is up 12% on lower costs."),
summarize("Churn fell to 2% after the pricing change."),
]);
console.log(a, b);

A failed inference rejects at the resolution site, exactly as a rejected promise does at await: wrap the ask or the await in try/catch. See Error handling.

These properties determine when an intent runs and how it can be reused.

Nothing runs until something resolves the intent. An intent that is never asked never reaches a provider — the triage example above depends on that.

An intent resolves at most once and retains its result. Resolving it again does not repeat the work. This is useful when two callers need the same answer, but a shared intent can accidentally reuse an answer from an earlier invocation:

shared.tsi
const nameIntent = ..`the person's full name`: string; // one intent for the life of the module
export infer function whoIsIt(.text: string) {
return ask nameIntent; // the first invocation runs it; every later invocation gets that FIRST answer
}

Build intents where you use them, or behind a function that returns a fresh one:

fresh.tsi
const nameIntent = () => ..`the person's full name`: string;
export infer function whoIsIt(.text: string) {
return ask nameIntent(); // a new intent per invocation
}

Every .with* method returns a new, unstarted intent with the option merged in; the original is untouched. Chain what you need, then resolve the result:

tuned.tsi
export infer function tuned(.text: string) {
const base = ..`the title of the text`: string;
const careful = base.withModel("careful").withRetry(2); // two clones; base is still unstarted
return ask careful;
}

Intent<T> implements PromiseLike<T>. Its then method makes it work with await, Promise.all, Promise.race and async return values. It has no catch or finally methods. Use try/catch around await, or wrap it in Promise.resolve(intent) when you need those methods.

For annotations and runtime checks:

  • Annotate an infer function with Intent<T>, never Promise<T> — nola check reports TS2739 otherwise (return type).
  • isIntent(value) from @nola-lang/runtime is the runtime guard for “is this any kind of intent” (Intent API).
A promise… An intent…
runs its executor immediately when created with new Promise leaves inference unstarted until ask or await
has no built-in model or inference settings carries model, retry, parameter and timeout settings
resolves the same wherever it is awaited resolves in the invocation that asks it: the ask site lends its context, history and provider pin to the intent
can be awaited anywhere as an Askable<T>, can be resolved only by ask — inside an infer function or at the top of a module

Context is supplied when an intent resolves. An extractor created in one function can therefore use another function’s context:

values.tsi
import type { Askable } from "@nola-lang/runtime";
export infer function answer(.document: string, question: Askable<string>) {
return ask question; // resolves against answer's context — the document
}
export infer function headline(.text: string) {
const question = ..`a headline for the document`: string; // built here, no document in sight
return ask answer(text, question); // resolved there, with the document
}

Intents are ordinary values: store them, pass them, return them from helpers. Only ask is position-restricted — an infer function body or the module body (NOLA2001); construction is legal anywhere.

Next: infer functions