How to use contracts¶
Overview¶
Contracts let you register a Pydantic model as a named, versioned schema in a central registry, and then reference it by name from anywhere in your system — nodes, tools, mappings, and the governance plane. This guide shows how to define a contract, register it, and attach it to a workflow step.
Minimal example¶
import asyncio
from pydantic import BaseModel
from zeroth.contracts.registry import (
ContractRegistry,
ContractReference,
StepContractBinding,
)
from zeroth.platform.storage.async_sqlite import AsyncSQLiteDatabase
class SummariseInput(BaseModel):
text: str
max_tokens: int = 512
class SummariseOutput(BaseModel):
summary: str
async def main() -> None:
db = AsyncSQLiteDatabase(path=":memory:")
await db.connect()
registry = ContractRegistry(db)
await registry.initialize()
# Register both contracts (version numbers are assigned automatically)
in_ref = await registry.register("summarise.input", SummariseInput)
out_ref = await registry.register("summarise.output", SummariseOutput)
# Attach the contracts to a workflow step
binding = StepContractBinding(
step_name="summarise",
input_contract=in_ref,
output_contract=out_ref,
)
print(binding)
# Later, resolve a contract by name/version to get its schema or class back
resolved = await registry.get(ContractReference(name="summarise.input"))
print(resolved.json_schema)
asyncio.run(main())
Common patterns¶
- Name contracts after the concept, not the call site. Prefer
customer.profile.v1overget_profile_inputso the same contract can be reused across nodes, tools, and APIs. - Register at startup. Call
registry.register(...)for every model your service uses during application bootstrap, not lazily — this gives you a fail-fast at boot if something is misconfigured. - Resolve by reference, not by class. In downstream code, pass a
ContractReference(name=..., version=...)instead of the Python class directly; this keeps runtime code decoupled from the class path. - Tool bindings. For governed tools, use
ToolContractBindinginstead ofStepContractBinding; it captures extra metadata like execution placement and side-effect flags.
Pitfalls¶
- Editing in place. Never mutate a registered contract — always register a new version. Old runs reference old versions.
- Unstable
model_path. Contracts persist the dotted path to your Pydantic class. Renaming or moving a class breaks resolution; useContractTypeResolutionErrorto detect this in tests. - Forgetting to
await registry.initialize(). The registry lazily creates its schema; callingregisterfirst raisesContractRegistryError. - Mixing pydantic v1 and v2 models. Zeroth assumes pydantic v2. Older
BaseModelsubclasses can be registered but will not resolve correctly. - Version drift between services. If two services register different classes under the same
(name, version), whoever writes last wins. Make registration the responsibility of exactly one service.