Docs/Guides/Templates and expressions
Plans6 min read

Templates and expressions

FML uses two expression systems, and mixing them up is the single most common source of bugs in a plan. The rule behind all of them is one sentence:

Go text templates always produce a string. Wrapped Expr $(...) preserves the real type.

This guide explains the three positions where expressions appear, why the string-vs-type distinction matters, and how to always pick the right one.

Prerequisites

Why there are two systems

A prompt is text, so interpolating a value into it should give you text — that's what Go templates do. But a tool argument might need a real array, object, or number, not a string that looks like one. Handing "[1, 2, 3]" (a string) to an argument that expects [1, 2, 3] (an array) is a type error waiting to happen. $(...) exists to pass the native typed value through untouched.

The three positions

PositionSyntaxTypeWhere
1. Text{{ ... }}Always stringprePrompt/prompt text, context strings, call string args, set values, parameter defaults
2. Typed tool args$( ... )Any (native)Only inside call argument values
3. Session conditions"..." (bare Expr)Bool / valueexpect="...", iterate="..."

1. Go text templates — {{ ... }}

Used anywhere the destination is text:

FML
+ Research {{ .params.topic }} in the {{ .vars.region }} region.
- Summarize the {{ .params.max_results }} findings above.

Common patterns:

PatternMeaning
{{ .params.x }}Parameter, as a string
{{ .vars.x }}Variable, as a string
{{ .context.session }}Full session output (Go struct notation)
{{ .context.session | json }}Session output serialized to a JSON string
{{ .it }}Current iterator element, as a string

The | json filter is how you drop a whole structured object into prompt text — it serializes to a JSON string, which is exactly what you want inside text.

2. Wrapped Expr — $( ... )

Used exclusively inside call argument values, when the argument must receive a non-string value from scope:

FML
call("process_items") {
    label = "Items for {{ .params.topic }}"   # string arg → template is correct
    items = $(context.gather.sources)          # array arg → Expr preserves the type
    limit = $(params.max_results)              # int arg   → Expr preserves the type
}
PatternReturnsUse case
$(context.session.field)AnyPass a nested object/array from a session
$(params.count)intPass a number as a number
$(vars.myList)arrayPass a variable holding an array
$(len(context.items))intPass a computed integer

3. Bare Expr in session arguments

expect and iterate take a plain quoted Expr string — no $(...) wrapper:

FML
session("elaborate",
        after="gather",
        expect="len(context.gather.keyPoints) > 0",
        iterate="context.gather.keyPoints") {
    ...
}
PatternMeaning
len(context.first) > 0Array length check
context.overview.keyPoints != nullNil check
params.max_resultsParameter value

The decision in one question

When you're about to interpolate a value, ask: is the destination text, or a tool argument?

  • Text (a prompt, a label, a context string) → {{ ... }}. If it's a structure, add | json.
  • A call argument that needs a real array/object/number$( ... ).
  • An expect/iterate condition → bare Expr in quotes, no wrapper.

The classic bug

You want to pass a session's array of sources to a tool:

FML
# WRONG — the tool receives the string "[{...},{...}]", not an array
call("rank") {
    items = "{{ .context.gather.sources }}"
}

# RIGHT — the tool receives the actual array
call("rank") {
    items = $(context.gather.sources)
}

And the inverse — trying to use $(...) in prompt text, where it doesn't belong:

FML
# WRONG — $(...) is only for call arguments
- Summarize these: $(context.gather.sources)

# RIGHT — serialize the structure into the text with | json
- Summarize these:
  {{ .context.gather.sources | json }}

Common mistakes

MistakeFix
"{{ .context.items }}" for an array/object tool argUse $(context.items) — templates only make strings
$( ... ) inside prompt or prePrompt textUse {{ ... | json }} in text; $() is for call args only
Wrapping expect/iterate in $(...)They take bare Expr strings: expect="len(context.x) > 0"
Passing a number as "{{ .params.n }}" to a numeric argUse $(params.n) to keep it an int

Next steps

← All guides

Go templates always make strings; $(...) preserves real types. Learn the three expression positions and never hand a tool a stringified array again.