-
Notifications
You must be signed in to change notification settings - Fork 0
Architectural Patterns
Architectural patterns in FML help you design high-performance, cost-effective, and resilient agent pipelines. By leveraging FML's separation between deterministic PreCalls, phased prompts, and typed scopes, you can solve common agent design challenges.
One tool needs the exact output of another tool (e.g., retrieving a list of IDs from an API and immediately writing them to a database) without incurring LLM latency, cost, or hallucination risk.
Route the first tool call's output into a variable using -> vars:name or the implicit -> name. Pass the resulting variable directly into the second tool call using Antonmedv expr syntax $(vars.name).
graph LR
T1["call('search_users')"] -->|-> vars:user_list| V[(vars.user_list)]
V -->|$(vars.user_list)| T2["call('db_bulk_insert')"]
# Retrieve user profiles based on a query parameter
call("search_users") -> vars:user_list {
query = "{{ .params.username }}"
}
# Insert the retrieved profiles directly into the database
call("db_bulk_insert") {
table = "user_records"
records = $(vars.user_list)
}
- Zero Token Consumption: The LLM is never invoked; data transfers directly between tools in memory.
- 100% Fidelity: Eliminates schema mutations, dropped fields, or hallucinated values.
External tools frequently return massive payloads (e.g., 500 audit logs, large SQL query dumps, raw web pages). Ingesting these raw payloads directly into an LLM context window exhausts token budgets, causes context truncation, and degrades reasoning performance.
- Route the raw API call to an intermediate variable
-> vars:raw_logs. - Run a second
callwith an embedded JavaScriptcode(...)block to filter, sanitize, and compact the data. - Route the sanitized result to
vars:critical_events. - Inject only the sanitized data into the session context.
graph TD
API["API: fetch_audit_logs (500 items, 2MB)"] -->|-> vars:raw_logs| V1[(vars.raw_logs)]
V1 --> JS["call('clean_logs') with code(...)"]
JS -->|-> vars:critical_events| V2[(vars.critical_events<br/>Only 12 errors, 8KB)]
V2 --> LLM["LLM Session: analyze_errors<br/>(High precision, low tokens)"]
call("fetch_audit_logs") -> vars:raw_logs {
limit = 500
}
# Clean the logs in sandboxed JavaScript, extracting only errors and timestamps
call("clean_logs") -> vars:critical_events {
logs = $(vars.raw_logs)
code(
(
args.logs
.filter(log => log.level === "ERROR" || log.level === "FATAL")
.map(log => ({ time: log.timestamp, msg: log.message }))
)
)
}
session("analyze_errors") {
# The LLM only receives the cleaned critical_events array
context "Errors: {{ .vars.critical_events | json }}"
- Analyze the error patterns and suggest mitigation steps.
schema {
incident_type: string
root_causes: string[]
remediation: string
}
}
Certain stages of a workflow are completely deterministic (e.g., reading a cached analytics calculation or generating an MD5 hash). Passing these through an LLM introduces unnecessary costs, latency, and the risk of hallucinated modifications.
Use -> context:target_key within a PreCall block inside the session. The session writes the raw tool or script output directly into the plan's context under that key.
graph LR
Call["PreCall: read_analytics_cache"] -->|-> context:analytics| CTX[("context.analytics<br/>(Directly Populated)")]
CTX --> LLM["LLM: Validates / Notes"]
session("get_cached_analytics", target="analytics") {
use collection database
# Reads directly from DB and writes straight to the analytics session context
call("read_analytics_cache") -> context:analytics {
cache_id = "{{ .params.cache_id }}"
}
# Minimal prompt needed since the PreCall already populated context:analytics
- Review the analytics cache and verify that the format is valid.
schema {
status: string
data: {
metric_name: string
value: float
}[]
}
}
Note
When generating plans via an automated agent or assistant, routing tool calls directly to -> context:var should be used with discretion. Routing to -> vars:var or omitting target routing is often cleaner and more modular.
When performing complex analytical tasks (such as auditing code, verifying financial balances, or diagnosing system failures), forcing the LLM to output rigid JSON immediately often degrades its reasoning capability because it cannot "think" or write notes before committing to JSON tokens.
-
Pre-prompt (
+): Instruct the model to perform free-form reasoning, review evidence, and draft step-by-step audit notes. -
Prompt (
-): Instruct the model to review its notes from the conversational history and map them directly into the output schema.
sequenceDiagram
participant User as Runtime
participant LLM as Gemini 3 Pro
User->>LLM: Phase 2: Pre-prompt (+)<br/>"Draft your step-by-step notes and findings..."
LLM-->>User: Free-form analysis, intermediate calculations & notes
User->>LLM: Phase 3: Prompt (-)<br/>"Now map your audit findings cleanly into the JSON schema."
LLM-->>User: Validated, perfectly structured JSON conforming to schema
session("analyze_security_log", target="audit_report") {
context "Raw Logs: {{ .context.fetch_logs | json }}"
# Step 1: Pre-prompt for free-text reasoning and draft notes
+ Read through the gathered logs in the context.
Identify any security events or anomalies.
Write down your step-by-step audit notes, identifying the root cause
and the security risk level of each anomaly.
# Step 2: Prompt for strict schema mapping
- Map your audit findings and the step-by-step notes into the schema format.
schema {
root_cause: string # Summary of what caused the breach
findings: {
anomaly: string
severity: low|medium|high
}[]
recommendations: string[]
}
}
You need to process an unknown number of items (e.g. web pages, repositories, database records) independently, then aggregate their results into a single synthesized report.
- Gather Session (Fan-Out Preparation): Discovers all target items and outputs an array.
-
Worker Session (
iterate- Map): Loops over the array. Each item runs in isolation with its own tools and produces a structured summary. The outputs are collected into a validated array. - Synthesis Session (Fan-In - Reduce): Ingests the collected array and generates the consolidated conclusion.
graph TD
S1["Session 1: Discover URLs<br/>(schema string[])"] --> S2["Session 2: Worker (iterate)<br/>(Scrapes each URL independently)"]
S2 --> S3["Session 3: Synthesize<br/>(Consolidates all summaries)"]
# Step 1: Discover items
session("find_targets", target="targets") {
use search
+ Find the top 3 open-source vector database projects.
- Output the project names.
schema string[]
}
# Step 2: Fan-out worker using iterate
session("analyze_target", after="find_targets", iterate=context.targets) {
use search
+ Research key features and performance metrics for: {{ .it }}
- Summarize project features.
schema {
project_name: string
primary_language: string
key_differentiator: string
}
}
# Step 3: Fan-in aggregation
session("compare_and_rank", after="analyze_target") {
context "All Analyses: {{ .context.analyze_target | json }}"
- Produce a comparison matrix and rank the projects.
schema {
rankings: {
rank: int
project: string
rationale: string
}[]
conclusion: string
}
}
FML (Frags Modeling Language) | Getting Started | Cheat Sheet | Examples
Documentation for FML & Gemini Agent Workflows — Maintained by Frags HQ