One contract, one 422
Every resolve surface rejects a bad payload with the same 422 body naming each field, so every client handles validation one way.
The situation
The same role is resolved by the dashboard, a webhook, an agent and a script. Each would otherwise invent its own error handling for a missing field or a wrong type.
The recipe
{ "error": "resolverPayload failed schema validation (2 violations)", "code": "schema_validation", "violations": [ { "field": "contact_email", "message": "Enter a valid email address" }, { "field": "tier", "message": "Must be one of: free, starter, professional, enterprise" } ], "role": "intake-reviewer", "schemaVersion": 3}| Surface | How it arrives |
|---|---|
| HTTP | The 422 response body |
| SDK | result.status === 422, body in result.data, narrow with isValidationErrorBody |
| MCP | The same JSON with isError: true |
| CLI | Each violation printed beneath the error line |
| Dashboard | Mapped into the form’s errors panel |
The guarantee
Validation runs before any state changes. The dashboard’s pre-submit check and the server gate are one shared module, so a payload that passes the panel passes the server. Bulk surfaces tag each violation with its escalationId, and one failing item blocks the batch. See Schema enforcement.
Related patterns
Resolving from anywhere
Validate before claiming
An agent's claim-and-resolve validates the payload first, so a rejected answer never leaves a row locked to the agent.
Resolving from anywhere
Roll out enforcement by watching 422s
Turn on schema enforcement for one role and let each 422 name the submitter and the exact fields it is missing.
Resolving from anywhere
Parse the resolution at the boundary
One zod schema gives the workflow its type, its runtime check and its version pin, so resolver payloads enter code already parsed.
The escalation boundary
Pin the form edition per row
Each row renders and validates against the form version its code was written for, while the role's form keeps evolving.
