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

- Skill: `majiayu000/output-dev-step-function-2` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add majiayu000/output-dev-step-function-2`
- Raw SKILL.md: https://api.skillmd.com/api/skills/majiayu000/output-dev-step-function-2/raw
- Safety review: pending (external: skill-scanner PASS, skillspector PASS)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: AI & ML
- Author: majiayu000 (https://skillmd.com/u/majiayu000)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/majiayu000/output-dev-step-function-2

---


# 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

