Steps
The atomic primitive. Run one unit of model judgment that produces a typed value.
A step runs one model turn — or as many as it takes — until the model produces a value matching your schema. It is the smallest piece of judgment Bridle exposes.
Signature
from bridle import step
step(
prompt: str,
*,
schema: type[T],
context: Any = None,
tools: Sequence[Tool] = (),
label: str | None = None,
) -> TIt returns a Call typed as T. The annotation lies on purpose — the value resolves on first use, but the type checker treats it as already-the-result, which is what your code wants.
The minimum
Prompt and schema:
from pydantic import BaseModel
from bridle import step
class Brief(BaseModel):
headline: str
body: str
brief = step("write a brief about Mars weather", schema=Brief)
print(brief.headline) # resolves nowThe model has no extra context — just the prompt — and produces a Brief.
Pass context
context is anything JSON-serializable. Pass values from earlier steps:
plan = step("draft a plan", schema=Plan, context=topic)
brief = step("write the brief", schema=Brief, context=(topic, plan))Internally, the context is rendered as JSON and appended to the prompt. Pydantic models, dicts, lists, tuples, and primitive types all work. Custom classes need to be serializable — wrap them in a BaseModel or convert to a dict first.
Add tools
Pass @tool-decorated functions as tools=. The model can call any of them during the step:
from bridle import step, tool
@tool
def search(query: str) -> list[str]:
"""Search the web. Returns up to 10 result URLs."""
...
sources = step(
"gather three sources on Mars weather",
schema=list[Source],
tools=[search],
)The model loops: call a tool, see the result, call another, eventually call the synthetic __bridle_return__ with the value. Bridle handles the loop. See tools for the tool side.
Schema retries
When the model returns invalid arguments to __bridle_return__, Bridle feeds the validation error back as a corrective tool result and lets it try again. The default cap is three attempts; on the fourth, SchemaSatisfactionError raises.
The cap shows up in the trace as retry events with reason: "schema". If you see these regularly, your schema is too tight, your prompt is too vague, or both.
Pitfalls
Forgetting to resolve before returning a non-attribute value.
# WRONG — returns a Call, the caller may not realize it
return step("write the brief", schema=Brief, context=ctx)Inside an @agent, the returned Call is resolved automatically by the dispatcher. Outside an agent, wrap with bridle.resolve if your caller expects the value:
brief = bridle.resolve(step("write the brief", schema=Brief, context=ctx))Putting decisions in step instead of branch. A step whose schema is bool runs the same model loop with tools enabled — slower and more expensive than necessary. Use branch for one-shot decisions.
Schemas that require text the model has to generate inside the function call. A Brief with a 2000-token body field is fine. A schema with a single text: str field where the model has to produce 5000 tokens of structured prose inside one tool argument is not. Break the work into a step that produces the structure and a step that produces the prose, or relax the schema.
A complete example
import bridle
from bridle import agent, step, tool
from bridle.models.anthropic import install
from pydantic import BaseModel
class Topic(BaseModel):
title: str
angle: str
class Plan(BaseModel):
topics: list[Topic]
@tool
def trending(category: str) -> list[str]:
"""Return trending topic strings for the category."""
return ["solar wind", "polar caps", "dust storms"]
@agent(input=str, output=Plan, model="claude-sonnet-4-6")
def planner(category: str) -> Plan:
return step(
f"draft three topics under {category}",
schema=Plan,
context=category,
tools=[trending],
)
install()
plan = bridle.resolve(planner("Mars climate"))
for t in plan.topics:
print(f"- {t.title}: {t.angle}")