Designing Schema Evolution So Sub-Agent Handoffs Never Break
Put a typed contract at the boundary where a downstream agent receives an upstream agent's output, and learn how to evolve that schema without breaking existing flows — with validation code, a migration sequence, and the production symptoms to watch for.
When you run sub-agents side by side in Antigravity 2.0, one day a downstream agent quietly goes silent.
No error. It just reports "the field I expected was empty" and stops. Trace it back and the cause is almost always the same: the upstream agent slightly changed the shape of its output while someone was improving its prompt.
As an indie developer running multi-app update work with a main/sub agent setup, I've been tripped up by this "silent downstream" more than once. This article shares how to remove that pain by design — through typed handoff contracts and disciplined schema evolution between agents.
Why something "succeeds" and still breaks
The tricky part of multi-agent systems is that each agent succeeds on its own.
The upstream succeeds in the sense that "it returned JSON." The downstream succeeds in the sense that "it received JSON." But the shapes don't mesh. Tests are green, logs look clean, and yet the deliverable comes out empty.
I call this state "silent handoff breakage." With untyped handoffs, a shape mismatch only surfaces far downstream at runtime — and usually only in production.
The key to preventing it is one thing: validate the upstream output as the downstream input contract, right at the boundary.
Pin the contract in code
First, make the shape of the deliverable that flows between agents explicit as a JSON Schema. Merely asking "please return it in this shape" inside a prompt leaves it to the model's mood. You need to reject mismatches mechanically at the boundary.
With it, the moment the upstream adds a field on its own, validation fails immediately. You gain early detection of "something extra slipped in," but in exchange the schema evolution below needs an extra step. I recommend erring on the strict side first, fully aware of that trade-off.
✦
Thank you for reading this far.
Continue Reading
What follows includes implementation code, benchmarks, and practical content we hope you'll find useful. This site runs without ads — server and development costs are supported entirely by members like you. If it's been helpful, we'd be truly grateful for your support.
WHAT YOU'LL LEARN
✦A concrete implementation that validates upstream output as the downstream input contract using JSON Schema
✦A version-field method for adding fields while preserving backward compatibility
✦The production symptoms of a broken handoff, plus three safeguards that stop recurrence
Secure payment via Stripe · Cancel anytime
✦
Unlock This Article
Get full access to the rest of this article. Buy once, read anytime. This site is ad-free — your support goes directly toward keeping it running.
Evaluate the contract before the downstream agent starts working. Validate right after receiving the payload, and if it doesn't conform, never launch the downstream at all. This is the one place that turns "silent breakage" into "explicit failure."
import jsonschemadef accept_handoff(payload: dict, schema: dict) -> dict: """The boundary where the downstream receives a deliverable. Stop immediately on mismatch.""" try: jsonschema.validate(instance=payload, schema=schema) except jsonschema.ValidationError as e: # Always record which field, what was expected, and what arrived path = "/".join(str(p) for p in e.absolute_path) or "(root)" raise HandoffError( f"Handoff contract violation at '{path}': {e.message}" ) from e return payloadclass HandoffError(Exception): """Raised when an upstream deliverable fails the downstream input contract."""
What matters here is recording "which field" failed and "why" in the error message.
A human reads sub-agent failure logs later. Staring at a log that says only validation failed at midnight is more draining than you'd think. Adding path shrinks root-cause analysis from minutes to seconds.
A realistic sequence for evolving the schema
Once operations begin, a request like "I want to add a confidence score to findings" is inevitable. That's where you hit the additionalProperties: False wall.
The approach I take is to bump schema_version and let old and new coexist. Don't apply a breaking change all at once — set up a migration window.
Define a V2 schema that adds the new field as optional (not in required)
Make the downstream validator accept both V1 and V2 (dual support via anyOf)
Switch the upstream to V2 output
Confirm via logs that every flow has moved to V2, then drop V1 acceptance
Follow this order and the handoff survives even when upstream and downstream deploy at different times. Make a new field required from the start and a single moment of mismatch wipes out every existing flow. Add as optional first; remove last. That's the one principle I'd urge you never to compromise on.
Symptoms I actually saw in production
In real operation, it showed up like this:
The downstream returns an empty deliverable, yet no agent throws
Re-running sometimes fixes it and sometimes doesn't (it tracked the upstream's deploy progress)
In a release-note generation pipeline for the App Store, the body text went entirely missing
That last one was a particularly scary symptom. In a flow that generates copy shipped to both Google Play and the App Store, the downstream was waving through a state of "the title arrived but the body key is absent." Before boundary validation, a human had to catch it during the pre-release review.
Once contract validation lived at the boundary, this kind of omission started stopping at the very first stage of the pipeline. The point of failure moved forward from "just before shipping" to "just after the handoff." That's the biggest peace of mind you can buy with design.
Three mechanisms to prevent recurrence
Finally, three mechanisms I build in so the same incident doesn't repeat.
First, have the upstream and downstream import the contract schema from the same file. Write the shape twice — once in each prompt — and one of them will always drift out of sync.
Second, embed schema_version in the deliverable itself. When you dig through logs later, you can see at a glance which contract produced that deliverable.
Third, split validation failures into "retryable errors" and "contract-violation errors." The former are transient (network, etc.) where retrying is meaningful. The latter are shape mismatches where retrying is pointless, so escalate to a human immediately.
When you're orchestrating several agents as an indie developer, plain boundary validation does more for a good night's sleep than clever orchestration does. Typed handoffs are an investment that compounds the more agents you add.
Thank you for reading.
Share
Thank You for Reading
Antigravity Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.