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.

Stars 163
Forks 31

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

typescript
// 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

typescript
// 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

typescript
// 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:

typescript
// CORRECT
import { InputSchema, OutputSchema } from './types.js';

// WRONG - Missing .js extension
import { InputSchema, OutputSchema } from './types';

Basic Structure

typescript
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.

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.

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

outputSchema (Zod schema)

Schema for validating step output.

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

fn (async function)

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

typescript
fn: async (input) => {
  // I/O operations allowed here
  const result = await someExternalService(input);
  return result;
}

HTTP Client Usage

Creating an HTTP Client

typescript
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

typescript
// 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

typescript
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

typescript
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:

typescript
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:

typescript
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:

typescript
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

typescript
// 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

typescript
// 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

typescript
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, ValidationError imported from @output.ai/core
  • httpClient imported from @output.ai/http (not axios)
  • generateText/generateObject imported from @output.ai/llm (not direct provider)
  • 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
  • No bare try-catch blocks that swallow errors

Related Skills

  • output-dev-workflow-function - Orchestrating steps in workflow.ts
  • output-dev-types-file - Defining step input/output schemas
  • output-dev-http-client-create - Creating shared HTTP clients
  • 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

Didn't find tool you were looking for?

Be as detailed as possible for better results