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¶
- Function Structure
- Sandbox Environment
- Available APIs
- Parameters
- HTTP Requests
- Best Practices
- Examples
- Restrictions
Function Structure¶
Required Format¶
All compute functions must define and export a run() function:
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()orimportstatements- Node.js built-in modules (
fs,path,child_process,crypto, etc.) processobjectBufferobjectglobalorglobalThis(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:
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 <script>
// 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¶
- No File System Access
- Cannot read or write files
-
Cannot access local directories
-
No System Commands
- Cannot spawn child processes
-
Cannot execute shell commands
-
No Direct Network Access
- Must use
api.httpGet(),api.httpPost(), etc. -
Cannot use
fetch(),axios, or other HTTP libraries directly -
No External Packages
- Cannot use
require()orimport - Cannot install or use npm packages
-
Use provided utilities (
api.lodash,api.math) -
No Prototype Modification
- Cannot modify built-in prototypes
-
SES prevents
Object.prototypepollution -
No Infinite Loops
- Functions have execution time limits
- 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