MALDA™ Reference Manual

The AI-First Programming Language - Version 1.0.11

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.

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

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

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

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 and typed prompts; 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. See Examples/Prompts/sum_type_intent_prompt.malda, Examples/Basics/sumtype_typed_payloads.malda, Examples/Basics/schema_sumtype_validate.malda, and Data Types for sum types.

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.

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 13. Built-in Functions).

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 13. Built-in Functions.

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)

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

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