From outputai
Create step functions in steps.ts for Output SDK workflows. Use when implementing I/O operations, error handling, HTTP requests, or LLM calls.
How this skill is triggered — by the user, by Claude, or both
Slash command
/outputai:output-dev-step-functionThis skill is limited to the following tools:
The summary Claude sees in its skill listing — used to decide when to auto-load this skill
This skill documents how to create step functions in `steps.ts` for Output SDK workflows. Steps are where all I/O operations happen - HTTP requests, LLM calls, database operations, file system access, etc.
This skill documents how to create step functions in steps.ts for Output SDK workflows. Steps are where all I/O operations happen - HTTP requests, LLM calls, database operations, file system access, etc.
For smaller workflows, use a single steps.ts file:
src/workflows/{workflow-name}/
├── workflow.ts
├── steps.ts # All steps in one file
├── types.ts
└── ...
For larger workflows with many steps, use a steps/ folder:
src/workflows/{workflow-name}/
├── workflow.ts
├── steps/ # Steps split into individual files
│ ├── fetch_data.ts
│ ├── process.ts
│ └── validate.ts
├── types.ts
└── ...
Important: step() calls MUST be in files containing 'steps' in the path:
src/workflows/my_workflow/steps.ts ✓src/workflows/my_workflow/steps/fetch_data.ts ✓src/shared/steps/common_steps.ts ✓src/workflows/my_workflow/helpers.ts ✗ (cannot contain step() calls)Steps are Temporal activities with strict import rules to ensure deterministic replay.
./utils.js, ./types.js, ./helpers.js./clients/pokeapi.js, ./lib/helpers.js../../shared/utils/*.js../../shared/clients/*.js../../shared/services/*.jsExample of WRONG imports:
// WRONG - steps cannot import other steps
import { otherStep } from '../../shared/steps/other.js'; // ✗
import { anotherStep } from './other_steps.js'; // ✗
// CORRECT - Import from @outputai/core
import { step, z, FatalError, ValidationError } from '@outputai/core';
// WRONG - Never import z from zod
import { z } from 'zod';
// CORRECT - Use @outputai/http wrapper
import { httpClient } from '@outputai/http';
// WRONG - Never use axios directly
import axios from 'axios';
Related Skill: output-error-http-client
// CORRECT - Use @outputai/llm wrapper
import { generateText, Output } from '@outputai/llm';
// WRONG - Never call LLM providers directly
import OpenAI from 'openai';
All imports MUST use .js extension:
// CORRECT
import { InputSchema, OutputSchema } from './types.js';
import { GeminiService } from '../../shared/clients/gemini_client.js';
// WRONG - Missing .js extension
import { InputSchema, OutputSchema } from './types';
import { step, z, FatalError, ValidationError } from '@outputai/core';
import { httpClient } from '@outputai/http';
import { generateText, Output } from '@outputai/llm';
import { StepInputSchema, StepOutputSchema } from './types.js';
export const myStep = step( {
name: 'myStep',
description: 'Description of what this step does',
inputSchema: StepInputSchema,
outputSchema: StepOutputSchema,
fn: async input => {
// Implementation with I/O operations
return { /* output matching outputSchema */ };
}
} );
Unique identifier for the step. Use camelCase.
name: 'generateImageIdeas'
Human-readable description of the step's purpose.
description: 'Generate creative infographic prompt ideas using Claude'
Schema for validating step input. Define in types.ts and import.
inputSchema: z.object( {
content: z.string(),
numberOfIdeas: z.number()
} )
Schema for validating step output. Define in types.ts and import.
outputSchema: z.array( z.string() )
The step execution function. This is where I/O operations happen.
fn: async input => {
const result = await someExternalService( input );
return result;
}
import { httpClient } from '@outputai/http';
import { FatalError, ValidationError } from '@outputai/core';
const RETRY_STATUS_CODES = [ 408, 429, 500, 502, 503, 504 ];
const FATAL_STATUS_CODES = [ 401, 403, 404 ];
const httpClientInstance = httpClient( {
timeout: 30000,
retry: {
limit: 3,
statusCodes: RETRY_STATUS_CODES
},
hooks: {
beforeError: [
error => {
const status = error.response?.status;
const message = error.message;
if ( status && FATAL_STATUS_CODES.includes( status ) ) {
throw new FatalError(
`HTTP ${status} error: ${message}. This is a permanent error.`
);
}
throw new ValidationError(
`HTTP request failed: ${message}`
);
}
]
}
} );
// GET request
const response = await httpClientInstance.get( 'https://api.example.com/data' );
const data = await response.json();
// POST request with JSON body
const response = await httpClientInstance.post( 'https://api.example.com/submit', {
json: { field: 'value' }
} );
// HEAD request (check URL accessibility)
const response = await httpClientInstance.head( url );
const contentType = response.headers.get( 'content-type' );
Related Skill: output-dev-http-client-create for creating shared clients
Schemas used in Output.object() must be defined in types.ts and imported -- never defined inline in step functions. Inline schemas lead to duplication, drift between the step's outputSchema and the LLM schema, and make it harder to maintain types.
// WRONG - inline schema in Output.object()
output: Output.object( {
schema: z.object( {
analysis: z.string()
} )
} )
// CORRECT - import from types.ts
import { AnalysisLlmSchema } from './types.js';
// ...
output: Output.object( {
schema: AnalysisLlmSchema
} )
Important: The variables field only accepts string | number | boolean values. Arrays and objects must be pre-formatted into strings in the step before passing. See output-dev-prompt-file for the full constraint and examples.
import { generateText, Output } from '@outputai/llm';
import {
AnalyzeContentInputSchema,
AnalyzeContentOutputSchema,
AnalysisLlmSchema
} from './types.js';
export const analyzeContent = step( {
name: 'analyzeContent',
description: 'Analyze content using Claude',
inputSchema: AnalyzeContentInputSchema,
outputSchema: AnalyzeContentOutputSchema,
fn: async ( { content } ) => {
const { output } = await generateText( {
prompt: 'analyzeContent@v1',
variables: {
content
},
output: Output.object( {
schema: AnalysisLlmSchema
} )
} );
return { analysis: output.analysis };
}
} );
import { generateText } from '@outputai/llm';
import { SummarizeInputSchema, SummarizeOutputSchema } from './types.js';
export const generateSummary = step( {
name: 'generateSummary',
description: 'Generate a text summary',
inputSchema: SummarizeInputSchema,
outputSchema: SummarizeOutputSchema,
fn: async ( { content } ) => {
const { result } = await generateText( {
prompt: 'summarize@v1',
variables: { content }
} );
return { summary: result };
}
} );
Related Skill: output-dev-prompt-file for creating prompt files
Use FatalError for permanent failures that should not be retried:
import { FatalError } from '@outputai/core';
// Authentication failures
if ( response.status === 401 ) {
throw new FatalError( 'Invalid API key' );
}
// Invalid input that cannot be fixed by retry
if ( !input.requiredField ) {
throw new FatalError( 'Missing required field: requiredField' );
}
// Resource not found
if ( response.status === 404 ) {
throw new FatalError( `Resource not found: ${resourceId}` );
}
// Configuration errors
if ( !process.env.API_KEY ) {
throw new FatalError( 'API_KEY environment variable not set' );
}
Use ValidationError for temporary failures that may succeed on retry:
import { ValidationError } from '@outputai/core';
// Rate limiting
if ( response.status === 429 ) {
throw new ValidationError( 'Rate limit exceeded, will retry' );
}
// Temporary service unavailability
if ( response.status === 503 ) {
throw new ValidationError( 'Service temporarily unavailable' );
}
// Network errors
try {
const response = await httpClientInstance.get( url );
} catch ( error ) {
throw new ValidationError( `Network error: ${error.message}` );
}
// Empty response that might be temporary
if ( results.length === 0 ) {
throw new ValidationError( 'No results returned, will retry' );
}
Related Skill: output-error-try-catch for proper error handling patterns
Based on a real workflow step:
import { step, z, FatalError, ValidationError } from '@outputai/core';
import { httpClient } from '@outputai/http';
import { generateText, Output } from '@outputai/llm';
import { GeminiImageService } from '../../shared/clients/gemini_client.js';
import {
GenerateImageIdeasInputSchema,
GenerateImagesInputSchema,
ImageIdeasSchema
} from './types.js';
const RETRY_STATUS_CODES = [ 408, 429, 500, 502, 503, 504 ];
const FATAL_STATUS_CODES = [ 401, 403, 404 ];
const httpClientInstance = httpClient( {
timeout: 30000,
retry: {
limit: 3,
statusCodes: RETRY_STATUS_CODES
},
hooks: {
beforeError: [
error => {
const status = error.response?.status;
const message = error.message;
if ( status && FATAL_STATUS_CODES.includes( status ) ) {
throw new FatalError( `HTTP ${status} error: ${message}` );
}
throw new ValidationError( `HTTP request failed: ${message}` );
}
]
}
} );
// Step 1: Generate Ideas using LLM
export const generateImageIdeas = step( {
name: 'generateImageIdeas',
description: 'Generate creative infographic prompt ideas using Claude',
inputSchema: GenerateImageIdeasInputSchema,
outputSchema: z.array( z.string() ),
fn: async ( { content, numberOfIdeas, colorPalette, artDirection } ) => {
const { output } = await generateText( {
prompt: 'generateImageIdeas@v1',
variables: {
content,
numberOfIdeas,
colorPalette: colorPalette || '',
artDirection: artDirection || ''
},
output: Output.object( {
schema: ImageIdeasSchema
} )
} );
return output.ideas;
}
} );
// Step 2: Generate Images using external API
export const generateImages = step( {
name: 'generateImages',
description: 'Generate images using Gemini API',
inputSchema: GenerateImagesInputSchema,
outputSchema: z.array( z.string() ),
fn: async ( { input, prompt } ) => {
const geminiImageService = new GeminiImageService();
const generatedImages = await geminiImageService.generateImage( {
prompt,
aspectRatio: input.aspectRatio,
resolution: input.resolution,
numberOfImages: input.numberOfGenerations
} );
if ( generatedImages.length === 0 ) {
throw new ValidationError( 'No images were generated by Gemini' );
}
return generatedImages;
}
} );
// Step 3: Validate URLs using HTTP client
export const validateReferenceImages = step( {
name: 'validateReferenceImages',
description: 'Validates that all provided reference image URLs are accessible',
inputSchema: z.object( {
referenceImageUrls: z.array( z.string() ).optional()
} ),
outputSchema: z.boolean(),
fn: async ( { referenceImageUrls } ) => {
if ( !referenceImageUrls || referenceImageUrls.length === 0 ) {
return true;
}
for ( const [ index, url ] of referenceImageUrls.entries() ) {
const response = await httpClientInstance.head( url );
const contentType = response.headers.get( 'content-type' );
if ( contentType && !contentType.startsWith( 'image/' ) ) {
throw new FatalError(
`Reference URL ${index + 1} (${url}) is not an image file`
);
}
}
return true;
}
} );
// Good - focused step
export const fetchUserData = step( {
name: 'fetchUserData',
description: 'Fetch user data from the API'
// ...
} );
// Avoid - step doing too much
export const fetchAndProcessAndSaveUserData = step( {
name: 'fetchAndProcessAndSaveUserData'
// ...
} );
// Good - specific error message
throw new FatalError( `Invalid API key for service: ${serviceName}` );
// Avoid - generic error message
throw new FatalError( 'Error occurred' );
fn: async input => {
if ( !input.url.startsWith( 'https://' ) ) {
throw new FatalError( 'URL must use HTTPS protocol' );
}
const response = await httpClientInstance.get( input.url );
// ...
}
step, z, FatalError, ValidationError imported from @outputai/corehttpClient imported from @outputai/http (not axios)generateText and Output imported from @outputai/llm (not direct provider)Output.object() with .describe() (not .min()/.max()/.length()) on number and array schemasOutput.object() are defined in types.ts and imported, not inline.js extensionname, description, inputSchema, outputSchema, fnoutput-dev-code-style)output-dev-workflow-function - Orchestrating steps in workflow.tsoutput-dev-evaluator-function - Using steps in evaluator functionsoutput-dev-types-file - Defining step input/output schemasoutput-dev-code-style - Code formatting and style conventionsoutput-dev-http-client-create - Creating shared HTTP clientsoutput-dev-prompt-file - Creating prompt files for LLM operationsoutput-error-try-catch - Proper error handling patternsoutput-error-direct-io - Avoiding direct I/O in workflowsnpx claudepluginhub growthxai/output --plugin outputaiProvides comprehensive project context for the Output.ai framework: durable LLM workflows, Temporal orchestration, project structure, and available agents/commands.
Provides expert guidance for Vercel Workflow DevKit when building durable workflows, long-running tasks, API routes, or agents needing pause/resume, retries, step execution, or crash-safe orchestration.
Creates bite-sized, testable implementation plans from specs or requirements, with file structure and task decomposition. Activates before coding multi-step tasks.