feat(workflows)!: TypeBox-native input/output schemas (replace Zod) - #1181
Conversation
Workflow inputs and outputs are now declared with TypeBox schemas instead
of the legacy { type, required, default, choices } descriptor, and Zod is
removed entirely.
- Authoring: .input("prompt", Type.String()), .input("count", Type.Number({ default: 2 })),
.input("flavor", Type.Union([Type.Literal("a"), Type.Literal("b")])),
.output("packet", Type.Object({ topic: Type.String(), score: Type.Number() })).
Type.Optional(...) marks an optional field. Type is re-exported from
@bastani/workflows (with the Static and TSchema types) for single-import.
- Typing: ctx.inputs, the .run() return, and ctx.workflow(child).outputs are
precisely typed via Static<> (missing-required, wrong-type, and nested-shape
mismatches are caught statically; excess keys are enforced at runtime).
- Runtime: inputs and outputs validate via TypeBox Value (defaults applied,
precise path errors); serializable.ts reimplemented on TypeBox, dropping zod.
- A schema-introspection adapter derives the normalized field descriptor
(type/choices/default/required) from a TypeBox schema, so the inputs picker
UI, validation, and dispatch keep working without being rewritten
(TUnion<TLiteral> -> select, TBoolean -> toggle, TNumber -> number, etc.).
- Migrated all builtin and contract workflows and the test suite to the
TypeBox API; updated docs (coding-agent + workflows) and the CHANGELOG.
BREAKING CHANGE: the { type, required, default, choices } input/output
descriptor is removed; workflows must declare inputs/outputs with TypeBox
schemas. This feature was unreleased, so there is no migration path for
shipped consumers.
Assistant-model: Claude Opus 4.8
Code Review — TypeBox-native workflow schemasReviewed against Findings below, roughly in order of importance. Correctness / bugs1. Output
I'd lean toward the first — it preserves the "defaults always materialize" mental model and matches input semantics. 2. Nested schema mismatches lose path info in output validation. 3. function isPlainObjectValue(value: unknown): boolean {
if (value === null || typeof value !== \"object\" || Array.isArray(value)) return true;
const proto = Object.getPrototypeOf(value);
return proto === Object.prototype || proto === null;
}It's wired as a
The current code's intent is right; the predicate just has the wrong polarity for arrays. 4. const declaredDefault = (def as { default?: unknown }).default;This is exactly what Style / conventions (per
|
The TypeBox migration inlined select choices into Type.Union([Type.Literal(...)]), leaving the original const choice arrays (FLAVORS, VARIANTS, INVALID_MODES) unused, plus a stale reviewerErrorResult(iteration) param and an unused erasedDef binding. These tripped the project's noUnusedLocals/noUnusedParameters standard (enforced by the IDE) even though the root tsconfig does not set the flags. Remove them so the migrated built-in and .atomic contract workflows are clean. Assistant-model: Claude Opus 4.8
PR Review — TypeBox-native input/output schemasSolid migration — Code quality
Potential bugs / behavior
Performance
Security
Test coverage
Nice work overall — the typed |
…resh docs Post-migration polish for the TypeBox-native workflow schemas: - Precise output types where the shape is known, replacing loose Type.Array(Type.Unknown())/additionalProperties schemas: deep-research 'partitions' -> Type.Array(Type.String()); goal 'receipts' -> a precise Type.Object array; and the contract workflows (contract-valid echo/items, contract-hil-basic, contract-hil-nested-root, contract-parent, contract-complex-root) now declare precise Type.Object/Type.Array schemas or Type.Unsafe<Interface>(...) so ctx.inputs/.run() return/child.outputs are precisely typed and runtime validation enforces the real shape. - Enable noUnusedLocals/noUnusedParameters in the root tsconfig (the repo standard per AGENTS.md) so 'bun run typecheck'/lint/CI match the IDE and this can't drift again, and remove the dead locals/imports it surfaced across workflows, tests, coding-agent, subagents, and mcp. Exhaustiveness checks (const _x: never = ...) are preserved via an explicit reference. - Docs: packages/coding-agent/docs/workflows.md and packages/workflows README now steer authors toward precise TypeBox schemas (with a schema->Static table and a loose-vs-precise example) and document the Type.Unsafe<T> escape hatch for genuinely dynamic data. Assistant-model: Claude Opus 4.8
Review — TypeBox-native input/output schemasSolid migration overall. The Correctness — input validation silently ignores TypeBox refinements
So today, if an author writes: Two reasonable fixes:
Option 1 also fixes a related edge case in Code quality —
|
…1181) * feat(workflows)!: TypeBox-native input/output schemas (replace Zod) Workflow inputs and outputs are now declared with TypeBox schemas instead of the legacy { type, required, default, choices } descriptor, and Zod is removed entirely. - Authoring: .input("prompt", Type.String()), .input("count", Type.Number({ default: 2 })), .input("flavor", Type.Union([Type.Literal("a"), Type.Literal("b")])), .output("packet", Type.Object({ topic: Type.String(), score: Type.Number() })). Type.Optional(...) marks an optional field. Type is re-exported from @bastani/workflows (with the Static and TSchema types) for single-import. - Typing: ctx.inputs, the .run() return, and ctx.workflow(child).outputs are precisely typed via Static<> (missing-required, wrong-type, and nested-shape mismatches are caught statically; excess keys are enforced at runtime). - Runtime: inputs and outputs validate via TypeBox Value (defaults applied, precise path errors); serializable.ts reimplemented on TypeBox, dropping zod. - A schema-introspection adapter derives the normalized field descriptor (type/choices/default/required) from a TypeBox schema, so the inputs picker UI, validation, and dispatch keep working without being rewritten (TUnion<TLiteral> -> select, TBoolean -> toggle, TNumber -> number, etc.). - Migrated all builtin and contract workflows and the test suite to the TypeBox API; updated docs (coding-agent + workflows) and the CHANGELOG. BREAKING CHANGE: the { type, required, default, choices } input/output descriptor is removed; workflows must declare inputs/outputs with TypeBox schemas. This feature was unreleased, so there is no migration path for shipped consumers. Assistant-model: Claude Opus 4.8 * chore(workflows): drop unused locals in builtins and contract workflows The TypeBox migration inlined select choices into Type.Union([Type.Literal(...)]), leaving the original const choice arrays (FLAVORS, VARIANTS, INVALID_MODES) unused, plus a stale reviewerErrorResult(iteration) param and an unused erasedDef binding. These tripped the project's noUnusedLocals/noUnusedParameters standard (enforced by the IDE) even though the root tsconfig does not set the flags. Remove them so the migrated built-in and .atomic contract workflows are clean. Assistant-model: Claude Opus 4.8 * refactor(workflows): precise schemas, enable noUnused* repo-wide, refresh docs Post-migration polish for the TypeBox-native workflow schemas: - Precise output types where the shape is known, replacing loose Type.Array(Type.Unknown())/additionalProperties schemas: deep-research 'partitions' -> Type.Array(Type.String()); goal 'receipts' -> a precise Type.Object array; and the contract workflows (contract-valid echo/items, contract-hil-basic, contract-hil-nested-root, contract-parent, contract-complex-root) now declare precise Type.Object/Type.Array schemas or Type.Unsafe<Interface>(...) so ctx.inputs/.run() return/child.outputs are precisely typed and runtime validation enforces the real shape. - Enable noUnusedLocals/noUnusedParameters in the root tsconfig (the repo standard per AGENTS.md) so 'bun run typecheck'/lint/CI match the IDE and this can't drift again, and remove the dead locals/imports it surfaced across workflows, tests, coding-agent, subagents, and mcp. Exhaustiveness checks (const _x: never = ...) are preserved via an explicit reference. - Docs: packages/coding-agent/docs/workflows.md and packages/workflows README now steer authors toward precise TypeBox schemas (with a schema->Static table and a loose-vs-precise example) and document the Type.Unsafe<T> escape hatch for genuinely dynamic data. Assistant-model: Claude Opus 4.8
Summary
Replaces the legacy
{ type, required, default, choices }input/output descriptor with TypeBox-native schemas, removes thezoddependency, provides preciseStatic<>typing forctx.inputs,.run()return values, andctx.workflow(child).outputswith full runtimeValuevalidation, and enablesnoUnusedLocals/noUnusedParametersrepo-wide.Key Changes
Authoring surface
Authors now declare inputs and outputs with TypeBox schemas:
Type.Optional(...)marks an optional field.Type,Static, andTSchemaare re-exported from@bastani/workflowsfor single-import authoring.Static typing
ctx.inputstyped asStatic<TInputSchemaMap>.run()return typed against the declared.output(...)contractctx.workflow(child).outputstyped from the child definition's output contractRuntime validation
Valuewith precise path errorsserializable.tsreimplemented on TypeBox usingType.Cyclicfor the recursive JSON-serializable schema, replacing Zodzodremoved from@bastani/workflowsPicker / UI compatibility preserved
New
schema-introspection.tsderives the legacy normalized{ type, choices, default, required }descriptor from a TypeBox schema —TUnion<TLiteral[]>→select,TBoolean→boolean,default/Type.Optionalhonored. The inputs picker UI, validation, and dispatch are unchanged.Repo-wide TypeScript strictness
noUnusedLocalsandnoUnusedParametersin the roottsconfig.json.atomic/workflows/**/*to the TypeScript project so contract fixtures are type-checkedMigrated surfaces
goal,deep-research-codebase,ralph,open-claude-design) migrated to TypeBox API.atomic/workflows/) and the full test suite migratedpackages/coding-agent/docs/workflows.md,packages/workflows/README.md) and CHANGELOG updatedBreaking Changes
The
{ type, required, default, choices }input/output descriptor is removed. Workflows must declare inputs/outputs with TypeBox schemas..input("prompt", { type: "text", required: true }).input("prompt", Type.String()).input("count", { type: "number", default: 2 }).input("count", Type.Number({ default: 2 })).input("flavor", { type: "select", choices: ["a","b"], default: "a" }).input("flavor", Type.Union([Type.Literal("a"), Type.Literal("b")], { default: "a" }))This feature was unreleased, so there is no migration path needed for shipped consumers.
Quality
bun run typecheck: 0 errorsbun run lint: 0 warningsCLAUDECODE=1 bun run test:unit: 1945 pass / 0 fail