Workflows orchestrate, but they can’t do any real work themselves — no API calls, no database queries, no LLM requests. That’s by design. Output replays workflow code to recover from failures, so if you made an API call directly in a workflow, it could run twice. Steps are the boundary: they run once, their results are cached, and if something fails, only the failed step retries.
Here’s a step that looks up a company using an external API:
steps.ts
Copy
Ask AI
import { step } from '@outputai/core';import { lookupCompany as fetchFromClearbit } from '../../../clients/clearbit.js';import { LookupCompanyInput, LookupCompanyOutput } from './types.js';export const lookupCompany = step({ name: 'lookupCompany', description: 'Look up company data from Clearbit', inputSchema: LookupCompanyInput, outputSchema: LookupCompanyOutput, fn: async (domain) => { const company = await fetchFromClearbit(domain); return { name: company.name, industry: company.industry, size: company.employees }; }});// types.ts// import { z } from '@outputai/core';//// export const LookupCompanyInput = z.string();//// export const LookupCompanyOutput = z.object({// name: z.string(),// industry: z.string(),// size: z.number()// });
Steps are called from workflows like regular async functions — await lookupCompany('acme.com'). The workflow decides when to call each step; the step does the actual work.
Steps accept an optional options property to configure retry behavior and timeouts. Use options.activityOptions for Temporal activity options. Options set on a workflow apply to all its steps; a step can override them with its own options.activityOptions:
steps.ts
Copy
Ask AI
import { step } from '@outputai/core';import { searchNews as fetchFromTavily } from '../../../clients/tavily.js';import { SearchNewsInput, SearchNewsOutput } from './types.js';export const searchNews = step({ name: 'searchNews', description: 'Search for recent news about a company', inputSchema: SearchNewsInput, outputSchema: SearchNewsOutput, options: { activityOptions: { retry: { maximumAttempts: 5, initialInterval: '1s', backoffCoefficient: 2 } } }, fn: async (query) => { const articles = await fetchFromTavily(query); return articles; }});// types.ts// import { z } from '@outputai/core';//// export const SearchNewsInput = z.string();//// export const SearchNewsOutput = z.array(z.object({// title: z.string(),// url: z.string(),// snippet: z.string()// }));
For the full list of supported options and default values, see Activity Options in the Workflows guide. Evaluators use the same options.
Any file that is not a component file (steps, evaluators, or workflow)
Steps cannot import:
Other steps (activity calling activity)
Evaluators (they are also activities)
Workflow files
This isolation exists because of how Temporal executes activities. Each step runs independently, and Temporal needs to be able to retry or replay any step without side effects from other steps.