Output Dev Step Function

作者 growthxaia4f6bd40ab0e无许可证440 个星标收录于 2026年10月8日更新于 2026年10月8日仓库今天更新

Create step functions in steps.ts for Output SDK workflows. Use when implementing I/O operations, error handling, HTTP requests, or LLM calls.

AI 生成的概览

说明如何在 Output SDK 工作流的 steps.ts 中编写步骤函数,涵盖 I/O、HTTP、LLM 调用与错误处理。

功能
该技能为 Output SDK 工作流中的 steps.ts 步骤函数编写提供指导。内容涵盖文件组织、允许的导入、步骤必需属性、HTTP 客户端用法、带结构化输出的 LLM 调用,以及使用 FatalError 和 ValidationError 的错误处理。还包含验证清单和一个完整的多步骤示例。
适用场景
在 Output SDK 工作流中实现 I/O 操作、HTTP 集成、LLM 步骤或错误处理时使用。也适用于创建可复用步骤组件,或检查步骤代码是否符合 SDK 的导入与结构规则。
运行要求
仅为说明文档,不附带脚本。假定已有 Output SDK 项目,包含 @outputai/core、@outputai/http、@outputai/llm 和 @outputai/credentials 包,以及基于 Temporal 的工作流。

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:

typescript
// WRONG - steps cannot import other stepsimport { otherStep } from '../../shared/steps/other.js'; // ✗import { anotherStep } from './other_steps.js'; // ✗

Critical Import Patterns

Core Imports

typescript
// CORRECT - Import from @outputai/coreimport { step, z, FatalError, ValidationError } from '@outputai/core';
// WRONG - Never import z from zodimport { z } from 'zod';

HTTP Client Import

typescript
// CORRECT - Use @outputai/http wrapperimport { createKyClient } from '@outputai/http';
// WRONG - Never use axios directlyimport axios from 'axios';

Related Skill: output-error-http-client

LLM Client Import

typescript
// CORRECT - Use @outputai/llm wrapperimport { generateText, aiSdk } from '@outputai/llm';
// WRONG - Never call LLM providers directlyimport OpenAI from 'openai';

ES Module Imports

All imports MUST use .js extension:

typescript
// CORRECTimport { InputSchema, OutputSchema } from './types.js';import { GeminiService } from '../../shared/clients/gemini_client.js';
// WRONG - Missing .js extensionimport { InputSchema, OutputSchema } from './types';

Basic Structure

typescript
import { step, z, FatalError, ValidationError } from '@outputai/core';import { createKyClient } from '@outputai/http';import { generateText, aiSdk } 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.

typescript
name: 'generateImageIdeas'

description (string)

Human-readable description of the step's purpose.

typescript
description: 'Generate creative infographic prompt ideas using Claude'

inputSchema (Zod schema)

Schema for validating step input. Define in types.ts and import.

typescript
inputSchema: z.object( {  content: z.string(),  numberOfIdeas: z.number()} )

outputSchema (Zod schema)

Schema for validating step output. Define in types.ts and import.

typescript
outputSchema: z.array( z.string() )

fn (async function)

The step execution function. This is where I/O operations happen.

typescript
fn: async input => {  const result = await someExternalService( input );  return result;}

HTTP Client Usage

Creating an HTTP Client

typescript
import { createKyClient } 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 client = createKyClient( {  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

typescript
// GET requestconst response = await client.get( 'https://api.example.com/data' );const data = await response.json();
// POST request with JSON bodyconst response = await client.post( 'https://api.example.com/submit', {  json: { field: 'value' }} );
// HEAD request (check URL accessibility)const response = await client.head( url );const contentType = response.headers.get( 'content-type' );

When a non-HEAD request only uses response metadata, such as response.url, response.status, or headers, cancel the unused body in a finally block. Responses read with .json(), .text(), etc. are already consumed.

typescript
const response = await client.get( url );
try {  return response.url;} finally {  await response.body?.cancel();}

Related Skill: output-dev-http-client-create for creating shared clients

LLM Operations

Important: Define LLM Schemas in types.ts

Schemas used in aiSdk.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.

typescript
// WRONG - inline schema in aiSdk.Output.object()output: aiSdk.Output.object( {  schema: z.object( {    analysis: z.string()  } )} )
// CORRECT - import from types.tsimport { AnalysisLlmSchema } from './types.js';// ...output: aiSdk.Output.object( {  schema: AnalysisLlmSchema} )

Using generateText with aiSdk.Output.object()

The variables field accepts scalars, nested objects, and arrays. Use Liquid loops and dot notation in the prompt when it owns presentation; pre-format in the step when the exact rendered text is application logic.

generateText arguments: prompt, promptDir, variables, tools, output, toolChoice, stopWhen, abortSignal.

typescript
import { generateText, aiSdk } 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: aiSdk.Output.object( {        schema: AnalysisLlmSchema      } )    } );
    return { analysis: output.analysis };  }} );

Using generateText

typescript
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

Streaming LLM Progress

Prefer generateTextWithStreaming() in steps when the caller needs progress callbacks and a complete result:

typescript
import { generateTextWithStreaming } from '@outputai/llm';
const reportProgress = ( chunk: string ) => process.stdout.write( chunk );
const result = await generateTextWithStreaming( {  prompt: 'summarize@v1',  variables: { content },  onChunk( { chunk } ) {    if ( chunk.type === 'text-delta' ) {      reportProgress( chunk.text );    }  }} );
return result.result;

The returned promise rejects on stream failures, allowing Temporal to retry the activity. Use streamText() only when direct stream access is required; capture onError and throw the captured error after consumption. See output-dev-llm-streaming for complete patterns.

Error Handling

FatalError (Non-Retryable)

Use FatalError for permanent failures that should not be retried:

typescript
import { FatalError } from '@outputai/core';import { credentials } from '@outputai/credentials';
// Authentication failuresif ( response.status === 401 ) {  throw new FatalError( 'Invalid API key' );}
// Invalid input that cannot be fixed by retryif ( !input.requiredField ) {  throw new FatalError( 'Missing required field: requiredField' );}
// Resource not foundif ( response.status === 404 ) {  throw new FatalError( `Resource not found: ${resourceId}` );}
// Configuration errorsif ( !credentials.get( 'service.api_key' ) ) {  throw new FatalError( 'service.api_key credential not set' );}

ValidationError (Retryable)

Use ValidationError for temporary failures that may succeed on retry:

typescript
import { ValidationError } from '@outputai/core';
// Rate limitingif ( response.status === 429 ) {  throw new ValidationError( 'Rate limit exceeded, will retry' );}
// Temporary service unavailabilityif ( response.status === 503 ) {  throw new ValidationError( 'Service temporarily unavailable' );}
// Network errorstry {  const response = await client.get( url );} catch ( error ) {  throw new ValidationError( `Network error: ${error.message}` );}
// Empty response that might be temporaryif ( 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:

typescript
import { step, z, FatalError, ValidationError } from '@outputai/core';import { createKyClient } from '@outputai/http';import { generateText, aiSdk } 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 client = createKyClient( {  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 LLMexport 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: aiSdk.Output.object( {        schema: ImageIdeasSchema      } )    } );
    return output.ideas;  }} );
// Step 2: Generate Images using external APIexport 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 clientexport 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 client.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

typescript
// Good - focused stepexport const fetchUserData = step( {  name: 'fetchUserData',  description: 'Fetch user data from the API'  // ...} );
// Avoid - step doing too muchexport const fetchAndProcessAndSaveUserData = step( {  name: 'fetchAndProcessAndSaveUserData'  // ...} );

2. Clear Error Messages

typescript
// Good - specific error messagethrow new FatalError( `Invalid API key for service: ${serviceName}` );
// Avoid - generic error messagethrow new FatalError( 'Error occurred' );

3. Validate Input Early

typescript
fn: async input => {  if ( !input.url.startsWith( 'https://' ) ) {    throw new FatalError( 'URL must use HTTPS protocol' );  }
  const response = await client.get( input.url );  // ...}

Verification Checklist

  • step, z, FatalError, ValidationError imported from @outputai/core
  • createKyClient imported from @outputai/http (not axios)
  • generateText and aiSdk imported from @outputai/llm (not direct provider)
  • Structured output uses aiSdk.Output.object() with .describe() (not .min()/.max()/.length()) on number and array schemas
  • Schemas for aiSdk.Output.object() are defined in types.ts and imported, not inline
  • All imports use .js extension
  • 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
  • Non-HEAD HTTP responses are consumed or cancelled when only metadata is used
  • 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.ts
  • output-dev-evaluator-function - Using steps in evaluator functions
  • output-dev-types-file - Defining step input/output schemas
  • output-dev-code-style - Code formatting and style conventions
  • output-dev-http-client-create - Creating shared HTTP clients
  • output-dev-llm-streaming - Streaming LLM progress with Temporal-safe failures
  • output-dev-prompt-file - Creating prompt files for LLM operations
  • output-error-try-catch - Proper error handling patterns
  • output-error-direct-io - Avoiding direct I/O in workflows

来源与署名

来源:growthxai/output位于coding_assistants/claude/plugins/outputai/skills/output-dev-step-function提交a4f6bd4

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架