A workflow step is a TypeScript function with the 'use step' directive. Here's a complete example:
import { FatalError } from 'workflow';
// 1. Define parameter interface
interface MyCustomStepParams {
requiredParam: string;
optionalParam?: number;
}
/**
* 2. Add JSDoc documentation
*
* Describe what your step does, its parameters,
* and what it returns.
*/
export async function myCustomStep({
requiredParam,
optionalParam = 10,
}: MyCustomStepParams) {
'use step'; // 3. Add the directive
// 4. Validate inputs
if (!requiredParam) {
throw new FatalError('requiredParam is required');
}
// 5. Validate environment variables
const apiKey = process.env.MY_API_KEY;
if (!apiKey) {
throw new FatalError('MY_API_KEY not configured');
}
// 6. Implement your logic
try {
const response = await fetch('https://api.example.com', {
headers: { Authorization: `Bearer ${apiKey}` },
});
if (!response.ok) {
// 7. Handle non-retryable errors
if (response.status === 401) {
throw new FatalError('Invalid API key');
}
// 8. Let transient errors retry
throw new Error(`API error: ${response.status}`);
}
const data = await response.json();
// 9. Return the result
return {
success: true,
data,
};
} catch (error: any) {
if (error instanceof FatalError) {
throw error;
}
// Network errors will retry automatically
throw error;
}
}Throw FatalError when retrying won't help:
Regular errors will automatically retry. Use them for:
Check all required parameters and environment variables at the start of your function. This prevents wasting time on doomed operations.
Always define interfaces for parameters and return types. This provides excellent IDE support and catches errors at compile time.
Use JSDoc comments to explain what your step does, what parameters it accepts, and what it returns. Include examples in your documentation.
Each step should do one thing well. If you're doing multiple operations, consider breaking them into separate steps that can be composed in a workflow.
Test your step in a simple workflow:
import { myCustomStep } from '../steps/my-custom-step';
export async function testMyStep() {
'use workflow';
const result = await myCustomStep({
requiredParam: 'test-value',
});
console.log('Step result:', result);
return result;
}const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 30000);
try {
const response = await fetch(url, {
signal: controller.signal,
});
clearTimeout(timeout);
// ...
} catch (error: any) {
clearTimeout(timeout);
if (error.name === 'AbortError') {
throw new Error('Request timeout'); // Will retry
}
throw error;
}if (response.status === 429) {
// Let it retry automatically
throw new Error('Rate limited');
}import { z } from 'zod';
const schema = z.object({
email: z.string().email(),
age: z.number().min(0),
});
try {
const validated = schema.parse(data);
} catch (error) {
throw new FatalError('Invalid data');
}Once you've created a step you're happy with, consider contributing it to the registry so others can benefit from your work!