MALDA™ Reference Manual

The AI-First Programming Language - Version 1.0.39

10. Prompts

A prompt is a named, parameterized template. Calling it interpolates parameters and returns a PromptInstance. await can call an LLM and validate JSON against a declared type. Decorators such as @within and @budget apply here as well as on functions — see 9.7 Decorators. Optional @version("…") is an informational pin for malda prompts --diff. Every await / Chat goes through cassettes when MALDA_RECORD / MALDA_REPLAY is set. .usage on a result is metadata (an own key named usage wins). Narrative: 24. Agentic Runs.

Templates without await are core language. await prompt, live LLM clients, and cassettes are host-only (interpreter and C# transpile). JavaScript can run validate / asVariant but not a live model call. See the backend capability matrix.

@version("2026-09-01")
prompt extract(raw) -> Card {
    user: raw;
}

10.1 Overview

Prompt blocks are reusable prompt templates. Start with a template that does not call a model, then await with a schema, then tools. Same file as the course lesson: Examples/Basics/prompt_template.malda. Further files: Examples/Prompts/basic_prompt.malda, Examples/Prompts/schema_prompt_structured.malda, Examples/Prompts/prompt_tools_mode.malda, Examples/Prompts/prompt_tools_then_structured.malda.

First-read stop. Step 1 (template, no API key) is the course lesson: Learn MALDA — Lesson 14. await, tools, and cassettes stay in this chapter.
StepWhat you writeCalls a model?
1. Templateprompt greet(name) { user: "Hello, {name}" } then greet("Alice")No. You get a PromptInstance (interpolated strings).
2. Structured awaitschema + prompt ... -> Schema + await extract(...)Yes. JSON is validated against the schema.
3. ToolsMode B: tools: [...] on one call. Mode C: gather: [...] then a typed extractYes. Do not combine tools: and gather:.

Step 1 — template, no API key

prompt greet(name) {
    user: "Hello, {name}! How are you today?"
}

var greeting = greet("Alice");
io.print(greeting.user);

Step 2 — await plus schema (needs an LLM)

schema ContactCard {
    name: string;
    email: string;
}

prompt extractContact(raw) -> ContactCard {
    system: "Extract a contact. Reply with JSON only matching the schema.";
    user: """
    Text:
    {raw}
    """
}

var card = await extractContact("Ada Lovelace — ada@example.com");
print("name=" + card.name);

Step 3 — Mode B vs Mode C

ModeFieldOn await
Btools: ["name"]One LLM turn with tools. Schema response_format is not sent.
Cgather: ["name"] and -> TypeTool round, then a fresh typed extract without tools.

Without await, constructing the prompt still does not call the model. Details of validation, repair, pipes, and PromptInstance follow in this chapter.

10.2 Prompt Declaration Syntax

Prompts support two body syntaxes:

Object Literal Syntax (Backward Compatible)

prompt name(params) -> ReturnType? {
    system: "Optional system prompt"
    user: "Required user prompt with {parameter} interpolation"
    model: "Optional model name"
    temperature: 0.7
    tools: ["tool1", "tool2"]
    gather: ["read_file", "grep"]
    maxTokens: 2000
}

Statement-Based Syntax (New)

prompt name(params) -> ReturnType? {
    system "Optional system prompt";
    user expression;
    model "Optional model name";
    temperature 0.7;
    tools ["tool1", "tool2"];
    gather ["read_file", "grep"];
    maxTokens 2000;
}

Key Features:

Few-Shot Examples

prompt classify(text) {
    system: "Classify sentiment as positive or negative."
    examples: [
        { input: "I love it!", output: "positive" },
        { input: "Terrible.", output: "negative" }
    ]
    user: "Classify: {text}"
}

var p = classify("Pretty good");
print(length(p.examples));  // 2

When the prompt is executed (via agent.think, await prompt(...), or runPrompt), each example is sent as a user message followed by an assistant message, then the interpolated user field.

Multimodal attachments

prompt look(name) {
    user: "Describe {name}"
    attachments: [
        { path: "shots/{name}.png" }
    ]
}

var p = look("cat");
print(p.attachments[0].kind);

The instance stores metadata only. On await, HTTP clients send OpenAI content parts; in-process GGUF throws. Same file: Examples/Prompts/multimodal_attachments.malda.

Dynamic Few-Shot at Runtime

Use withExamples() to attach or replace examples on a PromptInstance built at runtime. Works in pipe pipelines:

prompt classify(text) {
    user: "Classify: {text}"
}

var dynamic = [
    { input: "Great!", output: "positive" },
    { input: "Awful.", output: "negative" }
];

var p = classify("Pretty good") |> withExamples(dynamic);
// merge static + runtime examples from the prompt declaration:
var merged = withExamples(classify("x"), dynamic, { merge: true });

await (p |> runPrompt(client));

10.3 Basic Prompt Examples

Simple Prompt

prompt simple(task) {
    user: "Task: {task}"
}

var p = simple("Add logging");
print(p.user);  // Output: Task: Add logging

Prompt with System Message

prompt planTask(task, docs) -> Plan {
    system: "You are a senior MALDA engineer specializing in feature planning.",
    user: """
    Implement feature: {task}
    
    Relevant documentation:
    {docs}
    
    Provide a detailed implementation plan.
    """
}

var plan = planTask("Add error handling", readFile("docs.md"));

Prompt with Interpolation

prompt greet(name, age) {
    user: $"Hello {name}, you are {age} years old."
}

var greeting = greet("Alice", 25);

10.4 Using Prompts with Agents

Prompt blocks can be passed directly to agent.think():

var client = new OpenRouterClient("openai/gpt-4");
var agent = new CodingAgent("Dev", "developer", "");

prompt codeReview(code, language) {
    system: "You are an expert code reviewer.",
    user: """
    Review this {language} code:
    
    ```{language}
    {code}
    ```
    
    Provide:
    1. Bug findings
    2. Performance issues
    3. Style suggestions
    """
}

var review = agent.think(codeReview(sourceCode, "malda"));
print(review.content);

When a PromptInstance is passed to agent.think():

10.5 Direct Await Execution

Prompts can be executed directly using await, which calls the LLM and returns the response string:

// Optional: use OpenRouter; omit to use default local LLM
var client = new OpenRouterClient();
var agent = new Agent("Assistant", "helper", "You are helpful.", client);
setDefaultAgent(agent);

prompt summarize(text) {
    system "You are a summarizer. Respond with only the summary.";
    user "Summarize: " + text;
}

var summary = await summarize("Long document text here...");
print(summary);  // Prints the LLM response directly

How it works:

10.6 Schema Declarations for Structured Output

Syntax, field types, validate, and the parseJSON / parseJson distinction live in 4.6 Schema declarations. This section is the prompt-shaped use: typed returns and parseJson after a model (or fixture) payload.

Declare reusable JSON schemas with the schema keyword. Schemas are used by parseJson() and typed prompt returns (-> Type):

schema Answer {
    text: string;
    sources: string[];   // array types: elementType[]
    score: float?;       // optional field
}

schema Address {
    city: string;
}

schema Person {
    name: string;
    address: Address;    // nested schema (expanded for validate / typed prompts)
}

var parsed = parseJson('{"text":"hi","sources":["a.md"]}', "Answer");

Supported field types: JSON primitives (string, int, float, bool, object, array), array forms such as string[] or Address[], other declared schema names (nested objects are expanded inline), and declared sum types (intent: Intent / Intent[] — tagged JSON oneOf). Use ? after the type for optional fields. Unknown field type names error when the schema is resolved; cyclic schema references are rejected. See Examples/Basics/schema_nested_validate.malda and Examples/Basics/schema_sumtype_validate.malda.

For end-to-end RAG pipelines combining schemas, prompts, retrievers, and pipes, see Examples/Prompts/rag_pipeline.malda and 13. Built-in Functions (AI pipeline helpers).

10.7 Sum Types as Prompt Return Types

When the model must choose one of several shapes (intents), declare a sum type and use it as the prompt return type. On success, await returns a real variant value for match:

type Intent =
    Search(query: string)
  | Buy(sku: string, qty: int)
  | Help();

prompt parseUtterance(text) -> Intent {
    system: "Map the user to exactly one Intent variant. JSON only.";
    user: text;
}

var intent = await parseUtterance("buy 2 of SKU-9");
match intent {
    case Search(q): print("search: " + q);
    case Buy(sku, qty): print("buy " + sku);
    case Help(): print("help");
}

Wire JSON uses a tag field equal to the constructor name, plus payload fields named like the constructor parameters — for example {"tag":"Buy","sku":"SKU-9","qty":2} or {"tag":"Help"}. Optional payload types (Buy(sku: string, qty: int)) become JSON Schema types for validate, asVariant, typed prompts, and evalPrompt; name-only parameters stay permissive. The same name cannot be both a schema and a type (sum type). validate("Intent", dict) accepts that tagged shape and returns the original dict; it does not produce a variant. Use asVariant("Intent", dict) for match without await, or evalPrompt(instance, fixture) to fixture-test the same coerce path as await with no LLM. See Examples/Prompts/sum_type_intent_prompt.malda, Examples/Prompts/eval_prompt.malda, Examples/Basics/as_variant.malda, Examples/Basics/sumtype_typed_payloads.malda, Examples/Basics/schema_sumtype_validate.malda, and 4.5 Sum Types / 4.6 Schema.

10.8 Closed APIs and Deterministic Programs

Declare a closed api of method signatures. Implementations are ordinary top-level functions with the same names. Method parameters may optionally take a SchemaType (the same form as schema fields and sum-type payloads: add(a: number, b: number)); name-only stays permissive. Declared types feed the program JSON Schema and coerce LLM args ("2" becomes a number only when the hint is number/int). Prompt parameters stay name-only, and the implementing function body stays untyped. A typed prompt may return program(ApiName); on success you get a program value that runProgram executes with no further LLM calls (unlike @Tool agent loops or executePlan):

api Calc {
    function add(a: number, b: number);
    function mul(a: number, b: number);
}

function add(a, b) { return a + b; }
function mul(a, b) { return a * b; }

prompt solve(expr) -> program(Calc) {
    system: "Translate into Calc steps. JSON only.";
    user: expr;
}

var prog = await solve("(2 + 3) * 4");
var result = runProgram(prog);

Wire JSON shape. Offline (no LLM), pass the same object to runProgram directly — args are JSON literals or "$alias" references to a prior step's as. Models often emit nested {"call","args"}, TypeChat @func/@ref, numeric strings ("2"), or {"type":"number","value":2} wrappers; the host flattens and coerces those before calling the api methods. When methods are named _add/_mul, the host also maps unique aliases such as add/mul and +/*, and treats a bare t0 as "$t0". Leftover objects in args are rejected (they used to be passed through, so add received an object instead of a number). Prefer JSON numbers, not numeric strings:

api Calc {
    function add(a: number, b: number);
    function mul(a: number, b: number);
}

function add(a, b) { return a + b; }
function mul(a, b) { return a * b; }

var prog = parseJSON("""
{
  "@api": "Calc",
  "steps": [
    { "call": "add", "args": [2, 3], "as": "t0" },
    { "call": "mul", "args": ["$t0", 4], "as": "result" }
  ],
  "return": "$result"
}
""");

var result = runProgram(prog);
print(result);

See Examples/Prompts/api_program_calc.malda. A name cannot be both an api and a schema / sum type.

Fixture-test the same coerce path with evalPrompt(solve(expr), fixture) (no LLM). On await, Mode A sends response_format and in-process GGUF applies a GBNF grammar from that schema. Few-shot: docs/llm/few-shot/28_api_program_prompt.malda.

For a durable agent, produce the program JSON before startWorkflow, then execute it in a memoized step. A step right-hand side is a single call, so the shape is step result = runProgram(prog);. Do not await a prompt in the workflow body: replay would call the model again, and that is not WF1001/WF1002. See Examples/Workflows/runprogram_in_step.malda and few-shot docs/llm/few-shot/29_runprogram_in_step.malda.

10.9 Reusable Pipe Pipelines

Name and reuse multi-step AI pipelines with ordinary function declarations and the pipe operator (|>). Return the final pipe result explicitly.

function ragContext(question, retriever) {
    return question |> retriever.get |> formatRetrievedDocs;
}

function ragPrompt(question, context) {
    return context |> (ctx) => answerPrompt(question, ctx);
}

var context = ragContext("What is GraphMemory?", retriever);
var prompt = ragPrompt("What is GraphMemory?", context);

With an LLM client and typed output:

function ragAnswer(question, client) -> Answer {
    return question |> retriever.get |> formatRetrievedDocs
        |> (ctx) => answerPrompt(question, ctx)
        |> runPrompt(client)
        |> parseJson("Answer");
}

var answer = await ragAnswer("How does retrieval work?", client);

Local bindings for readable multi-stage bodies (retrieval, branching, reuse of intermediate values):

function buildContext(question, retriever, fallback) {
    var hits = question |> retriever.get;
    if (length(hits) == 0) {
        return fallback;
    }
    var text = formatRetrievedDocs(hits);
    return text;
}

Stream tokens during runPrompt in interpreted and transpiled mode: pass { onToken: (token) => ... } for content and { onReasoning: (token) => ... } for reasoning deltas (see 10.9.1).

See 7. Expressions (pipe operator) and Examples/Prompts/rag_function_pipeline.malda.

Advanced composition: composePipe, parallelRun, and mergeRetrievedDocs compose with functions and pipes for parallel retrieval and reusable pipeline functions. See Examples/Prompts/rag_compose_pipeline.malda and the catalog below.

10.9.1 AI pipeline helpers

These built-ins compose with the pipe operator (|>) for retrieval-augmented generation (RAG) and structured LLM output. Indexing into a VectorDB: 16. VectorDB (asRetriever, indexInto). Offline fixtures: 13.7.1.2 evalPrompt. Worked files: Examples/Prompts/rag_pipeline.malda, Examples/Prompts/rag_compose_pipeline.malda.

// Load text files matching a glob pattern (default directory: current)
var docs = loadDocuments("**/*.md", "docs/");

// Split long documents into overlapping chunks
var chunks = splitDocuments(docs, 512, 64);

// Index documents into a VectorDB (embeds content, preserves metadata)
indexInto(vdb, chunks);

// Format retrieved documents for prompt context
var context = query |> retriever.get |> formatRetrievedDocs;

// Execute a PromptInstance with the default or supplied LLM client
var response = await (promptInstance |> runPrompt(client));

// Stream tokens while the model responds
var seen = "";
function onTok(token) { seen = seen + token; }
var response = await runPrompt(promptInstance, client, { onToken: onTok });

// Stream model reasoning/thinking deltas (OpenRouter-compatible providers)
var reasoning = "";
function onReason(token) { reasoning = reasoning + token; }
var response = await runPrompt(promptInstance, client, { onReasoning: onReason });

// Attach runtime few-shot examples to a prompt instance
var p = classify("query") |> withExamples(dynamicExamples);

// Validate LLM JSON output against a schema
var parsed = response |> parseJson("Answer");

// Advanced composition: sequence + parallel branches
var pipeline = composePipe(formatRetrievedDocs, (ctx) => answerPrompt(q, ctx));
var context = question |> pipeline;

var branches = parallelRun(question, {
    context: (q) => q |> retriever.get,
    tags: classifyIntent
});
var merged = mergeRetrievedDocs(branches.context, branches.tags);
FunctionDescription
loadDocuments(pattern, dirPath?)Glob-matches files under dirPath (default "."); returns { content, metadata: { source } } per file
splitDocuments(docs, chunkSize, overlap?)Splits document content into overlapping chunks; copies metadata to each chunk
indexInto(vdb, docs)Embeds and adds each document to a VectorDB via its calculator; stores metadata with hits
formatRetrievedDocs(docs)Formats document arrays as [source: …] blocks joined for prompt context
composePipe(step1, step2, …)Composes 2+ callables (functions, lambdas, built-ins, prompts) left-to-right into a reusable function (input) => …. Awaits async steps. Pipe-friendly: input |> composePipe(f, g)
parallelRun(input, branches)RunnableParallel: feeds the same input to each named branch concurrently; returns an object/map of results. Use await when branches include async steps.
mergeRetrievedDocs(docArrays…)Flattens multiple Document[] arrays (e.g. from parallelRun) into one array; dedupes by source+chunk metadata or content
withExamples(prompt, examples, options?)Returns a copy of a PromptInstance with runtime few-shot examples. Pass { merge: true } to append after any static examples from the prompt declaration. Pipe-friendly.
evalPrompt(prompt, fixture, typeName?)Offline fixture in/out on a PromptInstance (no LLM). Returns { ok, data } or { ok: false, error }. data matches await (variant for sum types). Fixture: dict or LLM-shaped JSON string. instance.eval(fixture) is the same helper. See 13.7.1.2.
runPrompt(prompt, client?, options?)Async: runs a PromptInstance through an LLM; returns response text. Optional options: { onToken: (token) => ... } for streamed content tokens; { onReasoning: (token) => ... } for streamed reasoning/thinking tokens. Both work in interpreted and transpiled mode with await and pipe steps.
parseJson(json, schemaName)Parses and validates JSON against a schema declaration; returns a typed object

Name index: 13.7.2.

Typed Await Example (Validated Output)

prompt planTask(task) -> Plan {
    system "You are a planner. Return only JSON.";
    user "Task: " + task;
}

// With a return type, await validates the model output
var plan = await planTask("Add parser tests");
print(plan.steps[0].description);

Setting a Default Agent:

If you omit a client, the default is the local Qwen/Qwen2.5-0.5B-Instruct model (auto-downloaded as a GGUF build from Hugging Face). To use a remote model (e.g. OpenRouter), pass a client:

// Optional: use OpenRouter for remote model
var client = new OpenRouterClient();
var agent = new Agent("MyAgent", "assistant", "You are helpful.", client);
setDefaultAgent(agent);

// Now await prompt() calls will use this agent
var result = await myPrompt("input");

10.10 PromptInstance Properties

When a prompt is invoked, it returns a PromptInstance object with the following properties:

var p = myPrompt("arg1", "arg2");

// Access prompt fields
var system = p.system;      // string? (null if not provided)
var user = p.user;          // string (required)
var examples = p.examples;  // array? of { input, output } few-shot pairs
var model = p.model;        // string? (null if not provided)
var temperature = p.temperature;  // number? (null if not provided)
var tools = p.tools;        // array? (null if not provided; Mode B)
var gather = p.gather;      // array? (null if not provided; Mode C tool list)
var maxTokens = p.maxTokens; // number? (null if not provided)
var returnType = p.returnType; // string? declared -> Type (null if untyped)

// Methods
var userStr = p.getUser();     // Returns user prompt string
var systemStr = p.getSystem(); // Returns system prompt string or null
var promptStr = p.toPromptString(); // Returns user prompt (for backward compatibility)
var checked = p.eval(dict { "name": "Ada" }); // Offline fixture in/out; same as evalPrompt(p, fixture)

evalPrompt(instance, fixture, typeName?) (and instance.eval) feed simulated model JSON through the same extract / validate / coerce path as await, with no LLM and no repair loop. Success data for a sum type is a variant for match. See 13.7.1.2 evalPrompt and Examples/Prompts/eval_prompt.malda.

10.11 Advanced Prompt Features

Prompt with Metadata (Object Literal Syntax)

prompt advanced(task) {
    system: "You are an expert.",
    user: "Task: {task}",
    model: "openai/gpt-4",
    temperature: 0.7,
    tools: ["read_file", "write_file", "grep"],
    maxTokens: 2000
}

Prompt with Metadata (Statement-Based Syntax)

prompt advanced(task) {
    system "You are an expert.";
    user "Task: " + task;
    model "openai/gpt-4";
    temperature 0.7;
    tools ["read_file", "write_file", "grep"];
    maxTokens 2000;
}

Statement-Based Syntax with Expressions

The statement-based syntax allows user to be any expression, not just a string literal:

prompt combine(a, b) {
    user "First: " + a + ", Second: " + b;
}

var result = combine("A", "B");
print(result.user);  // Output: First: A, Second: B

Multi-line Prompts

prompt detailedPlan(goal, context) {
    system: "You are a strategic planner.",
    user: """
    Goal: {goal}
    
    Context:
    {context}
    
    Please provide:
    1. Step-by-step plan
    2. Risk assessment
    3. Timeline estimate
    """
}

10.12 Prompt Blocks vs String Concatenation

Before (String Concatenation):

var prompt = "Task: " + task + "\n\n" +
             "Documentation:\n" + docs;
var response = agent.think(prompt);

After (Prompt Blocks):

prompt planTask(task, docs) {
    user: """
    Task: {task}
    
    Documentation:
    {docs}
    """
}

var response = agent.think(planTask(task, docs));

Benefits:

See Also