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
- The Anatomy of a session and Data flow: params, vars & context guides.
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 plan 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 plan can read it — fetched exactly once, deterministically, before any reasoning starts.
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):
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:
The two kinds of PreCall
Tool PreCall
The first positional argument is the tool function name; the body is its named arguments:
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:
Two things to know about code(...):
- It uses completion-value notation — no
return. The value of the last expression is the result. Wrap a single expression in( ). - For multiple statements, end with a semicolon (or wrap the whole thing in parentheses):
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:
-> nameand-> vars.nameare equivalent — both store the result invars.name.-> context.keywrites into thecontextobject; treat it as experimental and use sparingly.
Routing to a var is what makes a fetch reusable across the plan — 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:
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
- Master the value-passing rules in Templates and expressions.
- See PreCalls alongside MCP servers and databases in Call MCP servers and databases.
- Full reference: PreCalls on the Frags wiki.
Run tools deterministically before prompts — global vs session PreCalls, tool vs code invocations, and routing output into vars or context.