Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Fix missing schema definitions in Output SDK steps. Use when seeing type errors, undefined properties at step boundaries, validation failures, or when step inputs/outputs aren't being properly typed.
.claude/skills/growthxai-output-error-missing-schemas/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-02 | ✗→✓ | ▲ Improved | 31% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 57% | 0% |
| case-10 | ✗→✓ | ▲ Improved | 72% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 94% | 0% |
| case-14 | ✗→✓ | ▲ Improved | 175% | 0% |
This skill helps diagnose and fix issues caused by steps that lack explicit inputSchema or outputSchema definitions. Schemas are essential for type safety, validation, and proper data serialization between steps.
You're seeing:
Steps without explicit schemas:
typescript// WRONG: No input validation export const processData = step( { name: 'processData', // inputSchema: missing! outputSchema: z.object( { result: z.string() } ), fn: async input => { return { result: input.value }; // input.value might be undefined! } } );
typescript// WRONG: No output validation export const fetchData = step( { name: 'fetchData', inputSchema: z.object( { id: z.string() } ), // outputSchema: missing! fn: async input => { return { data: await getFromApi( input.id ) }; // Output shape not validated } } );
typescript// WRONG: No validation at all export const transformData = step( { name: 'transformData', // No schemas! fn: async input => { return transform( input ); } } );
Always define both inputSchema and outputSchema for every step:
typescriptimport { z, step } from '@outputai/core'; export const processData = step( { name: 'processData', inputSchema: z.object( { id: z.string(), value: z.number(), optional: z.string().optional() } ), outputSchema: z.object( { result: z.string(), processedAt: z.number() } ), fn: async input => { // input is fully typed: { id: string, value: number, optional?: string } return { result: `Processed ${input.id}`, processedAt: Date.now() }; // output is validated against outputSchema } } );
typescript// Good: Clear, descriptive schema inputSchema: z.object( { userId: z.string().uuid(), email: z.string().email(), age: z.number().int().positive() } )
typescriptinputSchema: z.object( { required: z.string(), optional: z.string().optional(), withDefault: z.string().default( 'fallback' ) } )
typescript// Define reusable schemas const userSchema = z.object( { id: z.string(), name: z.string() } ); const addressSchema = z.object( { street: z.string(), city: z.string() } ); // Compose in step inputSchema: z.object( { user: userSchema, address: addressSchema } )
typescriptinputSchema: z.object( { items: z.array( z.object( { id: z.string(), quantity: z.number() } ) ), metadata: z.record( z.string() ) } )
Search your codebase:
bash# Find step definitions grep -rn "step({" src/workflows/ # Look for steps without inputSchema grep -A5 "step({" src/workflows/ | grep -B2 "fn:" # Check if schemas are present grep -rn "inputSchema:" src/workflows/ grep -rn "outputSchema:" src/workflows/
Review each step definition to ensure both schemas are present.
typescriptexport const fetchUser = step( { name: 'fetchUser', inputSchema: z.object( { userId: z.string() } ), outputSchema: z.object( { user: z.object( { id: z.string(), name: z.string(), email: z.string() } ).nullable(), // Handle not found found: z.boolean() } ), fn: async input => { const user = await api.getUser( input.userId ); return { user, found: user !== null }; } } );
typescriptexport const transformData = step( { name: 'transformData', inputSchema: z.object( { raw: z.array( z.unknown() ) } ), outputSchema: z.object( { processed: z.array( z.object( { id: z.string(), value: z.number() } ) ), count: z.number() } ), fn: async input => { const processed = input.raw.map( transformItem ); return { processed, count: processed.length }; } } );
For steps that don't return meaningful data:
typescriptexport const logEvent = step( { name: 'logEvent', inputSchema: z.object( { event: z.string(), data: z.record( z.unknown() ) } ), outputSchema: z.object( { logged: z.literal( true ) } ), fn: async input => { await logger.log( input.event, input.data ); return { logged: true }; } } );
After adding schemas:
npm run output:worker:build should pass without type errorsnpx output workflow run <name> --input '<input>' should validate correctlyoutput-error-zod-import| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | pass→pass | 7,795 | 4,470 | -43% | 1 | 1 | 0% | 1,412 | 2,442 | +73% | 0 | 0 | — |
case-02 | fail→pass | 10,291 | 5,126 | -50% | 1 | 1 | 0% | 1,988 | 2,612 | +31% | 0 | 0 | — |
case-03 | pass→pass | 4,275 | 4,361 | +2% | 1 | 1 | 0% | 808 | 2,415 | +199% | 0 | 0 | — |
case-04 | pass→pass | 3,678 | 3,082 | -16% | 1 | 1 | 0% | 726 | 2,187 | +201% | 0 | 0 | — |
case-05 | pass→pass | 10,016 | 4,433 | -56% | 1 | 1 | 0% | 1,598 | 2,401 | +50% | 0 | 0 | — |
case-06 | pass→pass | 5,579 | 3,278 | -41% | 1 | 1 | 0% | 1,019 | 2,212 | +117% | 0 | 0 | — |
case-07 | pass→pass | 7,921 | 4,374 | -45% | 1 | 1 | 0% | 1,343 | 2,460 | +83% | 0 | 0 | — |
case-08 | pass→pass | 8,000 | 4,370 | -45% | 1 | 1 | 0% | 1,409 | 2,419 | +72% | 0 | 0 | — |
case-09 | fail→pass | 7,419 | 1,420 | -81% | 1 | 1 | 0% | 1,155 | 1,817 | +57% | 0 | 0 | — |
case-10 | fail→pass | 6,067 | 1,821 | -70% | 1 | 1 | 0% | 1,111 | 1,906 | +72% | 0 | 0 | — |
case-11 | fail→pass | 6,994 | 2,684 | -62% | 1 | 1 | 0% | 1,080 | 2,096 | +94% | 0 | 0 | — |
case-12 | pass→pass | 10,135 | 4,122 | -59% | 1 | 1 | 0% | 1,660 | 2,395 | +44% | 0 | 0 | — |
case-13 | pass→pass | 3,803 | 3,286 | -14% | 1 | 1 | 0% | 624 | 2,256 | +262% | 0 | 0 | — |
case-14 | fail→pass | 4,924 | 2,923 | -41% | 1 | 1 | 0% | 752 | 2,066 | +175% | 0 | 0 | — |
case-15 | pass→pass | 11,960 | 5,923 | -50% | 1 | 1 | 0% | 2,070 | 2,643 | +28% | 0 | 0 | — |
case-16 | fail→pass | 4,530 | 2,671 | -41% | 1 | 1 | 0% | 770 | 2,029 | +164% | 0 | 0 | — |
case-17 | pass→pass | 2,344 | 2,923 | +25% | 1 | 1 | 0% | 392 | 2,069 | +428% | 0 | 0 | — |
case-18 | pass→pass | 2,943 | 2,134 | -27% | 1 | 1 | 0% | 485 | 1,947 | +301% | 0 | 0 | — |
case-19 | pass→pass | 13,127 | 9,153 | -30% | 1 | 1 | 0% | 1,903 | 2,970 | +56% | 0 | 0 | — |
case-20 | pass→pass | 14,722 | 11,810 | -20% | 1 | 1 | 0% | 2,225 | 3,484 | +57% | 0 | 0 | — |
case-21 | pass→pass | 8,588 | 4,980 | -42% | 1 | 1 | 0% | 1,549 | 2,498 | +61% | 0 | 0 | — |
case-22 | fail→pass | 14,047 | 10,305 | -27% | 1 | 1 | 0% | 2,315 | 3,487 | +51% | 0 | 0 | — |
case-23 | pass→pass | 3,798 | 4,212 | +11% | 1 | 1 | 0% | 616 | 2,340 | +280% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted. The headline lift of +30 percentage points is the difference between those two pass rates over the 23 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.