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 situation
Enforcement guarantees what entered the system. The workflow still receives a plain object, and code that reaches into it by hand drifts from the form over time.
The recipe
import { z } from 'zod';
export const INTAKE_SCHEMA_VERSION = 3;export const IntakeResolverV1Schema = z.object({ customer: z.object({ name: z.string(), email: z.string() }), contract: z.object({ tier: z.enum(['starter', 'professional']), approved: z.boolean() }),});export type IntakeResolverV1 = z.infer<typeof IntakeResolverV1Schema>;import { parseResolverPayload } from '@hotmeshio/long-tail/build/sdk/index.js';import { IntakeResolverV1Schema } from '../forms/intake';
export async function recordIntake(response: unknown) { const intake = parseResolverPayload(IntakeResolverV1Schema, response); return saveCustomer(intake.customer.name, intake.contract.tier);}The workflow pins schemaVersion: INTAKE_SCHEMA_VERSION on its conditional, so the form people see and the schema code parses stay in step.
The guarantee
A payload that does not conform throws ResolverPayloadTypeError carrying the same field and message violation list as the server’s 422, with the ZodError as cause. Parse inside an activity so the check is checkpointed. See Schema enforcement.
Related patterns
Resolving from anywhere
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 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.
Forms and configuration
The form is data
A role's form is a versioned JSON Schema; each row pins the edition it renders, so editing the form never breaks work in flight.
