Create step functions in steps.ts for Output SDK workflows. Use when implementing I/O operations, error handling, HTTP requests, or LLM calls.
Create step functions in steps.ts for Output SDK workflows. Use when implementing I/O operations, HTTP requests, LLM calls, or error handling with FatalError/ValidationError.
/plugin marketplace add growthxai/output-claude-plugins/plugin install growthxai-outputai-plugins-outputai@growthxai/output-claude-pluginsThis skill is limited to using the following tools:
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.
// 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';
// 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
// CORRECT - Use @output.ai/llm wrapper
import { generateText, generateObject } from '@output.ai/llm';
// WRONG - Never call LLM providers directly
import OpenAI from 'openai';
All imports MUST use .js extension:
// CORRECT
import { InputSchema, OutputSchema } from './types.js';
// WRONG - Missing .js extension
import { InputSchema, OutputSchema } from './types';
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 */ };
}
});
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.
inputSchema: z.object({
content: z.string(),
numberOfIdeas: z.number()
})
Schema for validating step output.
outputSchema: z.array(z.string())
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;
}
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}`
);
}
]
}
});
// 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
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 };
}
});
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
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');
}
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
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;
}
});
// 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) => {
// 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);
// ...
}
step, z, FatalError, ValidationError imported from @output.ai/corehttpClient imported from @output.ai/http (not axios)generateText/generateObject imported from @output.ai/llm (not direct provider).js extensionname, description, inputSchema, outputSchema, fnoutput-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 workflowsThis skill should be used when the user asks to "create an agent", "add an agent", "write a subagent", "agent frontmatter", "when to use description", "agent examples", "agent tools", "agent colors", "autonomous agent", or needs guidance on agent structure, system prompts, triggering conditions, or agent development best practices for Claude Code plugins.
This skill should be used when the user asks to "create a slash command", "add a command", "write a custom command", "define command arguments", "use command frontmatter", "organize commands", "create command with file references", "interactive command", "use AskUserQuestion in command", or needs guidance on slash command structure, YAML frontmatter fields, dynamic arguments, bash execution in commands, user interaction patterns, or command development best practices for Claude Code.
This skill should be used when the user asks to "create a hook", "add a PreToolUse/PostToolUse/Stop hook", "validate tool use", "implement prompt-based hooks", "use ${CLAUDE_PLUGIN_ROOT}", "set up event-driven automation", "block dangerous commands", or mentions hook events (PreToolUse, PostToolUse, Stop, SubagentStop, SessionStart, SessionEnd, UserPromptSubmit, PreCompact, Notification). Provides comprehensive guidance for creating and implementing Claude Code plugin hooks with focus on advanced prompt-based hooks API.