Bridle
Concepts

Typed I/O and schemas

Pydantic models are the contract between your code and the model. Bridle validates every value that crosses.

Every value the model produces in a Bridle program is typed. step, branch, and loop each take a schema= and return a value matching that schema. @agent(input=, output=) validates the function's input and return value. Pydantic does the validation; Bridle wires it in at every boundary.

What a schema can be

The schema= parameter accepts:

  • A Pydantic BaseModel. The common case.
  • A bare type: bool, int, str, float, list[X], dict[K, V]. Bridle wraps these in a synthetic Pydantic root model.
  • An Enum. For multi-way decisions in a branch.
  • A Literal[...]. Same idea as an enum, lighter weight.

A BaseModel is the right default. Bare types are useful for the simplest possible step — "give me the next number" — and enums are the right shape for branching on more than two outcomes.

from enum import Enum
from typing import Literal

class Verdict(Enum):
    APPROVE = "approve"
    REJECT = "reject"
    ESCALATE = "escalate"

verdict = branch("decide", schema=Verdict, context=case)
match verdict:
    case Verdict.APPROVE: ...
    case Verdict.REJECT: ...
    case Verdict.ESCALATE: ...

# Same shape, lighter:
verdict: Literal["approve", "reject", "escalate"] = branch("decide", schema=Literal["approve", "reject", "escalate"], context=case)

How validation runs

Inside a step, Bridle injects a synthetic __bridle_return__ tool whose parameter schema is your schema=. The model can call other tools you provided, but it ends the step by calling __bridle_return__ with arguments that match the schema.

If the arguments do not validate, Bridle feeds the validation error back to the model as a corrective tool result and lets it try again. By default it tries up to three times before raising SchemaSatisfactionError. Tune the budget per call:

step("...", schema=Brief, context=ctx)  # default: 3 schema retries
# Coming in v0.1: max_schema_retries as a first-class kwarg.
# Today, override via Call.options:
call = step("...", schema=Brief, context=ctx)
call.options["max_schema_retries"] = 5

The retries happen inside one step, not across multiple calls. The trace records each as a retry event with reason: "schema".

Use BaseModel aggressively

The schemas that work best in practice are explicit and narrow:

class Source(BaseModel):
    url: str
    summary: str   # one or two sentences, not the full page

class Plan(BaseModel):
    topics: list[Topic]
    rationale: str

A few rules of thumb:

  • Fields are nouns. headline, topics, rationale. The model fills nouns better than instructions-disguised-as-fields.
  • Constrain ranges. Use Field(min_length=, max_length=, ge=, le=) where it matters. Validation enforces the constraint.
  • Use enums for closed sets. Don't make the model invent strings from a fixed list.
  • Keep nested depth shallow. Two or three levels max. Deep nesting confuses both the model and the reader.
  • Document fields when the name is not enough. Pydantic descriptions get into the schema and the model sees them.

Input validation on agents

@agent(input=Schema, output=Schema) validates the first positional argument against input and the return value against output:

class Query(BaseModel):
    topic: str
    depth: Literal["shallow", "deep"] = "shallow"

@agent(input=Query, output=Brief, model="claude-sonnet-4-6")
def brief_writer(q: Query) -> Brief:
    ...

If the caller passes a dict, Bridle coerces it through the schema:

brief_writer({"topic": "Mars weather", "depth": "deep"})  # validated, coerced

If the body returns something that does not match output, Pydantic raises a ValidationError at the agent boundary.

Schemas in caching

The cache key includes a fingerprint of the schema. Two calls with the same prompt and context but different schemas have different cache keys — a structural change to your BaseModel invalidates the cache automatically. See caching for the full key composition.

Next

On this page