Skip to content

Complete Guide to Compute Functions

Overview

Compute Functions are serverless JavaScript functions that run in Centrali's secure cloud environment. They enable you to:

  • Process and transform data
  • Implement business logic
  • Integrate with external APIs
  • Respond to events and triggers
  • Generate reports and analytics
  • Send notifications

Functions run in a secure SES (Secure ECMAScript) sandbox with controlled access to specific APIs.

Function Anatomy

Every Centrali function follows this structure:

exports.handler = async (event, context) => {
  // Your function logic here

  return {
    success: true,
    data: {
      // Your response data
    }
  };
};

Input Parameters

Event Object

The event object contains the input data:

{
  // Direct invocation
  "data": {
    "userId": "user123",
    "action": "process"
  },

  // Trigger invocation (record change)
  "trigger": {
    "type": "record.created",
    "structureId": "str_orders",
    "record": {
      "id": "rec_abc123",
      "data": { /* record data */ }
    }
  },

  // HTTP trigger
  "http": {
    "method": "POST",
    "path": "/webhook",
    "headers": { /* headers */ },
    "body": { /* parsed body */ },
    "query": { /* query params */ }
  },

  // Schedule trigger
  "schedule": {
    "expression": "0 9 * * MON",
    "lastRun": "2024-01-15T09:00:00Z"
  }
}

Context Object

The context object provides execution context:

{
  "functionId": "fn_abc123",
  "functionName": "processOrder",
  "workspace": "acme",
  "executionId": "exec_xyz789",
  "timeout": 30000,  // milliseconds
  "memoryLimit": 256, // MB
  "environment": {
    // Your function's environment variables
    "API_KEY": "secret_key",
    "SERVICE_URL": "https://api.example.com"
  }
}

Return Value

Functions must return an object with:

{
  success: boolean,      // Required: indicates success/failure
  data: any,            // Optional: response data
  error: string,        // Optional: error message if success=false
  logs: string[],       // Optional: debug logs
  metrics: object       // Optional: custom metrics
}

TriggerParams vs ExecutionParams

Understanding the difference between triggerParams and executionParams is crucial for writing flexible compute functions.

TriggerParams

triggerParams are static configuration parameters defined when creating a trigger. They remain constant across all executions of that trigger.

Use cases: - API keys and secrets - Configuration settings - Service endpoints - Default values - Feature flags

Example - Setting triggerParams:

curl -X POST "https://api.centrali.io/data/workspace/acme/api/v1/function-triggers" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "payment-processor",
    "functionId": "fn_abc123",
    "executionType": "on-demand",
    "triggerMetadata": {
      "params": {
        "stripeApiKey": {
          "value": "sk_live_abc123",
          "encrypt": true
        },
        "webhookEndpoint": "https://api.example.com/webhook",
        "retryAttempts": 3,
        "enableLogging": true
      }
    }
  }'

ExecutionParams

executionParams are dynamic parameters passed at the time of execution. They change with each function invocation.

Use cases: - Record IDs to process - User input data - Transaction amounts - Timestamps - Event-specific data

Example - Passing executionParams:

curl -X POST "https://api.centrali.io/data/workspace/acme/api/v1/function-triggers/{triggerId}/execute" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "params": {
      "orderId": "order_123",
      "amount": 99.99,
      "customerId": "cust_456",
      "couponCode": "SAVE20"
    }
  }'

Using in Your Function

Both parameter types are available as global variables in your function:

exports.handler = async (event, context) => {
  // Access trigger params (static configuration)
  const apiKey = triggerParams.stripeApiKey;  // Automatically decrypted
  const webhookUrl = triggerParams.webhookEndpoint;
  const maxRetries = triggerParams.retryAttempts || 3;

  // Access execution params (dynamic runtime data)
  const orderId = executionParams.orderId;
  const amount = executionParams.amount;
  const customerId = executionParams.customerId;

  // Use both together
  const { http, centrali } = context.apis;

  // Use static config with dynamic data
  const paymentResult = await http.post('https://api.stripe.com/v1/charges', {
    headers: {
      'Authorization': `Bearer ${apiKey}`  // triggerParam
    },
    body: {
      amount: amount * 100,  // executionParam (converted to cents)
      currency: 'usd',
      customer: customerId   // executionParam
    }
  });

  // Update record with result
  await centrali.records.update(orderId, {
    paymentId: paymentResult.body.id,
    status: 'paid'
  });

  // Notify webhook if configured
  if (webhookUrl) {
    await http.post(webhookUrl, {
      body: {
        orderId,
        paymentId: paymentResult.body.id,
        amount
      }
    });
  }

  return {
    success: true,
    data: {
      paymentId: paymentResult.body.id,
      processed: true
    }
  };
};

Event-Driven Triggers

For event-driven triggers, the event data is automatically provided as executionParams:

exports.handler = async (event, context) => {
  // For record.created event, executionParams contains:
  // {
  //   event: "record_created",
  //   workspaceSlug: "acme",
  //   recordSlug: "orders",
  //   recordId: "rec_123",
  //   data: { /* record data */ },
  //   timestamp: "2025-01-15T10:30:00Z"
  // }

  const recordData = executionParams.data;
  const recordId = executionParams.recordId;

  // Use triggerParams for configuration
  const shouldNotify = triggerParams.sendNotifications;
  const emailTemplate = triggerParams.emailTemplate;

  if (shouldNotify && recordData.customerEmail) {
    await centrali.notifications.email({
      to: recordData.customerEmail,
      template: emailTemplate,
      data: {
        orderId: recordId,
        ...recordData
      }
    });
  }

  return { success: true };
};

Parameter Encryption

Sensitive triggerParams can be encrypted at rest:

// When creating trigger
{
  "triggerMetadata": {
    "params": {
      "apiKey": {
        "value": "sk_live_secret",
        "encrypt": true  // This will be encrypted
      },
      "endpoint": "https://api.example.com"  // Not encrypted
    }
  }
}

// In your function
exports.handler = async (event, context) => {
  // Encrypted params are automatically decrypted
  const apiKey = triggerParams.apiKey;  // Decrypted value
  const endpoint = triggerParams.endpoint;

  // Use normally
  const response = await http.get(endpoint, {
    headers: { 'Authorization': `Bearer ${apiKey}` }
  });

  return { success: true };
};

Re-run with Parameter Override

When re-running a function, you can override both types of parameters:

curl -X POST "https://api.centrali.io/data/workspace/acme/api/v1/functions/{functionId}/rerun/{runId}" \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "triggerParams": {
      "enableLogging": false  // Override trigger param
    },
    "executionParams": {
      "amount": 150.00  // Override execution param
    }
  }'

Best Practices

  1. Use triggerParams for:
  2. API credentials
  3. Service endpoints
  4. Feature flags
  5. Default configurations
  6. Retry settings

  7. Use executionParams for:

  8. Record/entity IDs
  9. User inputs
  10. Transaction data
  11. Timestamps
  12. Event payloads

  13. Security:

  14. Always encrypt sensitive triggerParams
  15. Never log sensitive parameters
  16. Validate executionParams before use

  17. Example Pattern:

    exports.handler = async (event, context) => {
      // Validate execution params
      if (!executionParams.recordId) {
        return {
          success: false,
          error: 'recordId is required in executionParams'
        };
      }
    
      // Use trigger params with defaults
      const retries = triggerParams.maxRetries || 3;
      const timeout = triggerParams.timeout || 5000;
    
      // Combine both for processing
      const result = await processWithRetry(
        executionParams.recordId,
        {
          apiKey: triggerParams.apiKey,
          endpoint: triggerParams.endpoint,
          retries,
          timeout
        }
      );
    
      return { success: true, data: result };
    };
    

Available APIs

Centrali SDK

Access Centrali's services within your function:

exports.handler = async (event, context) => {
  const { centrali } = context.apis;

  // Query records
  const products = await centrali.records.query({
    structure: 'Product',
    filter: { inStock: true },
    limit: 10
  });

  // Create a record
  const order = await centrali.records.create({
    structure: 'Order',
    data: {
      customerId: event.data.customerId,
      total: 99.99,
      status: 'pending'
    }
  });

  // Update a record
  await centrali.records.update('rec_xyz789', {
    status: 'processing'
  });

  // Execute another function
  const result = await centrali.functions.execute('calculateShipping', {
    orderId: order.id
  });

  // Send email
  await centrali.notifications.email({
    to: 'customer@example.com',
    subject: 'Order Confirmation',
    body: 'Your order has been confirmed!'
  });

  return {
    success: true,
    data: { orderId: order.id }
  };
};

HTTP Client

Make external API calls:

exports.handler = async (event, context) => {
  const { http } = context.apis;

  // GET request
  const response = await http.get('https://api.example.com/data', {
    headers: {
      'Authorization': `Bearer ${context.environment.API_KEY}`
    }
  });

  // POST request
  const result = await http.post('https://api.example.com/process', {
    body: {
      data: event.data
    },
    headers: {
      'Content-Type': 'application/json'
    }
  });

  // With error handling
  try {
    const data = await http.get('https://api.example.com/resource');
    return {
      success: true,
      data: data.body
    };
  } catch (error) {
    return {
      success: false,
      error: `API call failed: ${error.message}`
    };
  }
};

Utilities

Built-in utilities for common tasks:

exports.handler = async (event, context) => {
  const { utils } = context.apis;

  // Crypto functions
  const hash = utils.crypto.sha256('data');
  const hmac = utils.crypto.hmac('sha256', 'data', 'secret');
  const uuid = utils.crypto.uuid();

  // Date/time
  const now = utils.date.now();
  const formatted = utils.date.format(now, 'YYYY-MM-DD');
  const parsed = utils.date.parse('2024-01-15', 'YYYY-MM-DD');

  // Data manipulation
  const encoded = utils.base64.encode('Hello World');
  const decoded = utils.base64.decode(encoded);
  const urlEncoded = utils.url.encode('hello world');

  // Validation
  const isValid = utils.validate.email('user@example.com');
  const isUrl = utils.validate.url('https://example.com');

  return {
    success: true,
    data: { hash, uuid, formatted }
  };
};

Common Patterns

Data Processing

Process and transform records:

exports.handler = async (event, context) => {
  const { centrali } = context.apis;

  // Get records to process
  const records = await centrali.records.query({
    structure: 'Invoice',
    filter: { status: 'pending' }
  });

  // Process each record
  const processed = await Promise.all(
    records.map(async (record) => {
      // Calculate tax
      const tax = record.data.amount * 0.1;
      const total = record.data.amount + tax;

      // Update record
      await centrali.records.update(record.id, {
        tax,
        total,
        status: 'processed',
        processedAt: new Date().toISOString()
      });

      return { id: record.id, total };
    })
  );

  return {
    success: true,
    data: {
      processedCount: processed.length,
      records: processed
    }
  };
};

External API Integration

Integrate with third-party services:

exports.handler = async (event, context) => {
  const { http, centrali } = context.apis;

  // Get customer data
  const customer = await centrali.records.get(event.data.customerId);

  // Call external CRM API
  const crmResponse = await http.post('https://crm.example.com/api/customers', {
    body: {
      email: customer.data.email,
      name: customer.data.name,
      source: 'centrali'
    },
    headers: {
      'Authorization': `Bearer ${context.environment.CRM_API_KEY}`,
      'Content-Type': 'application/json'
    }
  });

  // Store CRM ID back in Centrali
  await centrali.records.update(event.data.customerId, {
    crmId: crmResponse.body.id,
    syncedAt: new Date().toISOString()
  });

  return {
    success: true,
    data: {
      crmId: crmResponse.body.id
    }
  };
};

Webhook Handler

Handle incoming webhooks:

exports.handler = async (event, context) => {
  const { centrali, utils } = context.apis;

  // Verify webhook signature
  const signature = event.http.headers['x-webhook-signature'];
  const expectedSignature = utils.crypto.hmac(
    'sha256',
    JSON.stringify(event.http.body),
    context.environment.WEBHOOK_SECRET
  );

  if (signature !== expectedSignature) {
    return {
      success: false,
      error: 'Invalid signature'
    };
  }

  // Process webhook data
  const webhookData = event.http.body;

  switch (webhookData.event) {
    case 'payment.completed':
      await centrali.records.update(webhookData.orderId, {
        paymentStatus: 'completed',
        paymentId: webhookData.paymentId
      });
      break;

    case 'payment.failed':
      await centrali.records.update(webhookData.orderId, {
        paymentStatus: 'failed',
        error: webhookData.error
      });
      break;
  }

  return {
    success: true,
    data: { processed: true }
  };
};

Batch Processing

Process large datasets efficiently:

exports.handler = async (event, context) => {
  const { centrali } = context.apis;

  // Process in batches to avoid timeout
  const BATCH_SIZE = 100;
  let offset = 0;
  let hasMore = true;
  let totalProcessed = 0;

  while (hasMore) {
    // Get batch of records
    const batch = await centrali.records.query({
      structure: 'User',
      filter: { needsProcessing: true },
      limit: BATCH_SIZE,
      offset
    });

    if (batch.length === 0) {
      hasMore = false;
      break;
    }

    // Process batch
    await Promise.all(
      batch.map(async (user) => {
        // Your processing logic
        await processUser(user);

        // Mark as processed
        await centrali.records.update(user.id, {
          needsProcessing: false,
          processedAt: new Date().toISOString()
        });
      })
    );

    totalProcessed += batch.length;
    offset += BATCH_SIZE;

    // Check if we're approaching timeout
    if (context.getRemainingTime() < 5000) {
      // Less than 5 seconds left, stop processing
      hasMore = false;
    }
  }

  return {
    success: true,
    data: {
      processed: totalProcessed,
      hasMore
    }
  };
};

async function processUser(user) {
  // Processing logic here
}

Error Handling

Implement robust error handling:

exports.handler = async (event, context) => {
  const { centrali, http } = context.apis;

  try {
    // Validate input
    if (!event.data.orderId) {
      throw new Error('Order ID is required');
    }

    // Get order with error handling
    let order;
    try {
      order = await centrali.records.get(event.data.orderId);
    } catch (error) {
      if (error.code === 'NOT_FOUND') {
        return {
          success: false,
          error: `Order ${event.data.orderId} not found`
        };
      }
      throw error; // Re-throw other errors
    }

    // External API call with retry
    let apiResponse;
    let retries = 3;

    while (retries > 0) {
      try {
        apiResponse = await http.post('https://api.example.com/process', {
          body: { orderId: order.id },
          timeout: 5000
        });
        break; // Success, exit retry loop
      } catch (error) {
        retries--;
        if (retries === 0) {
          // Log error and continue with fallback
          console.error('API call failed after 3 retries:', error);
          apiResponse = { body: { status: 'pending' } };
        } else {
          // Wait before retry (exponential backoff)
          await new Promise(resolve => setTimeout(resolve, Math.pow(2, 3 - retries) * 1000));
        }
      }
    }

    return {
      success: true,
      data: {
        orderId: order.id,
        status: apiResponse.body.status
      }
    };

  } catch (error) {
    // Log error for debugging
    console.error('Function error:', error);

    // Return user-friendly error
    return {
      success: false,
      error: 'An error occurred processing your request',
      logs: [error.stack]
    };
  }
};

Environment Variables

Store sensitive configuration as environment variables:

Setting Environment Variables

Via API:

curl -X PATCH "https://api.centrali.io/workspace/acme/api/v1/functions/fn_abc123" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "environment": {
      "API_KEY": "sk_live_abc123",
      "SERVICE_URL": "https://api.example.com",
      "FEATURE_FLAG": "true"
    }
  }'

Using in Functions

exports.handler = async (event, context) => {
  // Access via context.environment
  const apiKey = context.environment.API_KEY;
  const serviceUrl = context.environment.SERVICE_URL;
  const featureEnabled = context.environment.FEATURE_FLAG === 'true';

  // Use in API calls
  const { http } = context.apis;
  const response = await http.get(`${serviceUrl}/data`, {
    headers: {
      'Authorization': `Bearer ${apiKey}`
    }
  });

  return {
    success: true,
    data: response.body
  };
};

Testing Functions

Local Testing

Test your functions locally before deployment:

// test.js
const functionCode = require('./myFunction');

async function test() {
  // Mock event
  const event = {
    data: {
      userId: 'user123',
      action: 'process'
    }
  };

  // Mock context
  const context = {
    functionId: 'fn_test',
    workspace: 'test',
    environment: {
      API_KEY: 'test_key'
    },
    apis: {
      centrali: {
        records: {
          get: async (id) => ({ id, data: { name: 'Test' } }),
          create: async (data) => ({ id: 'rec_new', ...data }),
          update: async (id, data) => ({ id, ...data })
        }
      },
      http: {
        get: async (url) => ({ body: { success: true } }),
        post: async (url, options) => ({ body: { id: 'ext_123' } })
      },
      utils: {
        crypto: {
          sha256: (data) => 'hash',
          uuid: () => 'uuid-123'
        }
      }
    }
  };

  // Run function
  const result = await functionCode.handler(event, context);
  console.log('Result:', result);
}

test().catch(console.error);

Test via API

# Direct execution
curl -X POST "https://api.centrali.io/workspace/acme/api/v1/functions/fn_abc123/execute" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "data": {
      "test": true,
      "userId": "user123"
    }
  }'

# Dry run (doesn't save changes)
curl -X POST "https://api.centrali.io/workspace/acme/api/v1/functions/fn_abc123/test" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": {
      "data": { "test": true }
    },
    "context": {
      "environment": { "API_KEY": "test_key" }
    }
  }'

Performance Optimization

Best Practices

  1. Minimize Cold Starts

    // Initialize outside handler
    const config = {
      apiUrl: 'https://api.example.com',
      timeout: 5000
    };
    
    exports.handler = async (event, context) => {
      // Use pre-initialized config
      const response = await fetch(`${config.apiUrl}/data`);
      // ...
    };
    

  2. Parallel Processing

    exports.handler = async (event, context) => {
      const { centrali } = context.apis;
    
      // Parallel queries - faster
      const [users, orders, products] = await Promise.all([
        centrali.records.query({ structure: 'User', limit: 100 }),
        centrali.records.query({ structure: 'Order', limit: 100 }),
        centrali.records.query({ structure: 'Product', limit: 100 })
      ]);
    
      // vs Sequential - slower
      // const users = await centrali.records.query(...);
      // const orders = await centrali.records.query(...);
      // const products = await centrali.records.query(...);
    
      return { success: true, data: { users, orders, products } };
    };
    

  3. Efficient Queries

    exports.handler = async (event, context) => {
      const { centrali } = context.apis;
    
      // Good: Specific fields and filters
      const records = await centrali.records.query({
        structure: 'Order',
        filter: { status: 'pending', createdAt: { $gt: '2024-01-01' } },
        fields: ['id', 'total', 'customerId'],
        limit: 50
      });
    
      // Bad: Getting all data when you need specific fields
      // const records = await centrali.records.query({
      //   structure: 'Order'
      // });
    
      return { success: true, data: records };
    };
    

  4. Caching

    // Simple in-memory cache
    const cache = new Map();
    
    exports.handler = async (event, context) => {
      const { http } = context.apis;
      const cacheKey = `api_${event.data.endpoint}`;
    
      // Check cache
      if (cache.has(cacheKey)) {
        const cached = cache.get(cacheKey);
        if (cached.expiry > Date.now()) {
          return { success: true, data: cached.data, cached: true };
        }
      }
    
      // Fetch fresh data
      const response = await http.get(event.data.endpoint);
    
      // Cache for 5 minutes
      cache.set(cacheKey, {
        data: response.body,
        expiry: Date.now() + 5 * 60 * 1000
      });
    
      return { success: true, data: response.body, cached: false };
    };
    

Limitations & Quotas

Execution Limits

Resource Limit Description
Timeout 30 seconds Maximum execution time
Memory 256 MB Maximum memory allocation
Payload Size 6 MB Maximum input/output size
Environment Variables 4 KB Total size of all env vars
Concurrent Executions 100 Per workspace

API Rate Limits

API Limit Window
Centrali Records 1000 Per minute
HTTP Requests 100 Per minute
Function Calls 500 Per minute

Restricted Operations

Functions run in a secure sandbox and cannot: - Access the file system - Execute system commands - Open network sockets directly - Use Node.js modules (only provided APIs) - Access global objects like process, require, __dirname

Debugging & Monitoring

Logging

exports.handler = async (event, context) => {
  console.log('Function started', {
    eventData: event.data,
    functionId: context.functionId
  });

  try {
    // Your logic
    const result = await processData(event.data);

    console.log('Processing complete', {
      resultCount: result.length
    });

    return {
      success: true,
      data: result,
      logs: ['Processing completed successfully']
    };
  } catch (error) {
    console.error('Function error:', error);

    return {
      success: false,
      error: error.message,
      logs: [
        'Error occurred during processing',
        error.stack
      ]
    };
  }
};

Viewing Logs

# Get recent executions
curl "https://api.centrali.io/workspace/acme/api/v1/functions/fn_abc123/executions" \
  -H "Authorization: Bearer YOUR_API_KEY"

# Get specific execution logs
curl "https://api.centrali.io/workspace/acme/api/v1/functions/executions/exec_xyz789" \
  -H "Authorization: Bearer YOUR_API_KEY"

Metrics

Track custom metrics:

exports.handler = async (event, context) => {
  const startTime = Date.now();
  let recordsProcessed = 0;

  // Process records
  for (const record of event.data.records) {
    await processRecord(record);
    recordsProcessed++;
  }

  const duration = Date.now() - startTime;

  return {
    success: true,
    data: { processed: recordsProcessed },
    metrics: {
      duration,
      recordsProcessed,
      averageTime: duration / recordsProcessed
    }
  };
};

Security Best Practices

Input Validation

Always validate input data:

exports.handler = async (event, context) => {
  // Validate required fields
  if (!event.data.email || !event.data.userId) {
    return {
      success: false,
      error: 'Email and userId are required'
    };
  }

  // Validate email format
  const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  if (!emailRegex.test(event.data.email)) {
    return {
      success: false,
      error: 'Invalid email format'
    };
  }

  // Sanitize input
  const sanitizedData = {
    email: event.data.email.toLowerCase().trim(),
    userId: event.data.userId.replace(/[^a-zA-Z0-9-]/g, '')
  };

  // Continue processing...
};

Secrets Management

Never hardcode secrets:

// BAD - Never do this
const API_KEY = 'sk_live_abc123xyz';

// GOOD - Use environment variables
exports.handler = async (event, context) => {
  const API_KEY = context.environment.API_KEY;

  if (!API_KEY) {
    return {
      success: false,
      error: 'API key not configured'
    };
  }

  // Use the API key
};

Rate Limiting

Implement rate limiting for expensive operations:

const rateLimits = new Map();

exports.handler = async (event, context) => {
  const userId = event.data.userId;
  const now = Date.now();

  // Check rate limit (10 requests per minute)
  const userLimits = rateLimits.get(userId) || [];
  const recentRequests = userLimits.filter(time => now - time < 60000);

  if (recentRequests.length >= 10) {
    return {
      success: false,
      error: 'Rate limit exceeded. Try again later.'
    };
  }

  // Record this request
  recentRequests.push(now);
  rateLimits.set(userId, recentRequests);

  // Process request
  // ...
};

Common Use Cases

1. Data Enrichment

Enrich records with external data:

exports.handler = async (event, context) => {
  const { centrali, http } = context.apis;

  // Get the newly created user
  const user = event.trigger.record;

  // Lookup additional data from external service
  const enrichmentData = await http.get(
    `https://api.clearbit.com/v2/people/find?email=${user.data.email}`,
    {
      headers: {
        'Authorization': `Bearer ${context.environment.CLEARBIT_KEY}`
      }
    }
  );

  // Update user with enriched data
  await centrali.records.update(user.id, {
    company: enrichmentData.body.company.name,
    title: enrichmentData.body.employment.title,
    location: enrichmentData.body.geo.city,
    enrichedAt: new Date().toISOString()
  });

  return { success: true };
};

2. Notification System

Send notifications based on events:

exports.handler = async (event, context) => {
  const { centrali } = context.apis;

  // New order created
  const order = event.trigger.record;

  // Get customer details
  const customer = await centrali.records.get(order.data.customerId);

  // Send order confirmation email
  await centrali.notifications.email({
    to: customer.data.email,
    template: 'order-confirmation',
    data: {
      customerName: customer.data.name,
      orderNumber: order.id,
      total: order.data.total,
      items: order.data.items
    }
  });

  // Send SMS if phone number exists
  if (customer.data.phone) {
    await centrali.notifications.sms({
      to: customer.data.phone,
      message: `Your order ${order.id} has been confirmed! Total: $${order.data.total}`
    });
  }

  // Notify admin for high-value orders
  if (order.data.total > 1000) {
    await centrali.notifications.slack({
      channel: '#sales',
      message: `High-value order received: ${order.id} - $${order.data.total}`
    });
  }

  return { success: true };
};

3. Data Validation & Cleanup

Validate and clean data before saving:

exports.handler = async (event, context) => {
  const { utils } = context.apis;

  // Get the record being created/updated
  const record = event.trigger.record;

  // Validation rules
  const errors = [];

  // Validate email
  if (record.data.email && !utils.validate.email(record.data.email)) {
    errors.push('Invalid email address');
  }

  // Validate phone number
  if (record.data.phone) {
    const cleanPhone = record.data.phone.replace(/\D/g, '');
    if (cleanPhone.length !== 10) {
      errors.push('Phone number must be 10 digits');
    }
    // Update with cleaned phone
    record.data.phone = cleanPhone;
  }

  // Validate URL
  if (record.data.website && !utils.validate.url(record.data.website)) {
    errors.push('Invalid website URL');
  }

  // Normalize data
  if (record.data.email) {
    record.data.email = record.data.email.toLowerCase().trim();
  }

  if (record.data.name) {
    record.data.name = record.data.name.trim()
      .split(' ')
      .map(word => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase())
      .join(' ');
  }

  if (errors.length > 0) {
    return {
      success: false,
      error: errors.join(', ')
    };
  }

  return {
    success: true,
    data: record.data // Return cleaned data
  };
};

4. Scheduled Reports

Generate and send reports on schedule:

exports.handler = async (event, context) => {
  const { centrali } = context.apis;

  // Get yesterday's date range
  const yesterday = new Date();
  yesterday.setDate(yesterday.getDate() - 1);
  const startDate = new Date(yesterday.setHours(0, 0, 0, 0)).toISOString();
  const endDate = new Date(yesterday.setHours(23, 59, 59, 999)).toISOString();

  // Query orders from yesterday
  const orders = await centrali.records.query({
    structure: 'Order',
    filter: {
      createdAt: { $gte: startDate, $lte: endDate }
    }
  });

  // Calculate metrics
  const metrics = {
    totalOrders: orders.length,
    totalRevenue: orders.reduce((sum, o) => sum + o.data.total, 0),
    averageOrderValue: orders.length > 0 ?
      orders.reduce((sum, o) => sum + o.data.total, 0) / orders.length : 0,
    topProducts: calculateTopProducts(orders),
    ordersByStatus: groupByStatus(orders)
  };

  // Generate report
  const report = generateHTMLReport(metrics, startDate);

  // Email report to stakeholders
  await centrali.notifications.email({
    to: ['admin@example.com', 'sales@example.com'],
    subject: `Daily Sales Report - ${yesterday.toDateString()}`,
    html: report,
    attachments: [
      {
        filename: 'orders.csv',
        content: generateCSV(orders)
      }
    ]
  });

  // Store report as record
  await centrali.records.create({
    structure: 'Report',
    data: {
      type: 'daily-sales',
      date: startDate,
      metrics,
      sentTo: ['admin@example.com', 'sales@example.com']
    }
  });

  return {
    success: true,
    data: metrics
  };
};

function calculateTopProducts(orders) { /* ... */ }
function groupByStatus(orders) { /* ... */ }
function generateHTMLReport(metrics, date) { /* ... */ }
function generateCSV(orders) { /* ... */ }

Migration Guide

From AWS Lambda

// AWS Lambda
exports.handler = async (event, context) => {
  const body = JSON.parse(event.body);
  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'Success' })
  };
};

// Centrali Function
exports.handler = async (event, context) => {
  const body = event.http.body; // Already parsed
  return {
    success: true,
    data: { message: 'Success' }
  };
};

From Vercel Functions

// Vercel Function
export default async function handler(req, res) {
  const data = req.body;
  res.status(200).json({ success: true });
}

// Centrali Function
exports.handler = async (event, context) => {
  const data = event.http.body;
  return {
    success: true,
    data: { success: true }
  };
};

From Netlify Functions

// Netlify Function
exports.handler = async (event, context) => {
  return {
    statusCode: 200,
    body: JSON.stringify({ message: 'Hello' })
  };
};

// Centrali Function
exports.handler = async (event, context) => {
  return {
    success: true,
    data: { message: 'Hello' }
  };
};

Troubleshooting

Common Issues

  1. Function Timeout

    // Problem: Function times out after 30 seconds
    // Solution: Process in smaller batches
    
    exports.handler = async (event, context) => {
      const BATCH_SIZE = 50;
      const records = event.data.records;
    
      // Process in batches
      for (let i = 0; i < records.length; i += BATCH_SIZE) {
        const batch = records.slice(i, i + BATCH_SIZE);
        await processBatch(batch);
    
        // Check remaining time
        if (context.getRemainingTime() < 5000) {
          // Queue remaining for next execution
          await queueForProcessing(records.slice(i + BATCH_SIZE));
          break;
        }
      }
    
      return { success: true };
    };
    

  2. Memory Exceeded

    // Problem: Memory limit exceeded
    // Solution: Stream or paginate data
    
    exports.handler = async (event, context) => {
      const { centrali } = context.apis;
    
      // Instead of loading all records
      // const allRecords = await centrali.records.query({ structure: 'LargeTable' });
    
      // Process in pages
      let page = 0;
      let hasMore = true;
    
      while (hasMore) {
        const batch = await centrali.records.query({
          structure: 'LargeTable',
          limit: 100,
          offset: page * 100
        });
    
        if (batch.length === 0) {
          hasMore = false;
        } else {
          await processBatch(batch);
          page++;
        }
      }
    
      return { success: true };
    };
    

  3. Rate Limit Errors

    // Problem: External API rate limits
    // Solution: Implement backoff and retry
    
    async function callAPIWithRetry(url, options, maxRetries = 3) {
      for (let i = 0; i < maxRetries; i++) {
        try {
          return await http.get(url, options);
        } catch (error) {
          if (error.status === 429 && i < maxRetries - 1) {
            // Rate limited, wait and retry
            const delay = Math.pow(2, i) * 1000;
            await new Promise(resolve => setTimeout(resolve, delay));
          } else {
            throw error;
          }
        }
      }
    }
    

Next Steps