Docs/Guides/Connect Frags to your data
Plans8 min read

Connect Frags to your data

The hello-world plan only reasoned over its own prompts. Real plans pull in live data — answers from a web search, messages from an MCP server, rows from a database, results from an API. In Frags these are all tools, and a session can call them either deterministically or by letting the model decide.

This guide covers both approaches, then walks through three tools: a built-in web search, a real MCP example (Slack), and a database example (Postgres).

Prerequisites

  • The hello-world plan guide — you should be comfortable with sessions, prompts, and schemas.
  • Your tools configured in tools.json — see Configuration files. A plan can only reach connections that exist there.

How tools work

Every tool a plan uses must be declared at the root with require, using its type and name:

FML
require mcp Slack
require collection postgres
require apicp stripe

The three tool types are mcp (an MCP server), collection (built-in connectors like postgres, http, fs), and apicp (an OpenAPI-mapped API). The names match the connections in your tools.json.

The one exception is the built-in web search tool: it needs no require and no tools.json entry — you declare it only inside a session as use search, with no name after it. See Search the web below.

There are then two ways to actually call a tool:

  • PreCallcall("function") { … } invokes a specific tool function deterministically, before the session's prompts run. Use it when you know the exact function and arguments.
  • prePrompt — a + line with use <type> <name> in the session lets the LLM decide how to call the tool. Use it when the call depends on reasoning.
FML
require mcp Slack

session("summary") {
    use mcp Slack
    + Fetch the last 7 days of messages from the #sales channel and read them.
    - Summarize the key wins and blockers.

    schema { summary: string }
}

The examples below use PreCalls, since they call known functions with known arguments.

Search the web

The lowest-friction live-data tool is web search. It's built in, so — unlike every other tool — it needs no root require and no tools.json entry. Just add use search to a session and the model runs searches as it reasons:

FML
parameter("query", type=string, title="Search Query")

session("search_web") {
    use search

    + Search the internet for information about: {{ .params.query }}

    - Provide a comprehensive answer based on the search results.
}

What's going on here:

  • use search is the one tool declared only at the session level — no root require, and no name after it. (Every other tool type needs both a require and a name.)
  • The + prePrompt is where search actually runs: the model issues queries and reads the results before the main prompt.
  • The - prompt turns those results into the session's answer.

This session declares no schema, so its output is the model's raw text answer, landing in context.search_web as a plain string:

JSON
{
  "search_web": "The 2026 FIFA World Cup has not yet taken place. It is scheduled to be held from June 11 to July 19, 2026, and will be hosted by Canada, Mexico, and the United States. Therefore, there is no winner yet."
}

Add a schema when you need to pull specific fields out of the results instead of a prose answer — see Sessions without a schema.

Call an MCP server

Here's a trimmed version of a CEO-briefing plan that reads a Slack channel. It declares the Slack MCP, fetches a channel's history with a PreCall, cleans the raw payload with a transformer, and hands the result to a session:

FML
parameter("lookBackDays", type=string, default="7", title="Look back days")

require mcp Slack

# Reshape the raw tool output down to just the messages array
transformer("clean_history") {
    onFunctionOutput = "conversations_history"
    jmesPath         = "structuredContent.messages || messages"
}

# Deterministically fetch the channel history before the session runs.
# The result is routed into vars.salesRaw.
call("conversations_history") -> salesRaw {
    channel = "C091MES33UK"
    limit   = 200
}

session("sales-pulse", target="salesSignals") {
    - You are a business analyst preparing a CEO briefing.
      Here is the recent Slack history for the sales channel:
      ```json
      {{ json .vars.salesRaw }}
      ```
      Summarize the wins, blockers, and the team's overall sentiment for the week.

    schema {
        wins: string[]      # Deals closed, milestones landed
        blockers: string[]  # Active blockers or slowdowns raised in the channel
        sentiment: strong|steady|concerning|unclear  # Team momentum this week
        headline: string    # The single most important thing this week
    }
}

What each piece does:

  • require mcp Slack declares the Slack MCP server (configured in tools.json).
  • transformer(...) runs whenever the named function (conversations_history) returns, reshaping its output with a JMESPath expression before the LLM ever sees it — great for trimming noisy API payloads. Use jmesPath when you know the exact response shape.
  • call("conversations_history") -> salesRaw { … } is a PreCall: it runs before the prompts, calls the Slack tool with those arguments, and stores the cleaned result in vars.salesRaw.
  • The session's prompt reads that data with {{ json .vars.salesRaw }} and maps it into the schema.

Note the schema field sentiment: strong|steady|concerning|unclear — an inline enum constrains the model to one of those exact values.

Query a database

A database connection is a collection. A Postgres collection named X exposes a query function named X_postgres_query that takes a query string. Declare the collection, run your SQL in a PreCall, and route the rows into context:

FML
parameter("listingId", type=string)  # The listing to analyze

require collection listings

# Run a query and route the rows into context.listings_offers
call("listings_postgres_query") -> context:offers {
    query = "SELECT id, buyer_email, offer_price, status, lender_name
             FROM listing_offers
             WHERE listing_id = '{{ .params.listingId }}'
               AND status NOT IN ('withdrawn', 'draft')
             ORDER BY created_at DESC"
}

session("rank-offers") {
    context "Offers retrieved from the database:\n{{ json .context.offers }}"

    - You are a real estate analyst. Rank the offers from strongest to weakest
      by close certainty, and explain each ranking in one sentence.

    schema {
        offer_id: string
        rank: int      # 1 = strongest offer
        note: string   # Why this offer ranks where it does
    }[]
}

The new ideas here:

  • require collection listings declares a Postgres connection defined in tools.json; its query function is listings_postgres_query.
  • -> context:offers routes the PreCall result into context.offers. (Compare the Slack example's -> salesRaw, which routes into vars.salesRaw-> name writes to vars, -> context:name writes to context.)
  • context "…" injects those rows into the session's LLM context so the prompt can reason over them.
  • The query string interpolates the parameter with {{ .params.listingId }}. Because parameters are interpolated as text, validate or constrain any untrusted input before it reaches a query.

Parameters are substituted into the SQL as template strings, so treat query construction like any other place you build SQL from input — keep untrusted values constrained (e.g. validate that an ID is a UUID) rather than passing them through raw.

Next steps

← All guides

Pull live data into a plan — run a web search, call an MCP server (Slack), and query a database (Postgres) with PreCalls, transformers, and context routing.