> ## Documentation Index
> Fetch the complete documentation index at: https://docs.tinyfish.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Why Use an Output Schema

> Understand how an output schema gets you the same JSON fields and types from every run, on any site, instead of a different shape each time

An output schema is a JSON Schema you send with a run in the `output_schema` field. It tells TinyAgent the exact shape the final `result` has to come back in: which fields, what type each one is, and which ones are required. The goal still says what to find. The schema says what the answer looks like.

## Why describing the shape in the goal isn't enough

Without a schema, TinyAgent decides how to structure the result from the goal's wording and the page in front of it. One run is usually fine. The problem shows up when you run the same goal on many pages, or on the same page over time, and feed the results to code. Each site lays out its data differently, so each run picks its own field names, types and nesting.

Here's one goal sent to two SaaS pricing pages, `linear.app/pricing` and `notion.com/pricing`, with no schema:

```txt theme={null}
Extract all pricing plan details from this page including plan name,
monthly price, and what's included
```

Both runs completed, and both returned a paragraph of Markdown inside `result` rather than JSON fields. Trimmed to the first plan:

**Linear, no schema**

```json theme={null}
{
  "result": "Here are all the pricing plan details from Linear:\n\n**1. Free**\n- Monthly Price: $0/month\n- What's Included:\n  - Unlimited members\n  - 2 teams\n  - 250 issues\n ..."
}
```

**Notion, no schema**

```json theme={null}
{
  "result": "Here are the Notion pricing plan details extracted from https://www.notion.com/pricing:\n\n---\n\n**1. Free Plan**\n- **Price:** $0 per member/month\n- **What's included:**\n  - Trial of Notion AI\n ..."
}
```

The same goal produced two different layouts. Linear's plan is "Free" and its price line is "Monthly Price:". Notion's plan is "Free Plan", its price line is "**Price:**", and its answer ends with an extra note about yearly billing. To use either in code you'd have to parse prose, with a special case for each site.

Add this schema to the same goal:

```json theme={null}
{
  "type": "object",
  "properties": {
    "plans": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "monthly_price": { "type": "string", "nullable": true },
          "is_free": { "type": "boolean" }
        },
        "required": ["name", "is_free"]
      }
    }
  },
  "required": ["plans"]
}
```

The Linear run now returns JSON in that shape:

```json theme={null}
{
  "plans": [
    { "name": "Free", "monthly_price": "$0", "is_free": true },
    { "name": "Basic", "monthly_price": "$10 per user/month (billed yearly)", "is_free": false },
    { "name": "Business", "monthly_price": "$16 per user/month (billed yearly)", "is_free": false },
    { "name": "Enterprise", "monthly_price": "Custom pricing (contact sales)", "is_free": false }
  ]
}
```

| Without a schema | With a schema |
| - | - |
| `result` is one Markdown string | `result` is a JSON object with a `plans` array |
| Labels and plan names change from site to site | Every plan has `name`, `monthly_price` and `is_free` |
| Extra notes can show up anywhere in the text | Only the fields in the schema come back |
| Your parser needs a special case per site | One parser reads every run |

`monthly_price` is a string because pricing pages show values like "\$10 per user/month (billed yearly)" and "Custom". A `number` field can't hold those. Parse the string in your own code.

## The schema wins over the goal

When the goal and the schema disagree, the result follows the schema. A goal that says "list 10 tools" with a schema that describes a single object returns one object, the first match. To get a list, put an array field in the schema, like `plans` above. The top level of a schema is always an object, so lists always live inside a field.

This is also why a schema helps when you can't control the goal's wording, for example when another AI agent writes the goal. However the goal is phrased, the result keeps your shape.

> [!WARNING]
>
> **A schema fixes the shape, not the content.** A run can match your schema and still hold less than you expected. If a field's type can't hold what the page shows, the item is dropped instead of failing the run. A price that renders as `£63.00` can't fit a `number` field, so the run returns `"products": []` rather than an error. The same goes for a goal that can't be met: asked for a product that doesn't exist on `scrapeme.live/shop`, with a schema requiring `name` and `price`, the run finished `COMPLETED` and returned `{"name": "", "price": ""}`. Use `string` for any value with symbols or units, and check for empty arrays and empty values before you use a result.

## Bad schemas fail before the run starts

TinyFish checks the schema when you send the request, before a browser opens. A schema that uses something unsupported, such as `oneOf` or `additionalProperties`, returns a `400` that names the keyword and where it is, and no run is created. You find out in the first second, not after a long run comes back in the wrong shape.

The check supports a subset of JSON Schema, not all of it. The [Structured-Output Schema reference](https://docs.tinyfish.ai/agent/api/agent/structured-output.md) lists the supported types and keywords, the size and depth limits, and the usual rewrites, for example `anyOf` instead of `oneOf`, and `nullable: true` instead of `type: ["string", "null"]`.

## One schema, every run and every surface

The same rules apply wherever you start a run: the REST API, the SDKs, the CLI, the Playground and MCP integrations all pass the schema to the same validator. A schema you test in the Playground works unchanged in code.

For many URLs, `/run-batch` takes `output_schema` once at the top level and applies it to every run in the batch. Individual items in `runs` can't set their own, so one batch can't mix shapes:

```json theme={null}
{
  "output_schema": {
    "type": "object",
    "properties": {
      "plans": {
        "type": "array",
        "items": {
          "type": "object",
          "properties": {
            "name": { "type": "string" },
            "monthly_price": { "type": "string", "nullable": true }
          },
          "required": ["name"]
        }
      }
    },
    "required": ["plans"]
  },
  "runs": [
    { "url": "https://www.notion.com/pricing", "goal": "Extract all pricing plans" },
    { "url": "https://slack.com/pricing", "goal": "Extract all pricing plans" },
    { "url": "https://linear.app/pricing", "goal": "Extract all pricing plans" }
  ]
}
```

The run keeps the schema you sent. `GET /v1/runs/{id}` and `runs.list()` return it as `output_schema`, so you can see later which shape a stored result was asked for.

## A run with a schema

This is the Linear run from above, in code:

**Python**

```py theme={null}
from tinyfish import TinyFish

client = TinyFish()

run = client.agent.run(
    url="https://linear.app/pricing",
    goal="Extract all pricing plan details from this page including plan name, monthly price, and what's included",
    output_schema={
        "type": "object",
        "properties": {
            "plans": {
                "type": "array",
                "items": {
                    "type": "object",
                    "properties": {
                        "name": {"type": "string"},
                        "monthly_price": {"type": "string", "nullable": True},
                        "is_free": {"type": "boolean"},
                    },
                    "required": ["name", "is_free"],
                },
            },
        },
        "required": ["plans"],
    },
)

print(run.result)
```

**TypeScript**

```ts theme={null}
import { TinyFish } from "@tiny-fish/sdk";

const client = new TinyFish();

const run = await client.agent.run({
  url: "https://linear.app/pricing",
  goal: "Extract all pricing plan details from this page including plan name, monthly price, and what's included",
  output_schema: {
    type: "object",
    properties: {
      plans: {
        type: "array",
        items: {
          type: "object",
          properties: {
            name: { type: "string" },
            monthly_price: { type: "string", nullable: true },
            is_free: { type: "boolean" },
          },
          required: ["name", "is_free"],
        },
      },
    },
    required: ["plans"],
  },
});

console.log(run.result);
```

**Result**

```json theme={null}
{
  "plans": [
    { "name": "Free", "monthly_price": "$0", "is_free": true },
    { "name": "Basic", "monthly_price": "$10 per user/month (billed yearly)", "is_free": false },
    { "name": "Business", "monthly_price": "$16 per user/month (billed yearly)", "is_free": false },
    { "name": "Enterprise", "monthly_price": "Custom pricing (contact sales)", "is_free": false }
  ]
}
```

## How it connects

| Concept | How it relates to the schema | Read more |
| - | - | - |
| Goal | Says what to find. The schema says what shape the answer takes. Keep the goal specific and let the schema carry the field names. | [Goals](https://docs.tinyfish.ai/key-concepts/goals.md) |
| Run | The schema shapes `result` on a completed run. `COMPLETED` still means the run finished, not that the goal succeeded. | [Runs](https://docs.tinyfish.ai/agent/explanation/runs.md) |
| Batch runs | One top-level `output_schema` covers every run in a `/run-batch` request. | [Endpoints](https://docs.tinyfish.ai/agent/api/agent/endpoints.md) |
| Playground | The Schema button builds the same schema field by field. It only applies once the panel's Output Schema toggle is On. | [Get a TinyAgent Run as Structured JSON](https://docs.tinyfish.ai/agent/how-to/structured-output.md) |
| Errors | An unsupported schema returns `400` before the run starts, with the failing keyword and path. | [Structured-Output Schema](https://docs.tinyfish.ai/agent/api/agent/structured-output.md#example-errors) |

## Related

> [card] [Get a TinyAgent Run as Structured JSON](https://docs.tinyfish.ai/agent/how-to/structured-output.md)
>
> Build and run a schema in the Playground and the API

> [card] [Structured-Output Schema](https://docs.tinyfish.ai/agent/api/agent/structured-output.md)
>
> Supported types, keywords, limits and rewrites

> [card] [Run TinyAgent Across Multiple URLs](https://docs.tinyfish.ai/agent/how-to/multiple-urls.md)
>
> Send the same goal to many pages

> [card] [Runs](https://docs.tinyfish.ai/agent/explanation/runs.md)
>
> Run lifecycle and what COMPLETED means

> [card] [How TinyAgent works](https://docs.tinyfish.ai/agent/explanation/how-tinyagent-works.md)
>
> What happens between sending a goal and getting a result back


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.