Docs/Guides/PreCalls deep-dive
Plans8 min read

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 callThe modelYou
WhenDuring reasoningBefore prompts run
Requires exact signatureNoYes
DeterministicNoYes

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.

FML
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):

FML
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:

FML
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:

FML
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:

FormSyntaxProducesUse when
Go text template"{{ .params.x }}"A stringThe 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-isThe 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:

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

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):
FML
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:

FML
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 }}"
}
  • -> name and -> vars.name are equivalent — both store the result in vars.name.
  • -> context.key writes into the context object; 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:

FML
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

MistakeFix
Using a PreCall for a tool whose signature you don't knowUse a prePrompt (+) and let the model call it
Putting return in a code(...) blockUse 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 toolDeclare require at root and use in the session

Next steps

← All guides

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