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}`
    };
  }
};

Crypto API

The api.crypto module provides cryptographic functions for hashing and signing data:

exports.handler = async (event, context) => {
  // SHA256 Hash
  // Returns a base64-encoded SHA256 hash of the input data
  const contentHash = api.crypto.sha256('Hello World');
  // => "pZGm1Av0IEBKARczz7exkNYsZb8LzaMrV7J32a2fFG4="

  // HMAC-SHA256 Signature (with plain string key)
  const signature = api.crypto.hmacSha256('my-secret-key', 'data-to-sign');
  // => Base64-encoded HMAC signature

  // HMAC-SHA256 Signature (with base64-encoded key)
  // Useful for Azure Communication Services and other APIs that provide base64 keys
  const azureSignature = api.crypto.hmacSha256(accessKey, stringToSign, {
    keyEncoding: 'base64'
  });

  return {
    success: true,
    data: { contentHash, signature }
  };
};

Azure Communication Services Example

The crypto API is designed to work with services like Azure Communication Services that require HMAC-SHA256 signed requests:

exports.handler = async (event, context) => {
  const { executionParams, triggerParams } = event;

  // Azure Communication Services credentials (from trigger params)
  const endpoint = triggerParams.acsEndpoint; // e.g., "https://your-resource.communication.azure.com"
  const accessKey = triggerParams.acsAccessKey; // Base64-encoded access key

  // Prepare the request
  const path = '/sms?api-version=2021-03-07';
  const host = new URL(endpoint).host;
  const date = new Date().toUTCString();
  const requestBody = JSON.stringify({
    from: '+18001234567',
    to: ['+15551234567'],
    message: executionParams.message
  });

  // Create the content hash
  const contentHash = api.crypto.sha256(requestBody);

  // Create the string to sign
  const stringToSign = `POST\n${path}\n${date};${host};${contentHash}`;

  // Create the HMAC-SHA256 signature using base64-encoded key
  const signature = api.crypto.hmacSha256(accessKey, stringToSign, {
    keyEncoding: 'base64'
  });

  // Create the Authorization header
  const authHeader = `HMAC-SHA256 SignedHeaders=date;host;x-ms-content-sha256&Signature=${signature}`;

  // Make the API request
  const response = await api.httpPost(`${endpoint}${path}`, {
    headers: {
      'Authorization': authHeader,
      'Date': date,
      'x-ms-content-sha256': contentHash,
      'Content-Type': 'application/json'
    },
    data: requestBody
  });

  return {
    success: true,
    data: { messageId: response.data.value[0].messageId }
  };
};

Webhook Signature Verification

Verify incoming webhook signatures:

exports.handler = async (event, context) => {
  const webhookSecret = triggerParams.webhookSecret;
  const receivedSignature = event.http.headers['x-signature'];
  const payload = JSON.stringify(event.http.body);

  // Compute expected signature
  const expectedSignature = api.crypto.hmacSha256(webhookSecret, payload);

  // Constant-time comparison to prevent timing attacks
  if (receivedSignature !== expectedSignature) {
    return {
      success: false,
      error: 'Invalid webhook signature'
    };
  }

  // Process the webhook...
  return { success: true };
};

Base64 API

The api.base64 module provides Base64 encoding and decoding utilities:

exports.handler = async (event, context) => {
  // Encode a string to Base64
  const encoded = api.base64.encode("Hello, World!");
  // => "SGVsbG8sIFdvcmxkIQ=="

  // Decode a Base64 string
  const decoded = api.base64.decode("SGVsbG8sIFdvcmxkIQ==");
  // => "Hello, World!"

  return {
    success: true,
    data: { encoded, decoded }
  };
};

Basic Auth Header Example

The most common use case is creating Basic Authentication headers for external APIs:

exports.handler = async (event, context) => {
  const username = triggerParams.apiUsername;
  const password = triggerParams.apiPassword;

  // Create Basic Auth header
  const credentials = `${username}:${password}`;
  const authHeader = `Basic ${api.base64.encode(credentials)}`;

  // Use in HTTP request
  const response = await api.httpGet('https://api.example.com/data', {
    headers: {
      'Authorization': authHeader
    }
  });

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

Decoding Webhook Payloads

Some webhooks send Base64-encoded payloads:

exports.handler = async (event, context) => {
  // Webhook body is Base64 encoded
  const encodedPayload = event.http.body.data;

  // Decode the payload
  const decodedPayload = api.base64.decode(encodedPayload);
  const payload = JSON.parse(decodedPayload);

  // Process the decoded payload
  return {
    success: true,
    data: payload
  };
};

Template Rendering (Handlebars)

The api.renderTemplate() method provides native Handlebars templating support for generating dynamic content like emails, notifications, and formatted output.

exports.handler = async (event, context) => {
  const template = "Hello {{customer.first_name}}, your order #{{order.number}} is confirmed!";

  const data = {
    customer: { first_name: "Mary", last_name: "Olowu" },
    order: { number: "GWM-2024-001234" }
  };

  const result = api.renderTemplate(template, data);
  // Output: "Hello Mary, your order #GWM-2024-001234 is confirmed!"

  return { success: true, message: result };
};

Syntax:

api.renderTemplate(template, data, options?)

Parameters: - template (string): Handlebars template string - data (object): Data context for variable replacement - options (optional): Configuration object - helpers: Custom helper functions - partials: Reusable template fragments - strict: Throw on missing variables (default: false) - escapeHtml: HTML escape output (default: true)

Variable Interpolation

// Simple variables
"Hello {{name}}"

// Nested properties
"{{customer.address.city}}"

// Array access
"{{items.0.name}}"

// HTML escaping (default)
"{{content}}"  // <script> becomes &lt;script&gt;

// Unescaped HTML (use carefully)
"{{{rawHtml}}}"

Conditionals

const template = `
{{#if hasTracking}}
  Track your order: {{tracking.url}}
{{else}}
  Tracking info coming soon
{{/if}}

{{#unless cancelled}}
  Your order is on the way!
{{/unless}}
`;

Iteration

const template = `
{{#each items}}
  <tr>
    <td>{{this.name}}</td>
    <td>{{this.quantity}}</td>
    <td>{{formatCurrency this.price "NGN"}}</td>
  </tr>
{{/each}}
`;

// With index and first/last
const template2 = `
{{#each items}}
  {{@index}}. {{this.name}}{{#if @first}} (first){{/if}}{{#if @last}} (last){{/if}}
{{/each}}
`;

Built-in Helpers

Formatting Helpers:

Helper Example Output
formatCurrency {{formatCurrency 125000 "NGN"}} ₦125,000
formatDate {{formatDate created_at "long"}} Wednesday, November 27, 2024
formatNumber {{formatNumber 1234.5 2}} 1,234.50
// Currency (supports NGN, USD, EUR, GBP, JPY, and more)
"Total: {{formatCurrency order.total 'NGN'}}"  // ₦125,000

// Date formatting
"{{formatDate date 'short'}}"    // 11/27/2024
"{{formatDate date 'long'}}"     // Wednesday, November 27, 2024
"{{formatDate date 'date'}}"     // November 27, 2024
"{{formatDate date 'time'}}"     // 10:30 AM
"{{formatDate date 'datetime'}}" // November 27, 2024, 10:30 AM
"{{formatDate date 'iso'}}"      // 2024-11-27T10:30:00.000Z (default)

// Number formatting
"{{formatNumber 1234567}}"       // 1,234,567

String Helpers:

Helper Example Output
uppercase {{uppercase "hello"}} HELLO
lowercase {{lowercase "HELLO"}} hello
capitalize {{capitalize "hello world"}} Hello World
truncate {{truncate text 50 "..."}} First 50 chars...

Comparison Helpers (use with #if):

Helper Example Description
eq {{#if (eq status "shipped")}} Equal
ne {{#if (ne status "cancelled")}} Not equal
gt {{#if (gt quantity 0)}} Greater than
gte {{#if (gte total 1000)}} Greater than or equal
lt {{#if (lt stock 5)}} Less than
lte {{#if (lte stock 10)}} Less than or equal
and {{#if (and paid shipped)}} Logical AND
or {{#if (or cancelled refunded)}} Logical OR
not {{#if (not cancelled)}} Logical NOT

Array Helpers:

Helper Example Output
length {{length items}} 5
first {{first items}} First item
last {{last items}} Last item
join {{join tags ", "}} tag1, tag2, tag3

Math Helpers:

Helper Example Output
add {{add a b}} Sum
subtract {{subtract a b}} Difference
multiply {{multiply a b}} Product
divide {{divide a b}} Quotient
round {{round value 2}} Rounded value

Conditional Helpers:

Helper Example Output
ifThen {{ifThen isPremium "Premium" "Standard"}} Premium or Standard
default {{default name "Guest"}} Value or fallback
coalesce {{coalesce name nickname "Anonymous"}} First non-empty

Custom Helpers

Register custom helpers for specialized formatting:

async function run() {
  const template = "Total: {{formatNaira amount}} | Date: {{shortDate orderDate}}";

  const result = api.renderTemplate(template, data, {
    helpers: {
      formatNaira: (amount) => `₦${Number(amount).toLocaleString()}`,
      shortDate: (dateStr) => {
        const date = new Date(dateStr);
        return `${date.getDate()}/${date.getMonth() + 1}/${date.getFullYear()}`;
      }
    }
  });

  return { success: true, message: result };
}

Partials (Reusable Templates)

async function run() {
  const template = "{{> header}}Main content here{{> footer}}";

  const result = api.renderTemplate(template, { title: "Welcome" }, {
    partials: {
      header: "<header><h1>{{title}}</h1></header>",
      footer: "<footer>© 2024 Company</footer>"
    }
  });

  return { success: true, html: result };
}

Complete Email Example

async function run() {
  const template = `
<!DOCTYPE html>
<html>
<body>
<h1>Order Confirmed!</h1>
<p>Hi {{customer.first_name}},</p>
<p>Thank you for your order #{{order.order_number}}.</p>

<h2>Order Summary</h2>
<table>
  {{#each items}}
  <tr>
    <td>{{this.name}}{{#if this.variant}} ({{this.variant}}){{/if}}</td>
    <td>{{this.quantity}}</td>
    <td>{{formatCurrency this.total "NGN"}}</td>
  </tr>
  {{/each}}
</table>

<p><strong>Total:</strong> {{formatCurrency order.total "NGN"}}</p>

{{#if tracking}}
<h2>Tracking Information</h2>
<p>Carrier: {{tracking.carrier}}</p>
<p>Tracking #: {{tracking.number}}</p>
{{/if}}

<p>Order placed on {{formatDate order.created_at "long"}}</p>
</body>
</html>
  `;

  const data = {
    customer: { first_name: "Mary", email: "mary@example.com" },
    order: {
      order_number: "GWM-2024-001234",
      total: 115000,
      created_at: new Date().toISOString()
    },
    items: [
      { name: "Silk Blouse", variant: "Size M", quantity: 1, total: 45000 },
      { name: "Linen Trousers", variant: "Size 32", quantity: 2, total: 70000 }
    ],
    tracking: { carrier: "DHL", number: "1234567890" }
  };

  const html = api.renderTemplate(template, data);

  // Send via Resend
  await api.httpPost("https://api.resend.com/emails", {
    from: "Store <orders@example.com>",
    to: data.customer.email,
    subject: `Order #${data.order.order_number} Confirmed!`,
    html: html
  }, {
    headers: {
      "Authorization": `Bearer ${triggerParams.resendApiKey}`,
      "Content-Type": "application/json"
    }
  });

  return { success: true };
}

Security Notes

  • HTML Escaping: By default, all output is HTML-escaped to prevent XSS attacks
  • Triple Braces: Use {{{rawHtml}}} only for trusted content
  • No Code Execution: Handlebars is logic-less; no arbitrary JavaScript execution
  • Sandbox Safe: Templates cannot access filesystem, network, or globals

Utilities

Built-in utilities for common tasks:

exports.handler = async (event, context) => {
  // UUID generation
  const uuid = api.uuid();  // e.g., "550e8400-e29b-41d4-a716-446655440000"

  // Date/time with dayjs
  const now = api.dayjs();
  const formatted = api.dayjs().format('YYYY-MM-DD');
  const parsed = api.dayjs('2024-01-15', 'YYYY-MM-DD');
  const nextWeek = api.dayjs().add(7, 'day');

  // Lodash utilities
  const grouped = api.lodash.groupBy(records, 'status');
  const unique = api.lodash.uniqBy(items, 'id');

  // Math operations with mathjs
  const result = api.math.evaluate('2 + 3 * 4');

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

File Storage API

The api.storeFile() method allows you to store binary files (PDFs, images, documents) directly to Centrali's blob storage from within your compute functions.

Basic Usage

exports.handler = async (event, context) => {
  // Store a base64-encoded PDF
  const result = await api.storeFile(
    pdfBase64Content,           // File content (base64 or UTF-8 string)
    "shipping-label.pdf",       // Filename
    {
      mimeType: "application/pdf",  // Required: MIME type
      encoding: "base64"            // "base64" or "utf8" (default: "utf8")
    }
  );

  if (result.success) {
    // File stored successfully
    api.log(`File URL: ${result.fileUrl}`);
    api.log(`Render ID: ${result.renderId}`);

    // Save the file reference to a record
    await api.updateRecord(executionParams.orderId, {
      shippingLabelUrl: result.fileUrl,
      shippingLabelId: result.renderId
    });
  } else {
    api.logError(`Failed to store file: ${result.error}`);
  }

  return { success: result.success };
};

Parameters

Parameter Type Required Description
content string Yes File content as a string (base64-encoded or raw UTF-8)
filename string Yes Name for the file (e.g., "report.pdf", "image.png")
options.mimeType string Yes MIME type (e.g., "application/pdf", "image/png")
options.encoding string No Content encoding: "base64" or "utf8" (default: "utf8")
options.folder string No Target folder path (default: "/root/shared")
options.isPublic boolean No Make file publicly accessible (default: false)

Return Value

// Success response
{
  success: true,
  fileUrl: "https://storage.centrali.io/workspace/exec_abc123_xyz.pdf",
  renderId: "a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6"
}

// Error response
{
  success: false,
  error: "File size (150MB) exceeds maximum allowed (100MB)"
}

DHL Shipping Label Example

A common use case is storing shipping labels returned by carrier APIs:

async function run() {
  // Get order details
  const order = await api.fetchRecord(executionParams.orderId);

  // Call DHL API to generate shipping label
  const dhlResponse = await api.httpPost('https://api.dhl.com/parcel/labels', {
    headers: {
      'Authorization': `Bearer ${triggerParams.dhlApiKey}`,
      'Content-Type': 'application/json'
    },
    data: {
      shipperAddress: triggerParams.warehouseAddress,
      recipientAddress: order.data.shippingAddress,
      weight: order.data.totalWeight,
      service: 'EXPRESS'
    }
  });

  // DHL returns the label as base64-encoded PDF
  const labelBase64 = dhlResponse.labelImage;

  // Store the label in Centrali storage
  const result = await api.storeFile(
    labelBase64,
    `dhl-label-${order.data.orderNumber}.pdf`,
    {
      mimeType: "application/pdf",
      encoding: "base64",
      folder: "/shipping-labels",
      isPublic: false  // Private - only accessible with auth
    }
  );

  if (!result.success) {
    return {
      success: false,
      error: `Failed to store shipping label: ${result.error}`
    };
  }

  // Update order with label URL
  await api.updateRecord(executionParams.orderId, {
    shippingLabelUrl: result.fileUrl,
    shippingLabelId: result.renderId,
    trackingNumber: dhlResponse.trackingNumber,
    status: 'shipped'
  });

  return {
    success: true,
    data: {
      trackingNumber: dhlResponse.trackingNumber,
      labelUrl: result.fileUrl
    }
  };
}

Public vs Private Files

async function run() {
  // Private file (default) - requires authentication to access
  const privateResult = await api.storeFile(
    invoiceContent,
    "invoice-2024-001.pdf",
    {
      mimeType: "application/pdf",
      encoding: "base64",
      isPublic: false  // Default - requires auth token to download
    }
  );

  // Public file - accessible without authentication
  const publicResult = await api.storeFile(
    productImage,
    "product-photo.jpg",
    {
      mimeType: "image/jpeg",
      encoding: "base64",
      isPublic: true  // Anyone with the URL can access
    }
  );

  return { success: true };
}

Organizing Files in Folders

async function run() {
  const orderId = executionParams.orderId;

  // Store in custom folder structure
  const result = await api.storeFile(
    documentContent,
    "contract.pdf",
    {
      mimeType: "application/pdf",
      encoding: "base64",
      folder: `/orders/${orderId}/documents`  // Custom folder path
    }
  );

  return { success: result.success };
}

Storing Generated Reports

async function run() {
  // Generate HTML report
  const reportHtml = api.renderTemplate(
    triggerParams.reportTemplate,
    { orders: executionParams.orders, date: new Date().toISOString() }
  );

  // Store as HTML file (UTF-8 encoding, not base64)
  const result = await api.storeFile(
    reportHtml,
    `sales-report-${api.formatDate(new Date(), 'YYYY-MM-DD')}.html`,
    {
      mimeType: "text/html",
      encoding: "utf8",  // UTF-8 for text content
      folder: "/reports/sales",
      isPublic: false
    }
  );

  return {
    success: true,
    data: { reportUrl: result.fileUrl }
  };
}

Limits and Best Practices

Limit Value
Maximum file size 100 MB
Supported encodings base64, utf8

Best Practices:

  1. Always check the result - api.storeFile() returns { success: false, error: "..." } on failure
  2. Use appropriate encoding - Use "base64" for binary files (PDFs, images), "utf8" for text files
  3. Store the renderId - Save the renderId in your records for future reference
  4. Choose visibility carefully - Use isPublic: false for sensitive documents
  5. Organize with folders - Use meaningful folder paths for easier file management
  6. Handle errors gracefully - Always handle the error case in your function

Error Handling

async function run() {
  try {
    const result = await api.storeFile(content, filename, options);

    if (!result.success) {
      // Handle specific errors
      if (result.error.includes('exceeds maximum')) {
        api.logError('File too large - consider compressing');
      } else if (result.error.includes('not available')) {
        api.logError('Storage service unavailable');
      }
      return { success: false, error: result.error };
    }

    return { success: true, data: { fileUrl: result.fileUrl } };

  } catch (error) {
    api.logError({ message: 'Unexpected error storing file', error: error.message });
    return { success: false, error: 'Failed to store file' };
  }
}

CSV and JSON Export Helpers

The API provides convenient methods for converting data to CSV/JSON formats and storing them as files.

Converting Data to CSV

Use api.toCSV() to convert an array of objects to a CSV string:

async function run() {
  // Query records
  const orders = await api.queryRecords('orders', {
    filter: { status: 'completed' }
  });

  // Convert to CSV string
  const csvString = api.toCSV(orders.items);

  // Use the CSV string (e.g., for email attachment, further processing)
  api.log(`Generated CSV with ${orders.items.length} rows`);

  return { success: true, csv: csvString };
}

Options:

Option Type Default Description
headers string[] Auto-detected Custom column headers (default: keys from first object)
delimiter string "," Field delimiter
includeHeaders boolean true Include header row in output
// Custom headers and delimiter
const csv = api.toCSV(data, {
  headers: ['name', 'email', 'status'],  // Only include these columns
  delimiter: ';',                         // Use semicolon for European Excel
  includeHeaders: true
});

Converting Data to JSON

Use api.toJSON() to convert data to a JSON string:

async function run() {
  const config = {
    version: '1.0',
    settings: { theme: 'dark', language: 'en' },
    users: ['alice', 'bob']
  };

  // Compact JSON (default)
  const compact = api.toJSON(config);

  // Pretty-printed JSON
  const pretty = api.toJSON(config, { pretty: true, indent: 2 });

  return { success: true };
}

Options:

Option Type Default Description
pretty boolean false Format with indentation and newlines
indent number 2 Spaces per indent level (when pretty=true)

Storing Data as CSV File

Use api.storeAsCSV() to convert data and store it as a CSV file in one step:

async function run() {
  // Query completed orders for the month
  const orders = await api.queryRecords('orders', {
    filter: { status: 'completed' },
    dateWindow: { field: 'createdAt', from: '2024-01-01', to: '2024-01-31' }
  });

  // Store as CSV file
  const result = await api.storeAsCSV(orders.items, 'january-orders.csv', {
    folder: '/exports/monthly',
    isPublic: false,
    headers: ['orderNumber', 'customerEmail', 'total', 'createdAt']
  });

  if (result.success) {
    api.log(`CSV exported: ${result.fileUrl}`);

    // Save the file reference to a record
    await api.createRecord('export-logs', {
      type: 'monthly-orders',
      fileId: result.renderId,
      fileUrl: result.fileUrl,
      rowCount: orders.items.length,
      exportedAt: new Date().toISOString()
    });
  }

  return { success: result.success, fileUrl: result.fileUrl };
}

Options:

Option Type Default Description
folder string "/root/shared" Target folder path
isPublic boolean false Make file publicly accessible
headers string[] Auto-detected Custom column headers
delimiter string "," Field delimiter
includeHeaders boolean true Include header row

Storing Data as JSON File

Use api.storeAsJSON() to convert data and store it as a JSON file:

async function run() {
  // Create a backup of all records
  const products = await api.queryRecords('products', { pageSize: 1000 });
  const categories = await api.queryRecords('categories', { pageSize: 100 });

  const backup = {
    exportedAt: new Date().toISOString(),
    version: '1.0',
    data: {
      products: products.items,
      categories: categories.items
    }
  };

  // Store as pretty-printed JSON
  const result = await api.storeAsJSON(backup, 'daily-backup.json', {
    pretty: true,
    folder: '/backups',
    isPublic: false
  });

  if (result.success) {
    api.log(`Backup created: ${result.renderId}`);
  }

  return { success: result.success, backupId: result.renderId };
}

Options:

Option Type Default Description
folder string "/root/shared" Target folder path
isPublic boolean false Make file publicly accessible
pretty boolean false Format with indentation
indent number 2 Spaces per indent level

Real-World Example: Automated Report Generation

async function run() {
  const { startDate, endDate } = executionParams;

  // Gather report data
  const orders = await api.queryRecords('orders', {
    dateWindow: { field: 'createdAt', from: startDate, to: endDate }
  });

  const aggregates = await api.aggregateRecords('orders', {
    filter: { createdAt: { $gte: startDate, $lte: endDate } },
    operations: {
      sum: ['total', 'tax'],
      count: true
    }
  });

  // Generate CSV export
  const csvResult = await api.storeAsCSV(orders.items, `orders-${startDate}-to-${endDate}.csv`, {
    folder: '/reports/orders',
    headers: ['orderNumber', 'customerName', 'total', 'tax', 'status', 'createdAt']
  });

  // Generate JSON summary
  const summary = {
    period: { start: startDate, end: endDate },
    metrics: {
      totalOrders: aggregates.count,
      totalRevenue: aggregates.sum.total,
      totalTax: aggregates.sum.tax
    },
    generatedAt: new Date().toISOString()
  };

  const jsonResult = await api.storeAsJSON(summary, `summary-${startDate}.json`, {
    folder: '/reports/summaries',
    pretty: true
  });

  return {
    success: true,
    data: {
      csvFile: csvResult.fileUrl,
      summaryFile: jsonResult.fileUrl,
      orderCount: orders.items.length
    }
  };
}

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) => {
  // Verify webhook signature using crypto API
  const signature = event.http.headers['x-webhook-signature'];
  const expectedSignature = api.crypto.hmacSha256(
    triggerParams.webhookSecret,
    JSON.stringify(event.http.body)
  );

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

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

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

    case 'payment.failed':
      await api.updateRecord(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

Use api.log() for general logging and api.logError() for error logging:

exports.handler = async (event, context) => {
  // General info logging
  api.log('Function started');
  api.log({
    message: 'Processing request',
    eventData: event.data,
    functionId: context.functionId
  });

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

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

    return {
      success: true,
      data: result,
      logs: ['Processing completed successfully']
    };
  } catch (error) {
    // Error logging - automatically includes workspace and execution context
    api.logError({
      message: error.message,
      stack: error.stack
    });

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

Logging Methods: - api.log(message) - General purpose logging (string or object) - api.logError(message) - Error logging with automatic workspace/execution context

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