Skip to content

Compute Function Code Writing Guide

This guide explains how to write code for Centrali compute functions. Compute functions execute in a secure, sandboxed environment with specific syntax requirements and available APIs.

Table of Contents

  1. Function Structure
  2. Sandbox Environment
  3. Available APIs
  4. Parameters
  5. HTTP Requests
  6. Best Practices
  7. Examples
  8. Restrictions

Function Structure

Required Format

All compute functions must define and export a run() function:

async function run() {
  // Your code here

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

Important: The function must be named run, not main or anything else.

Basic Template

async function run() {
  try {
    // Access trigger parameters
    const config = triggerParams;

    // Access execution parameters
    const input = executionParams;

    // Your business logic
    const result = await processData(input);

    // Return result
    return {
      success: true,
      data: result
    };

  } catch (error) {
    // Handle errors
    api.log({ error: error.message, stack: error.stack });

    return {
      success: false,
      error: error.message
    };
  }
}

function processData(input) {
  // Helper functions can be defined
  return { processed: true };
}

Sandbox Environment

Secure ECMAScript (SES)

Functions execute in a Compartment created by SES (Secure ECMAScript), which provides:

  • Isolated execution: No access to Node.js filesystem, child processes, or system calls
  • Deterministic environment: Predictable behavior across executions
  • Hardened JavaScript: Prevents prototype pollution and other attacks

Available Global Objects

Only the following global objects are available:

// Standard JavaScript globals
Date          // Date manipulation
Math          // Mathematical operations

// Timer functions
setTimeout        // Execute callback after delay
setInterval       // Execute callback repeatedly
clearTimeout      // Cancel a timeout
clearInterval     // Cancel an interval

// Centrali-provided globals
api               // Internal API for data operations and utilities
triggerParams     // Parameters from the trigger configuration
executionParams   // Parameters passed at execution time

Timers

You can use setTimeout and setInterval for delays, polling, and rate limiting.

Important: All timers are automatically cleaned up when your function completes. You don't need to manually clear them unless you want to cancel early.

setTimeout - Execute After Delay

async function run() {
  api.log('Starting...');

  // Wait 2 seconds before continuing
  await new Promise(resolve => {
    setTimeout(() => {
      api.log('2 seconds have passed');
      resolve();
    }, 2000);
  });

  return { success: true, message: 'Completed after delay' };
}

setInterval - Execute Repeatedly

async function run() {
  let count = 0;
  const maxTicks = 5;

  // Poll every second until condition met
  await new Promise(resolve => {
    const intervalId = setInterval(() => {
      count++;
      api.log(`Tick ${count}`);

      if (count >= maxTicks) {
        clearInterval(intervalId);  // Stop the interval
        resolve();
      }
    }, 1000);
  });

  return { success: true, ticks: count };
}

Rate Limiting with Delays

async function run() {
  const items = ['item1', 'item2', 'item3', 'item4', 'item5'];
  const results = [];

  for (const item of items) {
    // Process item
    const result = await api.httpPost(
      triggerParams.apiUrl,
      { data: item },
      { headers: { 'Authorization': `Bearer ${triggerParams.apiKey}` } }
    );
    results.push(result);

    // Rate limit: wait 500ms between requests
    await new Promise(resolve => setTimeout(resolve, 500));
  }

  return { success: true, processed: results.length };
}

Polling with Timeout

async function run() {
  const maxWaitTime = 10000; // 10 seconds
  const pollInterval = 1000; // 1 second
  const startTime = Date.now();

  // Poll until condition is met or timeout
  const result = await new Promise((resolve, reject) => {
    const checkStatus = async () => {
      try {
        const status = await api.httpGet(
          `${triggerParams.statusUrl}/${executionParams.jobId}`,
          { headers: { 'Authorization': `Bearer ${triggerParams.apiKey}` } }
        );

        if (status.completed) {
          resolve(status);
          return;
        }

        // Check timeout
        if (Date.now() - startTime > maxWaitTime) {
          reject(new Error('Polling timeout'));
          return;
        }

        // Poll again after interval
        setTimeout(checkStatus, pollInterval);
      } catch (error) {
        reject(error);
      }
    };

    checkStatus();
  });

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

Timer Notes: - Timers are cleared automatically when your function completes - Use clearTimeout() and clearInterval() to cancel timers early - Be mindful of execution time limits (30 second ack timeout) - Avoid creating many long-running intervals

NOT Available

The following are not available in the sandbox:

  • require() or import statements
  • Node.js built-in modules (fs, path, child_process, crypto, etc.)
  • process object
  • Buffer object
  • global or globalThis (use provided globals only)
  • Direct network access (use api.httpGet() instead)
  • File system access
  • External npm packages (except those exposed through api)

Available APIs

The api object provides access to Centrali data operations and utilities.

Record Operations

Query Records

Search and filter records with pagination and sorting.

async function run() {
  const results = await api.queryRecords('orders', {
    filter: { status: 'pending' },
    search: 'john',
    searchFields: ['data.customerName', 'data.email'],
    page: 1,
    pageSize: 50,
    sort: [{ field: 'createdAt', direction: 'desc' }],
    includeDeleted: false,
    includeTotal: true,
    dateWindow: {
      field: 'createdAt',
      from: '2025-01-01',
      to: '2025-01-31'
    }
  });

  api.log(`Found ${results.items.length} records`);
  api.log(`Total: ${results.total}`);

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

Options: - filter: Object with field filters (e.g., { status: 'active', 'data.age': { $gte: 18 } }) - search: Text search query - searchFields: Array of fields to search in - page: Page number (1-based) - pageSize: Results per page (default: 50) - sort: Array of { field, direction } objects - includeDeleted: Include soft-deleted records - includeTotal: Return total count - dateWindow: Filter by date range


Aggregate Records

Perform sum, average, min, max, and count operations.

async function run() {
  const aggregates = await api.aggregateRecords('sales', {
    filter: { 'data.region': 'US' },
    groupBy: ['data.category'],
    operations: {
      totalRevenue: { sum: 'data.amount' },
      avgOrderValue: { avg: 'data.amount' },
      minOrder: { min: 'data.amount' },
      maxOrder: { max: 'data.amount' },
      orderCount: { count: '*' }
    }
  });

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

Fetch Single Record

async function run() {
  const recordId = executionParams.recordId;
  const record = await api.fetchRecord(recordId);

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

Fetch by Unique Field

async function run() {
  const user = await api.fetchRecordByUniqueField(
    'users',
    'email',
    'john@example.com'
  );

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

Create Record

async function run() {
  const newRecord = await api.createRecord('customers', {
    name: 'John Doe',
    email: 'john@example.com',
    age: 30,
    status: 'active'
  });

  api.log({ message: 'Record created', id: newRecord.id });

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

Update Record

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

  const updated = await api.updateRecord(recordId, {
    status: 'completed',
    completedAt: new Date().toISOString()
  });

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

Delete Record

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

  // Hard delete (permanent)
  await api.deleteRecord(recordId, true);

  // Soft delete (marks as deleted)
  // await api.deleteRecord(recordId, false);

  return { success: true, message: 'Record deleted' };
}

Increment Field

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

  // Increment view count by 1
  const updated = await api.incrementField(recordId, 'data.viewCount', 1);

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

Decrement Field

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

  // Decrement stock by 5
  const updated = await api.decrementField(recordId, 'data.stock', 5);

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

Utility Functions

Logging

async function run() {
  // Simple string log
  api.log('Processing started');

  // Structured logging (recommended)
  api.log({
    message: 'Processing order',
    orderId: '12345',
    status: 'pending'
  });

  return { success: true };
}

Logs are visible in the Centrali debug console and function run details.


UUID Generation

async function run() {
  const uniqueId = api.uuid();

  api.log(`Generated ID: ${uniqueId}`);

  return { success: true, id: uniqueId };
}

Date Formatting

async function run() {
  const now = new Date().toISOString();

  // Format using dayjs
  const formatted = api.formatDate(now, 'YYYY-MM-DD HH:mm:ss');
  const readable = api.formatDate(now, 'MMMM D, YYYY');

  return {
    success: true,
    timestamps: {
      iso: now,
      formatted: formatted,
      readable: readable
    }
  };
}

Common Format Patterns: - YYYY-MM-DD → 2025-01-15 - YYYY-MM-DD HH:mm:ss → 2025-01-15 14:30:00 - MMMM D, YYYY → January 15, 2025 - MMM D → Jan 15 - h:mm A → 2:30 PM


Lodash Utilities

A subset of Lodash functions are available:

async function run() {
  const data = [1, 2, 3, 4, 5, 6, 7, 8, 9];

  // Chunk array into groups
  const chunks = api.lodash.chunk(data, 3);
  // [[1,2,3], [4,5,6], [7,8,9]]

  // Flatten nested arrays
  const nested = [[1, 2], [3, 4]];
  const flat = api.lodash.flatten(nested);
  // [1, 2, 3, 4]

  // Get unique values
  const duplicates = [1, 2, 2, 3, 3, 3];
  const unique = api.lodash.uniq(duplicates);
  // [1, 2, 3]

  // Deep merge objects
  const obj1 = { a: 1, b: { c: 2 } };
  const obj2 = { b: { d: 3 }, e: 4 };
  const merged = api.lodash.merge(obj1, obj2);
  // { a: 1, b: { c: 2, d: 3 }, e: 4 }

  return { success: true };
}

Math.js

Perform advanced mathematical operations:

async function run() {
  // Evaluate expressions
  const result = api.math.evaluate('sqrt(16) + 5 * 2');
  // 14

  // With variables
  const calc = api.math.evaluate('x^2 + y', { x: 3, y: 4 });
  // 13

  // Basic operations
  const sum = api.math.add(10, 5);          // 15
  const diff = api.math.subtract(10, 5);    // 5
  const product = api.math.multiply(10, 5); // 50
  const quotient = api.math.divide(10, 5);  // 2

  // Random number
  const random = api.math.random(); // 0 to 1

  return { success: true, calculations: { result, calc, random } };
}

Template Rendering (Handlebars)

Render dynamic templates using Handlebars syntax. Perfect for generating emails, notifications, and formatted content.

async function run() {
  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
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"}} November 27, 2024
formatNumber {{formatNumber 1234.5 2}} 1,234.50
// Currency formatting (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
"{{formatNumber 99.999 2}}"      // 100.00

String Helpers:

Helper Example Output
uppercase {{uppercase "hello"}} HELLO
lowercase {{lowercase "HELLO"}} hello
capitalize {{capitalize "hello world"}} Hello World
truncate {{truncate description 50 "..."}} Lorem ipsum...
"Status: {{uppercase status}}"          // SHIPPED
"{{capitalize customer.name}}"          // Mary Olowu
"{{truncate description 100 '...'}}"    // First 100 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
const template = `
{{#if (eq status "shipped")}}
  Your order has shipped!
{{/if}}

{{#if (and paid (not cancelled))}}
  Payment confirmed
{{/if}}

{{#if (gt items.length 3)}}
  And {{subtract items.length 3}} more items...
{{/if}}
`;

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 data = {
    amount: 125000,
    orderDate: "2024-11-27T10:30:00Z"
  };

  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>
  <thead>
    <tr><th>Product</th><th>Qty</th><th>Price</th></tr>
  </thead>
  <tbody>
    {{#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}}
  </tbody>
</table>

<p><strong>Subtotal:</strong> {{formatCurrency order.subtotal "NGN"}}</p>
{{#if order.discount}}
<p><strong>Discount:</strong> -{{formatCurrency order.discount "NGN"}}</p>
{{/if}}
<p><strong>Shipping:</strong> {{formatCurrency order.shipping "NGN"}}</p>
<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>
<p><a href="{{tracking.url}}">Track Your Package</a></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",
      subtotal: 120000,
      discount: 10000,
      shipping: 5000,
      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: 75000 }
    ],
    tracking: {
      carrier: "DHL",
      number: "1234567890",
      url: "https://dhl.com/track/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

Parameters

Your function receives two types of parameters: trigger parameters (configured when creating the trigger) and execution parameters (passed at runtime when invoking the trigger).

Understanding triggerParams vs executionParams

Parameter Source When Set Use Case
triggerParams Trigger configuration (saved in DB) When creating/updating the trigger Static config: API keys, endpoints, settings
executionParams API request body / Event payload At runtime when trigger executes Dynamic data: record IDs, user input, event data

Trigger Parameters (triggerParams)

Configured when creating the trigger via the API or UI. These are static values that don't change between executions.

async function run() {
  // triggerParams come from the trigger's triggerMetadata.params
  // Example: { apiKey: "secret123", endpoint: "https://api.example.com" }

  const apiKey = triggerParams.apiKey;
  const endpoint = triggerParams.endpoint;

  api.log({ message: 'Using trigger config', endpoint });

  return { success: true };
}

Note: Sensitive parameters (API keys, secrets) are automatically decrypted before function execution.


Execution Parameters (executionParams)

Passed at runtime when the trigger executes. The source depends on trigger type:

Trigger Type executionParams Source
On-Demand Request body when calling /execute endpoint or SDK triggers.invoke()
Event-Driven Event payload (record data, event type, etc.)
Webhook HTTP request body sent to the webhook URL
Scheduled Empty (scheduled triggers typically don't have execution params)

On-Demand Triggers (SDK/API)

When you invoke a trigger via the SDK or API, the payload you pass becomes executionParams:

SDK Call:

// Using the SDK
const result = await centrali.triggers.invoke('trigger-id', {
  payload: {
    orderId: '4f20dbde-167d-4f18-8271-a4b94ab0be85',
    packageType: 'medium',
    adminUserId: 'user_123'
  }
});

curl Request:

curl -X POST ".../function-triggers/{triggerId}/execute" \
  -H "Authorization: Bearer <token>" \
  -H "Content-Type: application/json" \
  -d '{
    "orderId": "4f20dbde-167d-4f18-8271-a4b94ab0be85",
    "packageType": "medium",
    "adminUserId": "user_123"
  }'

In Your Function:

async function run() {
  // The payload is available in executionParams (NOT triggerParams!)
  const orderId = executionParams.orderId;           // "4f20dbde-..."
  const packageType = executionParams.packageType;   // "medium"
  const adminUserId = executionParams.adminUserId;   // "user_123"

  api.log({ message: 'Processing order', orderId, packageType });

  // Fetch the order record
  const order = await api.fetchRecord(orderId);

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

Event-Driven Triggers

For event-driven triggers, executionParams contains the event payload:

async function run() {
  // executionParams contains event data automatically
  const event = executionParams;

  // Event structure:
  // {
  //   event: "record_created",
  //   workspaceSlug: "acme",
  //   recordSlug: "orders",
  //   recordId: "rec-123",
  //   data: { ... },
  //   timestamp: "2025-01-15T10:30:00.000Z"
  // }

  api.log({
    message: `Processing ${event.event} for ${event.recordSlug}`,
    recordId: event.recordId
  });

  return { success: true };
}

Common Mistake: Using the Wrong Parameter

Wrong - Looking for payload in triggerParams:

async function run() {
  // This won't work! The runtime payload is in executionParams
  const orderId = triggerParams.orderId;  // undefined!
  return { success: false, error: 'orderId not found' };
}

Correct - Using executionParams for runtime data:

async function run() {
  // Runtime payload from SDK/API is in executionParams
  const orderId = executionParams.orderId;  // "4f20dbde-..."
  return { success: true, orderId };
}


Combining Both Parameters

A typical pattern is to use triggerParams for configuration and executionParams for runtime data:

async function run() {
  // Static config from trigger setup
  const apiEndpoint = triggerParams.apiEndpoint;
  const apiKey = triggerParams.apiKey;

  // Dynamic data from runtime invocation
  const orderId = executionParams.orderId;
  const action = executionParams.action;

  if (!orderId) {
    return { success: false, error: 'orderId is required in payload' };
  }

  // Use both: config for API, runtime data for what to process
  const result = await api.httpPost(
    `${apiEndpoint}/orders/${orderId}/${action}`,
    { timestamp: new Date().toISOString() },
    { headers: { 'Authorization': `Bearer ${apiKey}` } }
  );

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

HTTP Requests

Domain Allowlist

HTTP requests are restricted to allowed domains only. Domains must be configured in your workspace settings.

Example: If api.example.com is in the allowlist:

async function run() {
  try {
    // This will work
    const data = await api.httpGet('https://api.example.com/data');
    return { success: true, data };

  } catch (error) {
    // "Domain not allowed" error if domain not in allowlist
    return { success: false, error: error.message };
  }
}

HTTP GET

async function run() {
  const response = await api.httpGet(
    'https://api.example.com/users/123',
    {
      headers: {
        'Authorization': `Bearer ${triggerParams.apiToken}`,
        'Accept': 'application/json'
      },
      timeout: 5000
    }
  );

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

HTTP POST

async function run() {
  const response = await api.httpPost(
    'https://api.example.com/orders',
    {
      customerId: executionParams.customerId,
      items: [{ id: 'item1', quantity: 2 }],
      total: 99.99
    },
    {
      headers: {
        'Authorization': `Bearer ${triggerParams.apiToken}`,
        'Content-Type': 'application/json'
      }
    }
  );

  return { success: true, orderId: response.id };
}

HTTP PUT

async function run() {
  const response = await api.httpPut(
    `https://api.example.com/orders/${executionParams.orderId}`,
    {
      status: 'shipped',
      trackingNumber: '1Z999AA10123456784'
    },
    {
      headers: {
        'Authorization': `Bearer ${triggerParams.apiToken}`
      }
    }
  );

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

HTTP DELETE

async function run() {
  await api.httpDelete(
    `https://api.example.com/cache/${executionParams.key}`,
    {
      headers: {
        'Authorization': `Bearer ${triggerParams.apiToken}`
      }
    }
  );

  return { success: true, message: 'Deleted successfully' };
}

Best Practices

1. Error Handling

Always wrap your code in try-catch blocks:

async function run() {
  try {
    const result = await performOperation();
    return { success: true, data: result };

  } catch (error) {
    api.log({
      level: 'error',
      message: error.message,
      stack: error.stack,
      context: executionParams
    });

    return {
      success: false,
      error: error.message
    };
  }
}

async function performOperation() {
  // Your logic
  return { result: 'done' };
}

2. Input Validation

Validate all parameters before use:

async function run() {
  // Validate required execution params
  if (!executionParams.recordId) {
    return { success: false, error: 'recordId is required' };
  }

  // Validate types
  if (typeof executionParams.amount !== 'number') {
    return { success: false, error: 'amount must be a number' };
  }

  // Validate ranges
  if (executionParams.amount < 0) {
    return { success: false, error: 'amount must be positive' };
  }

  // Process with validated inputs
  const record = await api.fetchRecord(executionParams.recordId);
  return { success: true, data: record };
}

3. Structured Logging

Use structured logging for better debugging:

async function run() {
  api.log({ stage: 'start', params: executionParams });

  try {
    const result = await processData();
    api.log({ stage: 'success', result });

    return { success: true, data: result };

  } catch (error) {
    api.log({
      stage: 'error',
      error: error.message,
      stack: error.stack
    });

    return { success: false, error: error.message };
  }
}

async function processData() {
  return { processed: true };
}

4. Batch Processing

Process records in batches to avoid memory issues:

async function run() {
  const BATCH_SIZE = 100;
  let page = 1;
  let processed = 0;

  while (true) {
    const results = await api.queryRecords('orders', {
      filter: { status: 'pending' },
      page: page,
      pageSize: BATCH_SIZE
    });

    if (results.items.length === 0) break;

    for (const order of results.items) {
      await api.updateRecord(order.id, {
        status: 'processing',
        processedAt: new Date().toISOString()
      });
      processed++;
    }

    api.log({ page, processed: results.items.length });
    page++;
  }

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

5. Return Consistent Format

Always return a consistent result format:

async function run() {
  try {
    // Success case
    return {
      success: true,
      data: {...},
      metadata: {
        processedAt: new Date().toISOString(),
        version: '1.0'
      }
    };

  } catch (error) {
    // Error case
    return {
      success: false,
      error: error.message,
      metadata: {
        failedAt: new Date().toISOString()
      }
    };
  }
}

Examples

Example 1: Order Processing

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

    if (!orderId) {
      return { success: false, error: 'orderId required' };
    }

    // Fetch the order
    const order = await api.fetchRecord(orderId);

    if (order.data.status !== 'pending') {
      return { success: false, error: 'Order not pending' };
    }

    // Process payment via external API
    const payment = await api.httpPost(
      triggerParams.paymentApiUrl,
      {
        orderId: order.id,
        amount: order.data.total,
        customerId: order.data.customerId
      },
      {
        headers: {
          'Authorization': `Bearer ${triggerParams.paymentApiKey}`
        }
      }
    );

    // Update order status
    const updated = await api.updateRecord(orderId, {
      status: 'paid',
      paymentId: payment.id,
      paidAt: new Date().toISOString()
    });

    api.log({
      message: 'Order processed',
      orderId: orderId,
      paymentId: payment.id
    });

    return {
      success: true,
      data: updated
    };

  } catch (error) {
    api.log({ error: error.message });
    return { success: false, error: error.message };
  }
}

Example 2: Data Synchronization

async function run() {
  try {
    // Fetch records created in last hour
    const oneHourAgo = new Date(Date.now() - 3600000).toISOString();

    const records = await api.queryRecords('products', {
      dateWindow: {
        field: 'createdAt',
        from: oneHourAgo
      },
      pageSize: 100
    });

    api.log({ message: 'Found records', count: records.items.length });

    // Sync to external system
    const synced = [];
    for (const record of records.items) {
      try {
        const result = await api.httpPost(
          `${triggerParams.syncEndpoint}/products`,
          {
            externalId: record.id,
            name: record.data.name,
            price: record.data.price,
            stock: record.data.stock
          },
          {
            headers: {
              'X-API-Key': triggerParams.syncApiKey
            }
          }
        );

        synced.push(record.id);

      } catch (error) {
        api.log({
          error: 'Sync failed',
          recordId: record.id,
          message: error.message
        });
      }
    }

    return {
      success: true,
      data: {
        total: records.items.length,
        synced: synced.length
      }
    };

  } catch (error) {
    return { success: false, error: error.message };
  }
}

Example 3: Analytics Aggregation

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

    // Aggregate sales data
    const aggregates = await api.aggregateRecords('sales', {
      filter: {
        'data.status': 'completed'
      },
      dateWindow: {
        field: 'createdAt',
        from: startDate,
        to: endDate
      },
      groupBy: groupBy ? [groupBy] : undefined,
      operations: {
        totalRevenue: { sum: 'data.amount' },
        avgOrderValue: { avg: 'data.amount' },
        orderCount: { count: '*' },
        maxOrder: { max: 'data.amount' },
        minOrder: { min: 'data.amount' }
      }
    });

    // Format results
    const formatted = aggregates.map(group => ({
      ...group,
      formattedRevenue: `$${group.totalRevenue.toFixed(2)}`,
      formattedAvg: `$${group.avgOrderValue.toFixed(2)}`
    }));

    api.log({
      message: 'Analytics generated',
      groups: formatted.length
    });

    return {
      success: true,
      data: formatted,
      summary: {
        period: { startDate, endDate },
        generatedAt: new Date().toISOString()
      }
    };

  } catch (error) {
    return { success: false, error: error.message };
  }
}

Example 4: Data Validation & Enrichment

async function run() {
  try {
    const recordId = executionParams.recordId;
    const record = await api.fetchRecord(recordId);

    const errors = [];
    const warnings = [];

    // Validate email
    const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
    if (!emailRegex.test(record.data.email)) {
      errors.push('Invalid email format');
    }

    // Validate age
    if (record.data.age < 18) {
      warnings.push('User is under 18');
    }

    // Validate phone
    const phoneRegex = /^\+?[\d\s-()]+$/;
    if (!phoneRegex.test(record.data.phone)) {
      errors.push('Invalid phone format');
    }

    if (errors.length > 0) {
      await api.updateRecord(recordId, {
        validationStatus: 'failed',
        validationErrors: errors
      });

      return {
        success: false,
        errors,
        warnings
      };
    }

    // Enrich data with external API
    const enrichment = await api.httpGet(
      `${triggerParams.enrichmentApi}/lookup?email=${record.data.email}`,
      {
        headers: {
          'Authorization': `Bearer ${triggerParams.enrichmentKey}`
        }
      }
    );

    // Update with enriched data
    await api.updateRecord(recordId, {
      validationStatus: 'passed',
      enrichedData: {
        company: enrichment.company,
        location: enrichment.location,
        socialProfiles: enrichment.social
      },
      enrichedAt: new Date().toISOString()
    });

    return {
      success: true,
      warnings,
      enriched: true
    };

  } catch (error) {
    return { success: false, error: error.message };
  }
}

Restrictions

What You Cannot Do

  1. No File System Access
  2. Cannot read or write files
  3. Cannot access local directories

  4. No System Commands

  5. Cannot spawn child processes
  6. Cannot execute shell commands

  7. No Direct Network Access

  8. Must use api.httpGet(), api.httpPost(), etc.
  9. Cannot use fetch(), axios, or other HTTP libraries directly

  10. No External Packages

  11. Cannot use require() or import
  12. Cannot install or use npm packages
  13. Use provided utilities (api.lodash, api.math)

  14. No Prototype Modification

  15. Cannot modify built-in prototypes
  16. SES prevents Object.prototype pollution

  17. No Infinite Loops

  18. Functions have execution time limits
  19. Design for efficient, bounded execution

Domain Allowlist

HTTP requests are restricted to allowed domains configured in workspace settings. Contact your administrator to add domains to the allowlist.

Error Messages

Common Errors

"Domain not allowed" - The URL you're trying to access is not in the allowlist - Solution: Add the domain to your workspace's allowed domains

"ReferenceError: require is not defined" - You're trying to use require() which is not available - Solution: Use the api object for external operations

"ReferenceError: process is not defined" - You're trying to access the process object - Solution: Use Date for timestamps, api.log() for logging

"run is not a function" - Your function is not named run - Solution: Ensure you have async function run() { ... }

Getting Help

  • Check the Centrali debug console for execution logs
  • Review function run details for error messages and stack traces
  • Contact your workspace administrator for domain allowlist changes
  • Refer to the Data Service API documentation for trigger configuration