Agent skill
output-dev-step-function
Create step functions in steps.ts for Output SDK workflows. Use when implementing I/O operations, error handling, HTTP requests, or LLM calls.
Install this agent skill to your Project
npx add-skill https://github.com/majiayu000/claude-skill-registry/tree/main/skills/development/output-dev-step-function-growthxai-output-claude-plugin
SKILL.md
Creating Step Functions
Overview
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.
When to Use This Skill
- Implementing I/O operations for a workflow
- Adding HTTP client integrations
- Implementing LLM-powered steps
- Handling errors with FatalError and ValidationError
- Creating reusable step components
Critical Import Patterns
Core Imports
// CORRECT - Import from @output.ai/core
import { step, z, FatalError, ValidationError } from '@output.ai/core';
// WRONG - Never import z from zod
import { z } from 'zod';
HTTP Client Import
// CORRECT - Use @output.ai/http wrapper
import { httpClient } from '@output.ai/http';
// WRONG - Never use axios directly
import axios from 'axios';
Related Skill: output-error-http-client
LLM Client Import
// CORRECT - Use @output.ai/llm wrapper
import { generateText, generateObject } from '@output.ai/llm';
// WRONG - Never call LLM providers directly
import OpenAI from 'openai';
ES Module Imports
All imports MUST use .js extension:
// CORRECT
import { InputSchema, OutputSchema } from './types.js';
// WRONG - Missing .js extension
import { InputSchema, OutputSchema } from './types';
Basic Structure
import { step, z, FatalError, ValidationError } from '@output.ai/core';
import { httpClient } from '@output.ai/http';
import { generateObject } from '@output.ai/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 */ };
}
});
Required Properties
name (string)
Unique identifier for the step. Use camelCase.
name: 'generateImageIdeas'
description (string)
Human-readable description of the step's purpose.
description: 'Generate creative infographic prompt ideas using Claude'
inputSchema (Zod schema)
Schema for validating step input.
inputSchema: z.object({
content: z.string(),
numberOfIdeas: z.number()
})
outputSchema (Zod schema)
Schema for validating step output.
outputSchema: z.array(z.string())
fn (async function)
The step execution function. This is where I/O operations happen.
fn: async (input) => {
// I/O operations allowed here
const result = await someExternalService(input);
return result;
}
HTTP Client Usage
Creating an HTTP Client
import { httpClient } from '@output.ai/http';
import { FatalError, ValidationError } from '@output.ai/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}`
);
}
]
}
});
Making HTTP Requests
// 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
LLM Operations
Using generateObject
import { generateObject } from '@output.ai/llm';
export const analyzeContent = step({
name: 'analyzeContent',
description: 'Analyze content using Claude',
inputSchema: z.object({ content: z.string() }),
outputSchema: z.object({ analysis: z.string() }),
fn: async ({ content }) => {
const response = await generateObject({
prompt: 'analyzeContent@v1', // References prompts/analyzeContent@v1.prompt
variables: {
content
},
schema: z.object({
analysis: z.string()
})
});
return { analysis: response.analysis };
}
});
Using generateText
import { generateText } from '@output.ai/llm';
export const generateSummary = step({
name: 'generateSummary',
description: 'Generate a text summary',
inputSchema: z.object({ content: z.string() }),
outputSchema: z.object({ summary: z.string() }),
fn: async ({ content }) => {
const response = await generateText({
prompt: 'summarize@v1',
variables: { content }
});
return { summary: response };
}
});
Related Skill: output-dev-prompt-file for creating prompt files
Error Handling
FatalError (Non-Retryable)
Use FatalError for permanent failures that should not be retried:
import { FatalError } from '@output.ai/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');
}
ValidationError (Retryable)
Use ValidationError for temporary failures that may succeed on retry:
import { ValidationError } from '@output.ai/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
Complete Example
Based on a real workflow step:
import { step, z, FatalError, ValidationError } from '@output.ai/core';
import { httpClient } from '@output.ai/http';
import { generateObject } from '@output.ai/llm';
import { GeminiImageService } from '#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 response = await generateObject({
prompt: 'generateImageIdeas@v1',
variables: {
content,
numberOfIdeas,
colorPalette: colorPalette || '',
artDirection: artDirection || ''
},
schema: ImageIdeasSchema
});
return response.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;
}
});
Best Practices
1. One Responsibility Per Step
// 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',
// ...
});
2. Clear Error Messages
// Good - specific error message
throw new FatalError(`Invalid API key for service: ${serviceName}`);
// Avoid - generic error message
throw new FatalError('Error occurred');
3. Validate Input Early
fn: async (input) => {
// Validate early
if (!input.url.startsWith('https://')) {
throw new FatalError('URL must use HTTPS protocol');
}
// Then proceed with operation
const response = await httpClientInstance.get(input.url);
// ...
}
Verification Checklist
-
step,z,FatalError,ValidationErrorimported from@output.ai/core -
httpClientimported from@output.ai/http(not axios) -
generateText/generateObjectimported from@output.ai/llm(not direct provider) - All imports use
.jsextension - Named exports used for each step
- Each step has
name,description,inputSchema,outputSchema,fn - FatalError used for non-retryable failures
- ValidationError used for retryable failures
- No bare try-catch blocks that swallow errors
Related Skills
output-dev-workflow-function- Orchestrating steps in workflow.tsoutput-dev-types-file- Defining step input/output schemasoutput-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 workflows
Didn't find tool you were looking for?