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:
Option 2: Folder-Based (Large workflows)
For larger workflows with many steps, use a steps/ folder:
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:
Critical Import Patterns
Core Imports
HTTP Client Import
Related Skill: output-error-http-client
LLM Client Import
ES Module Imports
All imports MUST use .js extension:
Basic Structure
Required Properties
name (string)
Unique identifier for the step. Use camelCase.
description (string)
Human-readable description of the step's purpose.
inputSchema (Zod schema)
Schema for validating step input. Define in types.ts and import.
outputSchema (Zod schema)
Schema for validating step output. Define in types.ts and import.
fn (async function)
The step execution function. This is where I/O operations happen.
HTTP Client Usage
Creating an HTTP Client
Making HTTP Requests
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.
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.
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.
Using generateText
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:
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:
ValidationError (Retryable)
Use ValidationError for temporary failures that may succeed on retry:
Related Skill: output-error-try-catch for proper error handling patterns
Complete Example
Based on a real workflow step:
Best Practices
1. One Responsibility Per Step
2. Clear Error Messages
3. Validate Input Early
Verification Checklist
-
step,z,FatalError,ValidationErrorimported from@outputai/core -
createKyClientimported from@outputai/http(not axios) -
generateTextandaiSdkimported 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 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
- 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.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-llm-streaming- Streaming LLM progress with Temporal-safe failuresoutput-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


