🔄output-dev-step-function
- プラグイン
- outputai
- ソース
- GitHub で見る ↗
説明
`steps.ts` に Output SDK ワークフロー用のステップ関数を作成します。 次のような場合に使用: I/O 操作、エラーハンドリング、HTTP リクエスト、または LLM 呼び出しを実装する場合。
原文を表示
Create step functions in steps.ts for Output SDK workflows. Use when implementing I/O operations, error handling, HTTP requests, or LLM calls.
ユースケース
- ✓I/O 操作を実装するとき
- ✓エラーハンドリングを実装するとき
- ✓HTTP リクエストを実装するとき
- ✓LLM 呼び出しを実装するとき
本文
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
File Organization
Option 1: Flat File (Default)
For smaller workflows, use a single steps.ts file:
src/workflows/{workflow-name}/
├── workflow.ts
├── steps.ts # All steps in one file
├── types.ts
└── ...
Option 2: Folder-Based (Large workflows)
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
└── ...
Component Location Rules
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)
Activity Isolation Constraints
Steps are Temporal activities with strict import rules to ensure deterministic replay.
Steps CAN import from:
- Local workflow files:
./utils.js,./types.js,./helpers.js - Local subdirectories:
./clients/pokeapi.js,./lib/helpers.js - Shared utilities:
../../shared/utils/*.js - Shared clients:
../../shared/clients/*.js - Shared services:
../../shared/services/*.js
Steps CANNOT import:
- Other step files (even shared steps - workflows import those)
- Evaluator files
- Workflow files
Example of WRONG imports:
// WRONG - steps cannot import other steps
import { otherStep } from '../../shared/steps/other.js'; // ✗
import { anotherStep } from './other_steps.js'; // ✗
Critical Import Patterns
Core Imports
// CORRECT - Import from @outputai/core
import { step, z, FatalError, ValidationError } from '@outputai/core';
// WRONG - Never import z from zod
import { z } from 'zod';
HTTP Client Import
// 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
LLM Client Import
// CORRECT - Use @outputai/llm wrapper
import { generateText, Output } from '@outputai/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';
import { GeminiService } from '../../shared/clients/gemini_client.js';
// WRONG - Missing .js extension
import { InputSchema, OutputSchema } from './types';
Basic Structure
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 */ };
}
} );
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. Define in types.ts and import.
inputSchema: z.object( {
content: z.string(),
numberOfIdeas: z.number()
} )
outputSchema (Zod schema)
Schema for validating step output. Define in types.ts and import.
outputSchema: z.array( z.string() )
fn (async function)
The step execution function. This is where I/O operations happen.
fn: async input => {
const result = await someExternalService( input );
return result;
}
HTTP Client Usage
Creating an HTTP Client
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}`
);
}
]
}
} );
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
Important: Define LLM Schemas in types.ts
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
} )
Using generateText with Output.object()
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 };
}
} );
Using generateText
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
Error Handling
FatalError (Non-Retryable)
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' );
}
ValidationError (Retryable)
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
Complete Example
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;
}
} );
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 => {
if ( !input.url.startsWith( 'https://' ) ) {
throw new FatalError( 'URL must use HTTPS protocol' );
}
const response = await httpClientInstance.get( input.url );
// ...
}
Verification Checklist
- [ ]
step,z,FatalError,ValidationErrorimported from@outputai/core - [ ]
httpClientimported from@outputai/http(not axios) - [ ]
generateTextandOutputimported from@outputai/llm(not direct provider) - [ ] Structured output uses
Output.object()with.describe()(not.min()/.max()/.length()) on number and array schemas - [ ] Schemas for
Output.object()are defined intypes.tsand imported, not inline - [ ] 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
- [ ] Steps only import allowed dependencies (local files, shared code)
- [ ] No imports of other steps, evaluators, or workflows
- [ ] Code follows style conventions (see
output-dev-code-style)
Related Skills
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 workflows
原文・著作権は Anthropic および各プラグイン作者に帰属します。日本語訳は Claude API による自動翻訳です。