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

# Designing the Output Schema

> Flat, described, nullable: the schema shape that fills every cell instead of shipping a partial result

The `output_schema` is the contract. When the prompt and the schema disagree about the shape, the schema wins. When the schema asks for more than the run can fill, the agent ships a partial result rather than failing, and the run still says `completed`. Schema design is the single biggest lever on whether cells come back filled.

## The shape that works

```json theme={"system"}
{
  "type": "array",
  "description": "One row per company matching the filters.",
  "items": {
    "type": "object",
    "required": ["company", "domain", "hq_country", "latest_round", "source_url"],
    "additionalProperties": false,
    "properties": {
      "company": { "type": "string", "description": "Company name as written on its own website." },
      "domain": { "type": "string", "description": "Bare hostname of the official site, e.g. acme.ai. No protocol, no path." },
      "hq_city": { "type": ["string", "null"], "description": "HQ city per the company's site or the funding announcement. Null if not stated." },
      "hq_country": { "type": "string", "description": "HQ country, English short name, e.g. Germany." },
      "latest_round": { "type": "string", "enum": ["Series A", "Series B", "Series C", "Series D+", "Other"], "description": "Most recent round announced since January 2024. 'Other' for growth or undisclosed-stage rounds." },
      "latest_round_year": { "type": ["integer", "null"], "description": "Four-digit year the latest round was announced. Null if unclear." },
      "source_url": { "type": "string", "description": "Direct URL of the announcement or article confirming the round. Never a search results page." }
    }
  }
}
```

Seven fields. Every one described. Identity fields required, everything uncertain nullable. One enum for the judgment call. One source URL.

## Rules

### Keep it flat and narrow

Aim for 5 to 8 fields per run. Wide schemas are the strongest predictor of partial output: in a recent month, dataset runs with 1 to 5 fields shipped a partial result 28% of the time; runs with more than 20 fields, 73% of the time. When a record needs 20 fields, use two agents: one to discover the entities, one to fill the columns, joined on your key.

Use an array root for [dataset building](https://docs.nimbleway.com/nimble-sdk/web-search-agents/use-cases/dataset-building.md) and for [enrichment](https://docs.nimbleway.com/nimble-sdk/web-search-agents/use-cases/enrichment.md) runs that pass a list in `input_data` (a list of rows requires a table-shaped schema; a single object in `input_data` takes an object root). Research uses an object root. Nesting is allowed to five levels; stay at one.

### Describe every field

The description is the prompt for that field. State what it is, the format, the unit, and the null rule.

**Undescribed**

```json theme={"system"}
"employee_count": { "type": "string" }
```

The agent decides whether that is a number, a band, a string with a date, or "about 200".

**Described**

```json theme={"system"}
"employee_band": {
  "type": "string",
  "enum": ["1-10", "11-50", "51-200", "201-1000", "1000+"],
  "description": "Headcount band from the company's own site or a dated press release. Use the most recent figure. Null if no public figure exists."
}
```

Describe all fields or none. Partially described schemas did worse than either in a recent month, most likely because only the hard fields got descriptions and the easy ones then came back in inconsistent formats.

### Name fields for what they hold

`ceo_name`, not `name`. `annual_revenue_usd_2025`, not `revenue`. `latest_round_year`, not `year`. The name is read by the agent too.

### Nullable for anything uncertain

Type uncertain fields as `["string", "null"]` and keep them out of `required`. Reserve `required` for identity fields the row cannot exist without. A required field the web cannot fill forces the agent to either invent a value or abandon the row.

A cited `null` is a good answer. The [trust report](https://docs.nimbleway.com/nimble-sdk/web-search-agents/trust.md#trust-for-structured-outputs) grades a verified absence `high`.

### Enums for judgments

Any field that is a classification or a yes/no gets an enum. Add a third value that separates "checked, and absent" from "could not be checked":

```json theme={"system"}
"has_pricing_page": {
  "type": "string",
  "enum": ["yes", "no", "cannot_verify"],
  "description": "yes if the official site has a public pricing or plans page (a 'Contact sales' page counts). no only after the site was read and no such page exists. cannot_verify if the site could not be read."
}
```

### Numbers as numbers, dates as ISO strings

`"price_usd": { "type": "number" }` with "number only, no currency symbol" in the description. Dates as `YYYY-MM-DD` strings, stated in the description. Free-text numbers cannot be sorted or summed downstream.

### Ask for a source URL, not a confidence score

When downstream systems need the evidence next to the value, add a `source_url` field per claim group. Never add `confidence`, `reasoning` or `is_verified` fields: the [trust report](https://docs.nimbleway.com/nimble-sdk/web-search-agents/trust.md) already carries a graded citation per path, and asking the agent to grade itself produces a less reliable copy.

## Wrong and fixed

**Nested array in an object**

**Wrong** (enrichment run):

```json theme={"system"}
{ "type": "object", "properties": { "competitors": { "type": "array", "items": { "type": "object", "properties": { "name": { "type": "string" }, "products": { "type": "array", "items": { "type": "object", "properties": { "sku": { "type": "string" } } } } } } } } }
```

Three levels of arrays for one entity. The write phase exhausts its budget and ships a fragment.

**Fixed**: one flat row per competitor in a dataset-building run, then a second enrichment run per competitor for products.

**\$ref**

**Wrong**:

```json theme={"system"}
{ "properties": { "hq": { "$ref": "#/$defs/address" } }, "$defs": { "address": { "type": "object" } } }
```

Fails at run start with `Reference '#/$defs/address' not found`.

**Fixed**: inline the definition.

**Constraint keywords**

**Wrong**:

```json theme={"system"}
"email": { "type": "string", "format": "email", "minLength": 5 }
```

`422: output_schema uses unsupported keyword(s): format, minLength`.

**Fixed**:

```json theme={"system"}
"email": { "type": ["string", "null"], "description": "A valid email address found on an official page. Null if none is published." }
```

**Too wide**

**Wrong**: 26 properties on one row, none described.

Accepted at create time, but the agent ships partial rows at run time.

**Fixed**: 7 described fields now; a second agent for the other 19 keyed on `domain`.

**Required but unknowable**

**Wrong**:

```json theme={"system"}
"required": ["company", "total_funding_usd", "ceo_email"]
```

Private companies have no public funding total and few executives publish an email. The agent invents or drops the row.

**Fixed**: `required: ["company", "domain"]`; funding and email typed `["string", "null"]`.

## Hard limits

### Allowed roots

The root must be one of:

* An **object** with non-empty `properties`
* An **array of objects** (`items.type` must be `"object"` with `properties`)

Omit `output_schema` (or pass `{}`) to let the generator infer the shape from the prompt.

### Supported examples

**Object root with nullable fields:**

```json theme={"system"}
{
  "type": "object",
  "properties": {
    "company_name": { "type": "string" },
    "website": { "type": ["string", "null"] }
  }
}
```

**Array root (table shape):**

```json theme={"system"}
{
  "type": "array",
  "items": {
    "type": "object",
    "properties": {
      "company_name": { "type": "string" },
      "hq_city": { "type": ["string", "null"] }
    }
  }
}
```

**Nested array of objects:**

```json theme={"system"}
{
  "type": "object",
  "properties": {
    "key_contacts": {
      "type": "array",
      "items": {
        "type": "object",
        "properties": {
          "name": { "type": "string" },
          "email": { "type": ["string", "null"] }
        }
      }
    }
  }
}
```

### Rejected patterns

The API returns **422** when a schema violates these hard limits:

| Limit | Rule |
| - | - |
| Root shape | No scalar roots, empty objects, or root `anyOf` |
| Array root | Must be an array of objects, not primitives |
| Nullable fields | Use `{"type": ["string", "null"]}`, not standalone `{"type": "null"}` |
| Nesting depth | At most **5** levels (objects and arrays count) |
| Property count | At most **100** properties across all levels |
| Enum size | At most **500** enum values total; enums with more than 250 values are also capped at 7,500 combined characters |
| Unsupported keywords | `format`, `pattern`, `minLength`, `maxLength`, `minimum`, `maximum`, `multipleOf`, `minItems`, `maxItems`, `uniqueItems`, `minProperties`, `maxProperties`, `patternProperties`, `propertyNames`, `contains`, `minContains`, `maxContains`, `unevaluatedItems`, `unevaluatedProperties` |

Move validation constraints into field `description` text instead of JSON Schema keywords.

**Unsupported examples:**

```json theme={"system"}
{ "type": "array", "items": { "type": "string" } }
```

```json theme={"system"}
{
  "type": "object",
  "properties": {
    "email": { "type": "string", "format": "email" }
  }
}
```

```json theme={"system"}
{
  "type": "object",
  "properties": {
    "x": { "type": "null" }
  }
}
```

```json theme={"system"}
{
  "type": "object",
  "properties": { "name": { "type": "string" } },
  "anyOf": [{ "required": ["name"] }]
}
```

### Still accepted

These are **not** hard-failed:

* `required` arrays on objects
* `additionalProperties: false`
* Nested `anyOf` / `oneOf` for nullability (not at the root)
* Enums within the size limits above
* Nested arrays of objects or primitives under an object root

### Field maps are invalid

Each field must be a JSON Schema subschema, not a bare type string:

```json theme={"system"}
{ "properties": { "city": { "type": "string" } } }
```

Not:

```json theme={"system"}
{ "city": "string" }
```

### Agent generation behavior

When creating or updating an Agent, the design path can infer and sanitize a schema from your goals and template. The generator strips unsupported keywords and fixes common mistakes (such as standalone `null`) before validation.

When you pass an explicit `output_schema` on create, update, or run, it is validated immediately. Fix the schema and retry on **422**.

### Common validation errors

| Error fragment | Cause |
| - | - |
| `must not use "anyOf"` | Root-level `anyOf` |
| `array of objects` | Root array whose `items` is not an object |
| `standalone "null" type` | Property typed as `"null"` alone |
| `unsupported keyword(s)` | A rejected keyword such as `format` or `pattern` |
| `nesting depth` | More than 5 nested object/array levels |
| `maximum of 100 properties` | Too many properties across the schema |
| `non-empty "properties"` | Object root with missing or empty `properties` |
| `declares nothing to write` | Field map syntax instead of proper subschemas |

Example **422** response:

```json theme={"system"}
{
  "detail": [
    {
      "type": "value_error",
      "loc": ["body", "output_schema"],
      "msg": "Value error, output_schema uses unsupported keyword(s): format. Remove them (or move constraints into field descriptions).",
      "input": {
        "type": "object",
        "properties": {
          "email": { "type": "string", "format": "email" }
        }
      }
    }
  ]
}
```

## Next Steps

> **[Writing the Task](https://docs.nimbleway.com/nimble-sdk/web-search-agents/best-practices/writing-the-task.md)**
>
> Input, skill and goals

> **[Evaluating Results](https://docs.nimbleway.com/nimble-sdk/web-search-agents/best-practices/evaluating-results.md)**
>
> Completed is not done

> **[Trust](https://docs.nimbleway.com/nimble-sdk/web-search-agents/trust.md)**
>
> How per-path claims are graded


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