Examples
Basic Step

Basic Step Definition Example

This example demonstrates how to create, register, and execute basic steps in ZoopFlow. Steps are the fundamental building blocks of workflows in ZoopFlow.

The source code for this example is available on GitHub.

What You'll Learn

  • How to define a step with input and output schemas
  • How to implement a step's execution logic
  • How to register steps with a registry
  • How to execute a step and validate its inputs/outputs
  • How to add custom validation to steps

Step Definition

A step in ZoopFlow is defined using the defineStep function, which takes a configuration object:

import { defineStep } from '@zoopflow/core';
 
const calculateTotal = defineStep({
  id: 'order.total.calculate',
  version: '1.0.0',
  description: 'Calculates the total price for an order',
  
  // Input validation schema
  inputSchema: {
    type: 'object',
    properties: {
      items: {
        type: 'array',
        items: {
          type: 'object',
          properties: {
            productId: { type: 'string' },
            quantity: { type: 'number' },
            price: { type: 'number' }
          },
          required: ['productId', 'quantity', 'price']
        }
      },
      taxRate: { type: 'number', default: 0 }
    },
    required: ['items']
  },
  
  // Output validation schema
  outputSchema: {
    type: 'object',
    properties: {
      subtotal: { type: 'number' },
      tax: { type: 'number' },
      total: { type: 'number' }
    },
    required: ['subtotal', 'tax', 'total']
  },
  
  // Step implementation
  execute: async (input, context) => {
    // Calculate the subtotal
    const subtotal = input.items.reduce(
      (sum, item) => sum + (item.price * item.quantity),
      0
    );
    
    // Calculate tax
    const tax = subtotal * (input.taxRate || 0);
    
    // Calculate total
    const total = subtotal + tax;
    
    // Return the result
    return {
      subtotal,
      tax,
      total
    };
  }
});

Step Flow Visualization

The example above performs a calculation. Here's a visualization of the step's execution flow:

Step Execution

Once a step is defined, it can be executed directly or as part of a flow:

import { executeStep } from '@zoopflow/core';
 
// Sample input data
const orderData = {
  items: [
    { productId: 'product1', quantity: 2, price: 10.99 },
    { productId: 'product2', quantity: 1, price: 24.99 }
  ],
  taxRate: 0.08 // 8% tax
};
 
// Execute the step
const result = await executeStep(calculateTotal, orderData);
 
console.log(result);
// Output:
// {
//   subtotal: 46.97,
//   tax: 3.76,
//   total: 50.73
// }

Files in this Example

  • basic-step-definition.ts - Demonstrates how to define a basic step
  • step-registry-example.ts - Shows how to register and use steps with a registry
  • run-example.ts - Entry point to run the example

Interactive Playground

Try the example in our interactive playground below:

Basic Step Example

import React, { useState } from 'react';
import { defineStep } from './zoopflow';

// Define the calculateTotal step
const calculateTotal = defineStep({
  id: 'order.total.calculate',
  version: '1.0.0',
  description: 'Calculates the total price for an order',
  
  execute: async (input, context) => {
    // Calculate the subtotal
    const subtotal = input.items.reduce(
      (sum, item) => sum + (item.price * item.quantity),
      0
    );
    
    // Calculate tax
    const tax = subtotal * (input.taxRate || 0);
    
    // Calculate total
    const total = subtotal + tax;
    
    // Return the result
    return {
      subtotal,
      tax,
      total
    };
  }
});

// Product data
const products = [
  { id: 'product1', name: 'Widget A', price: 10.99 },
  { id: 'product2', name: 'Widget B', price: 24.99 },
  { id: 'product3', name: 'Widget C', price: 15.49 }
];

export function App() {
  const [cart, setCart] = useState([]);
  const [taxRate, setTaxRate] = useState(0.08);
  const [result, setResult] = useState(null);
  
  // Add product to cart
  const addToCart = (product) => {
    const existing = cart.find(item => item.productId === product.id);
    
    if (existing) {
      setCart(cart.map(item => 
        item.productId === product.id 
          ? { ...item, quantity: item.quantity + 1 } 
          : item
      ));
    } else {
      setCart([...cart, { 
        productId: product.id,
        name: product.name,
        price: product.price,
        quantity: 1
      }]);
    }
  };
  
  // Remove item from cart
  const removeFromCart = (productId) => {
    setCart(cart.filter(item => item.productId !== productId));
  };
  
  // Calculate order total
  const calculateOrder = async () => {
    try {
      const orderData = {
        items: cart.map(item => ({
          productId: item.productId,
          price: item.price,
          quantity: item.quantity
        })),
        taxRate
      };
      
      // Execute the step
      const result = await calculateTotal.execute(orderData, {
        logger: {
          info: console.log,
          error: console.error
        }
      });
      
      setResult(result);
    } catch (err) {
      console.error('Error calculating total:', err);
    }
  };
  
  return (
    <div className="container">
      <h2>Order Calculator Example</h2>
      
      <div className="panel">
        <h3>Products</h3>
        <div className="product-list">
          {products.map(product => (
            <div key={product.id} className="product-item">
              <div>
                <strong>{`${product.name}`}</strong>
                <div>{`$${product.price.toFixed(2)}`}</div>
              </div>
              <button onClick={() => addToCart(product)}>Add to Cart</button>
            </div>
          ))}
        </div>
      </div>
      
      <div className="panel">
        <h3>Shopping Cart</h3>
        {cart.length === 0 ? (
          <p>Your cart is empty</p>
        ) : (
          <>
            <div className="cart-items">
              {cart.map(item => (
                <div key={item.productId} className="cart-item">
                  <div>
                    <strong>{`${item.name}`}</strong> × {`${item.quantity}`}
                    <div>{`$${(item.price * item.quantity).toFixed(2)}`}</div>
                  </div>
                  <button 
                    className="remove-button"
                    onClick={() => removeFromCart(item.productId)}
                  ></button>
                </div>
              ))}
            </div>
            
            <div className="tax-rate">
              <label>
                Tax Rate:
                <input
                  type="number"
                  min="0"
                  max="1"
                  step="0.01"
                  value={taxRate}
                  onChange={(e) => setTaxRate(Number(e.target.value))}
                />
              </label>
            </div>
            
            <button 
              className="primary-button"
              onClick={calculateOrder}
              disabled={cart.length === 0}
            >
              Calculate Total
            </button>
          </>
        )}
      </div>
      
      {result && (
        <div className="panel result">
          <h3>Order Summary</h3>
          <div className="summary-item">
            <span>Subtotal:</span>
            <span>{`$${result.subtotal.toFixed(2)}`}</span>
          </div>
          <div className="summary-item">
            <span>Tax {`(${(taxRate * 100).toFixed(0)}%)`}:</span>
            <span>{`$${result.tax.toFixed(2)}`}</span>
          </div>
          <div className="summary-item total">
            <span>Total:</span>
            <span>{`$${result.total.toFixed(2)}`}</span>
          </div>
        </div>
      )}
      
      <style>{
        `
        .container {
          font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
          max-width: 600px;
          margin: 0 auto;
          padding: 20px;
        }
        
        .panel {
          background-color: #f8f8f8;
          border-radius: 8px;
          padding: 16px;
          margin-bottom: 20px;
          border: 1px solid #e0e0e0;
        }
        
        h2, h3 {
          margin-top: 0;
        }
        
        .product-list {
          display: grid;
          grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
          gap: 12px;
        }
        
        .product-item {
          background-color: white;
          padding: 12px;
          border-radius: 6px;
          border: 1px solid #e0e0e0;
          display: flex;
          flex-direction: column;
          justify-content: space-between;
        }
        
        .cart-items {
          margin-bottom: 16px;
        }
        
        .cart-item {
          display: flex;
          justify-content: space-between;
          align-items: center;
          padding: 8px 0;
          border-bottom: 1px solid #e0e0e0;
        }
        
        .tax-rate {
          margin: 16px 0;
        }
        
        .tax-rate input {
          width: 60px;
          margin-left: 8px;
          padding: 4px;
        }
        
        .summary-item {
          display: flex;
          justify-content: space-between;
          margin: 8px 0;
        }
        
        .summary-item.total {
          font-weight: bold;
          font-size: 1.2em;
          margin-top: 16px;
          padding-top: 8px;
          border-top: 1px solid #e0e0e0;
        }
        
        button {
          background-color: #f0f0f0;
          border: 1px solid #ccc;
          border-radius: 4px;
          padding: 6px 12px;
          cursor: pointer;
          font-weight: 500;
          transition: all 0.2s;
        }
        
        button:hover {
          background-color: #e0e0e0;
        }
        
        .primary-button {
          background-color: #00f173;
          color: black;
          padding: 8px 16px;
          font-weight: 600;
          width: 100%;
          margin-top: 12px;
        }
        
        .primary-button:hover {
          background-color: #00d363;
        }
        
        .remove-button {
          background: transparent;
          border: none;
          color: #ff5252;
          font-weight: bold;
        }
        `
      }</style>
    </div>
  );
}

Key Concepts

  1. Step Definition: Steps are defined with defineStep and include metadata and implementation.
  2. Input Validation: The inputSchema validates inputs before execution.
  3. Output Validation: The outputSchema validates the step's return value.
  4. Execution Context: The context parameter provides utilities and services during execution.
  5. Error Handling: Steps can include error handling logic in their implementation.

Next Steps

  • Try modifying the steps to add new operations
  • Implement additional validation logic
  • Explore how to compose steps into a flow in the Flow Definition Example
  • Learn more about Error Handling patterns