Skip to content

Architectural Patterns

theirish81 edited this page Sep 3, 2026 · 1 revision

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.


Pattern 1: Data Piping between Tools (Variable Routing)

Problem

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.

Strategy

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')"]
Loading

FML Implementation

# 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)
}

Benefits

  • 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.

Pattern 2: Context Management & Token Optimization

Problem

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.

Strategy

  1. Route the raw API call to an intermediate variable -> vars:raw_logs.
  2. Run a second call with an embedded JavaScript code(...) block to filter, sanitize, and compact the data.
  3. Route the sanitized result to vars:critical_events.
  4. 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)"]
Loading

FML Implementation

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
    }
}

Pattern 3: Deterministic Bypassing of the LLM (-> context:key)

Problem

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.

Strategy

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"]
Loading

FML Implementation

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.


Pattern 4: Chain-of-Thought Reasoning without Tools

Problem

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.

Strategy

  1. Pre-prompt (+): Instruct the model to perform free-form reasoning, review evidence, and draft step-by-step audit notes.
  2. 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
Loading

FML Implementation

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[]
    }
}

Pattern 5: Fan-Out / Fan-In (MapReduce via iterate)

Problem

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.

Strategy

  1. Gather Session (Fan-Out Preparation): Discovers all target items and outputs an array.
  2. 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.
  3. 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)"]
Loading

FML Implementation

# 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
    }
}

Clone this wiki locally