PreCalls deep-dive | Diaphora Docs | Diaphora

PreCalls deep-dive

Most of the time you let the model decide when to call a tool, you write a prePrompt and it figures out the rest. But sometimes you know exactly what needs to happen: fetch these records, run this transform, before any reasoning starts. That's a PreCall — a deterministic, explicit tool or code invocation that runs before prompts.

This guide covers all of it: where PreCalls run (global vs session), the two kinds (tool vs code), and where their output goes (routing to vars or context).

Prerequisites

PreCall vs. letting the model call the tool

Both run tools, but they're different instruments:

prePrompt (+) PreCall (call)
Who decides to call The model You
When During reasoning Before prompts run
Requires exact signature No Yes
Deterministic No Yes

Why reach for a deterministic call? When you drive the call yourself, the same inputs always produce the same call — same function, same arguments, every run. The model never has to decide whether to fetch, never forgets a parameter, and never rephrases the query. That makes the fetch cheaper (no reasoning tokens spent deciding to call it), faster, and testable, and it guarantees the data is in place before any prompt runs. Spend the model's judgment on the reasoning, not the plumbing.

Rule of thumb: use a PreCall only when you know the exact function name and arguments. If you're unsure, write a prePrompt and let the model call the tool.

Where PreCalls run

Global PreCall — before the blueprint starts

Placed at the root of the file, a call runs once, before any session. This is the highest-leverage PreCall: route its result into a var, and because vars is a global namespace, every session in the blueprint can read it — fetched exactly once, deterministically, before any reasoning starts.

require mcp slack

# Fetch once, at plan start, into a global var
call("search_messages") -> recentMessages {
    author = "{{ .params.name }}"
}

vars.recentMessages is now available to every session — and no session needs after, because the data was in place before any of them ran. Two independent sessions can both consume it (and run concurrently, since neither depends on the other):

session("themes") {
    - Summarize the recurring themes across these messages:
      {{ json .vars.recentMessages }}
    schema { themes: string[] }
}

session("action_items") {
    - Extract any action items mentioned in these messages:
      {{ json .vars.recentMessages }}
    schema { items: string[] }
}

That's the payoff of a deterministic global fetch: one guaranteed call feeds many sessions, instead of each session independently — and non-deterministically — asking the model to go fetch the same data. Like any tool use, the PreCall's tool must be declared with require at the root.

Session PreCall — before a session's prompts

Placed inside a session body, a call runs before that session's prompts fire. By default its result is injected into that session's LLM context:

session("triage") {
    use mcp support_desk

call("sf_search_leads") {
        agent_name = "diego"
    }

- Group the leads returned above by priority.
    schema { groups: string[] }
}

The two kinds of PreCall

Tool PreCall

The first positional argument is the tool function name; the body is its named arguments:

call("list_repositories") {
    owner = "{{ .params.org }}"
    limit = $(params.max_results)
}

Argument values take one of three forms — pick based on what the argument must carry:

Form Syntax Produces Use when
Go text template "{{ .params.x }}" A string The arg wants text — a query, a name
Wrapped Expr $( expr ) The native type (array, object, int, bool) The arg wants a structured or numeric value from scope
Structured literal [{name: "foo"}] The literal, as-is The arg is a hardcoded object/array

The template-vs-$() choice matters: a template always yields a string, so passing "{{ .vars.items }}" to an argument that expects an array hands the tool a string representation, not the array. Use $(vars.items) for that. This distinction has its own guide — Templates and expressions.

Code PreCall

Instead of a tool, a call can run pure JavaScript with a code(...) block, handy for reshaping arguments:

call("do_something") {
    code(
        (args.items.map(it => it.name))
    )
    items = [
        { name: "foo" },
        { name: "bar" }
    ]
}

Two things to know about code(...):

code(
    (
        const names = args.items.map(i => i.name);
        names.join(', ');
    )
)

Inside code, args refers to the other arguments defined in the same call block.

Routing the output

By default a PreCall's result lands in the containing session's LLM context. The -> operator sends it somewhere you can address later instead:

call("list_repositories") -> repos {          # implicit vars target
    owner = "{{ .params.org }}"
}

call("list_repositories") -> vars.repos {      # explicit — identical to above
    owner = "{{ .params.org }}"
}

call("list_repositories") -> context.repos {   # writes to context (experimental)
    owner = "{{ .params.org }}"
}

Routing to a var is what makes a fetch reusable across the blueprint — inject it into a prompt with {{ json .vars.repos }}, pass it to another tool with $(vars.repos), or gate a session on it with expect="vars.repos != null".

Putting it together

Fetch deterministically into a var, then reason over it — the fetch is explicit, the reasoning is the model's job:

require mcp github

session("summarize_repos") {
    use mcp github

# Deterministic fetch → vars.repos, reshaped with code
    call("list_repositories") -> repos {
        owner = "{{ .params.org }}"
        code(
            (args.owner.toLowerCase())
        )
    }

context "Repositories:\n{{ json .vars.repos }}"

- Group the repositories by primary language and note the most active ones.
    schema {
        byLanguage: string[]  # one line per language with a count
        mostActive: string[]  # names of the most active repos
    }
}

Common mistakes

Mistake Fix
Using a PreCall for a tool whose signature you don't know Use a prePrompt (+) and let the model call it
Putting return in a code(...) block Use completion-value notation: wrap in () or end with ;
Passing an array via "{{ .context.items }}" Use $(context.items) to preserve the type
Forgetting require / use for the PreCall's tool Declare require at root and use in the session

Next steps

Run tools deterministically before prompts — global vs session PreCalls, tool vs code invocations, and routing output into vars or context.