Docs/Guides/Design your output schemas
Plans6 min read

Design your output schemas

A session's schema is a contract: it's the exact JSON shape the model must return, and it's what every downstream session — and your application — reads. A good schema makes the model more accurate (it knows precisely what to produce) and makes the plan's output easy to consume. This guide covers the full syntax and the design habits that keep schemas clean.

Prerequisites

The basics

A schema block lists fields and their types:

FML
schema {
    summary:   string
    keyPoints: string[]
    published: bool
}

Syntax rules:

  • Field names are unquoted identifierssummary, not "summary".
  • Types: string, int, float, bool, nested { } objects, Type[] arrays, and $ComponentName refs.
  • Mark a field optional by appending ?: url?: string. Everything else is required.

Comments are instructions, not decoration

This is the highest-leverage habit in the whole language. Inline # comments on schema fields are read by the model and used as field descriptions:

FML
schema {
    summary:   string    # one concise paragraph, no bullet points
    sentiment: string    # exactly one of: positive, neutral, negative
    score:     int       # confidence 0-100
}

Those comments meaningfully steer the output. Add one to every field unless the name is completely self-explanatory — they're the cheapest quality win available.

Keep the root flat

The session name is already the outer wrapper for its output. A session called overview publishes under context.overview, so wrapping the fields in another overview object just creates redundant nesting:

FML
# AVOID — produces context.overview.overview.summary
schema {
    overview: {
        summary:   string
        keyPoints: string[]
    }
}

# PREFER — produces context.overview.summary
schema {
    summary:   string
    keyPoints: string[]
}

Nest only when the data genuinely has sub-structure:

FML
# FINE — metadata is a real sub-object
schema {
    summary:  string
    metadata: {
        author:    string
        createdAt: string   # ISO 8601 timestamp
    }
}

Scalar and array shorthand

When a session's whole output is a single value or a flat list, skip the block and declare the type directly:

FML
schema string[]     # the output is a list of strings
schema int          # the output is a single integer

This pairs naturally with iterate: an iterating session runs once per element and collects the results into an array, so its schema must be an array — either schema type[] or a block with []:

FML
session("elaborate",
        after="gather",
        iterate="context.gather.keyPoints") {
    - Expand the key point "{{ .it }}" into a paragraph.
    schema string[]     # array required when iterate is set
}

Reuse shapes with components

When the same structure appears in more than one session, define it once in a root-level components block and reference it with $Name:

FML
components {
    schema("SourceRef") {
        id:    string   # unique identifier of the source
        title: string   # human-readable title
        url?:  string   # optional URL; omit if unavailable
    }
}

session("gather") {
    - Extract the sources used.
    schema {
        summary: string
        sources: $SourceRef[]   # reuse the component as an array
    }
}

Component schemas follow the exact same syntax as session schemas — fields, optionals, comments, nesting. Reference one anywhere a type is expected: sources: $SourceRef[] or primary: $SourceRef.

Sessions without a schema

A schema is optional. Omit it and the session still runs — its output is just the model's raw text response instead of a structured object:

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

The session's result lands in context.search_web (and in the plan output) as a single string — the model's free-form answer verbatim:

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

Skip the schema when the session's job is to produce prose for a person to read, or when a downstream session will consume the text as free-form input. Add a schema as soon as you need to reference specific fields (context.search_web.answer), enforce a shape, or hand structured data to another tool — a downstream session can't reach into a raw string.

This example uses the built-in web search tool — see Search the web for how use search works.

Common mistakes

MistakeFix
Wrapping fields in a top-level object named after the sessionDefine fields flat at the root — the session name is the wrapper
Quoting field namesUse unquoted identifiers: summary: string
A non-array schema on an iterate sessionUse schema type[] or schema { ... }[]
Skipping field commentsAdd a # description to every non-obvious field — the model reads them
Referencing $Name with no components blockDefine the type in a root-level components { } block

Next steps

← All guides

Shape a session's typed output — field syntax, comments the model reads as instructions, flat-vs-nested structure, scalar shorthand, and reusable $Component refs.