Guides
Defining Steps

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
  • 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.action using 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

  1. Single Responsibility: Each step should do one thing well
  2. Idempotency: Steps should be idempotent (repeated executions with the same input produce the same result)
  3. Error Handling: Include appropriate error handling and validation
  4. Checkpointing: Use checkpoints before operations that might fail
  5. History Tracking: Add custom history entries for observability
  6. Logging: Use the context logger for consistent, structured logging
  7. Lifecycle Management: Always mark steps as completed or failed
  8. Statelessness: Steps should be stateless for deterministic execution
  9. Documentation: Document your steps with clear descriptions and examples
  10. Testing: Write tests for normal operation and error cases