Defining Steps in ZoopFlow
Steps are the fundamental building blocks of workflows in ZoopFlow. This guide explains how to define, register, and use steps effectively.
What is a Step?
A step is a discrete, reusable unit of work with well-defined inputs and outputs. Steps should be focused on a single responsibility and designed for reuse across multiple workflows.
Creating Your First Step
Define the step interface
First, define the input and output types for your step. This ensures type safety throughout your workflow.
// Types for the step's input and output
interface StringTransformInput {
text: string;
operation: 'uppercase' | 'lowercase' | 'reverse';
}
interface StringTransformOutput {
result: string;
}Create the step definition
Use the defineStep function to create a step definition with validation schemas and execution logic.
import { defineStep } from '@zoopflow/core';
export const stringTransformer = defineStep({
id: 'examples.string.transform',
version: '1.0.0',
description: 'Transforms a string based on the specified operation',
// JSON Schema for input validation
inputSchema: {
type: 'object',
properties: {
text: { type: 'string' },
operation: {
type: 'string',
enum: ['uppercase', 'lowercase', 'reverse']
}
},
required: ['text', 'operation']
},
// JSON Schema for output validation
outputSchema: {
type: 'object',
properties: {
result: { type: 'string' }
},
required: ['result']
},
// Step implementation
execute: async (input: StringTransformInput, context) => {
const { text, operation } = input;
let result: string;
switch (operation) {
case 'uppercase':
result = text.toUpperCase();
break;
case 'lowercase':
result = text.toLowerCase();
break;
case 'reverse':
result = text.split('').reverse().join('');
break;
default:
throw new Error(`Unsupported operation: ${operation}`);
}
context.log('info', `Transformed text with ${operation} operation`);
return { result };
}
});Register the step
Register the step with the step registry to make it available for use in workflows.
import { registerStep } from '@zoopflow/core';
import { stringTransformer } from './steps/string-transformer';
// Register the step
registerStep(stringTransformer);Use the step
Now you can use the step in a workflow or execute it directly.
import { executeStep } from '@zoopflow/core';
import { stringTransformer } from './steps/string-transformer';
// Execute the step directly
const result = await executeStep(stringTransformer, {
text: 'Hello, World!',
operation: 'uppercase'
});
console.log(result); // { result: 'HELLO, WORLD!' }Step Naming Conventions
Steps should follow consistent naming conventions:
- ID Format:
domain.resource.action, e.g.,user.profile.update- Domain: The business domain or system area (e.g.,
user,payment,inventory) - Resource: The entity or object being operated on (e.g.,
profile,transaction,product) - Action: The operation being performed (e.g.,
update,process,validate) - Only lowercase alphanumeric characters are allowed in each segment
- Domain: The business domain or system area (e.g.,
- Version Format: Semantic versioning (
MAJOR.MINOR.PATCH) - Function Names: Descriptive verbs that indicate the action, e.g.,
validateEmail,processPayment
Note: Step ID must strictly follow the format
domain.resource.actionusing only lowercase alphanumeric characters with no hyphens or special characters.
Step Types
ZoopFlow supports several types of steps for different use cases:
Basic Steps
Simple steps that perform a single operation with input and output:
const validateEmail = defineStep({
id: 'validation.email.validate',
// ... other properties
execute: async (input, context) => {
const { email } = input;
const isValid = /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email);
return { isValid };
}
});Integration Steps
Steps that integrate with external systems:
const sendEmail = defineStep({
id: 'notification.email.send',
// ... other properties
execute: async (input, context) => {
const { to, subject, body } = input;
const emailService = context.getService('emailService');
const result = await emailService.send(to, subject, body);
return { messageId: result.id, sent: true };
}
});Composition Steps
Steps that compose other steps into a logical unit:
const processUserRegistration = defineStep({
id: 'user.registration.process',
// ... other properties
execute: async (input, context) => {
const { user } = input;
// Execute other steps as part of this step
const validationResult = await context.executeStep(validateUserData, { user });
if (!validationResult.valid) {
return { success: false, errors: validationResult.errors };
}
const userResult = await context.executeStep(createUserAccount, { user });
const notificationResult = await context.executeStep(sendWelcomeEmail, { user });
return {
success: true,
userId: userResult.userId,
emailSent: notificationResult.sent
};
}
});Step Configuration
Steps can be configured with additional options:
Retry Configuration
const paymentProcessor = defineStep({
id: 'payment.transaction.process',
// ... other properties
// Retry configuration
retryConfig: {
maxAttempts: 3,
backoffCoefficient: 1.5,
initialIntervalSeconds: 1,
maxIntervalSeconds: 10
},
execute: async (input, context) => {
// Payment processing logic
}
});Timeout Configuration
const longRunningTask = defineStep({
id: 'task.longrunning.execute',
// ... other properties
// Timeout configuration
timeoutSeconds: 600, // 10 minutes
execute: async (input, context) => {
// Long-running task logic
}
});Step Testing
Test your steps to ensure they work correctly:
import { executeStep, createTestContext } from '@zoopflow/core';
import { stringTransformer } from './steps/string-transformer';
describe('String Transformer Step', () => {
it('should transform text to uppercase', async () => {
const input = { text: 'hello', operation: 'uppercase' };
const context = createTestContext();
const result = await executeStep(stringTransformer, input, { context });
expect(result).toEqual({ result: 'HELLO' });
});
it('should reject invalid operations', async () => {
const input = { text: 'hello', operation: 'invalid' };
const context = createTestContext();
await expect(executeStep(stringTransformer, input, { context }))
.rejects.toThrow('Unsupported operation');
});
});Step Context
Steps receive a StepContext instance that provides utilities, services, and state management during execution:
const processDataStep = defineStep({
id: 'data.processing.process',
// ... other properties
execute: async (input, context) => {
// Use context for logging
context.log('Processing started', { dataSize: input.data.length });
// Use context for state management
await context.setState('status', 'processing');
// Create a checkpoint for potential recovery
const checkpointId = await context.createCheckpoint({ phase: 'pre-processing' });
try {
// Processing logic
const result = processData(input.data);
// Mark as completed
await context.complete({ status: 'success' });
return result;
} catch (error) {
// Restore from checkpoint
await context.restoreFromCheckpoint(checkpointId);
// Try alternative approach
const fallbackResult = fallbackProcessing(input.data);
return fallbackResult;
}
}
});For detailed information about the enhanced Step Context, see the Step Context guide.
Best Practices
- Single Responsibility: Each step should do one thing well
- Idempotency: Steps should be idempotent (repeated executions with the same input produce the same result)
- Error Handling: Include appropriate error handling and validation
- Checkpointing: Use checkpoints before operations that might fail
- History Tracking: Add custom history entries for observability
- Logging: Use the context logger for consistent, structured logging
- Lifecycle Management: Always mark steps as completed or failed
- Statelessness: Steps should be stateless for deterministic execution
- Documentation: Document your steps with clear descriptions and examples
- Testing: Write tests for normal operation and error cases