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¶
- Use triggerParams for:
- API credentials
- Service endpoints
- Feature flags
- Default configurations
-
Retry settings
-
Use executionParams for:
- Record/entity IDs
- User inputs
- Transaction data
- Timestamps
-
Event payloads
-
Security:
- Always encrypt sensitive triggerParams
- Never log sensitive parameters
-
Validate executionParams before use
-
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:
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 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:
- Always check the result -
api.storeFile()returns{ success: false, error: "..." }on failure - Use appropriate encoding - Use "base64" for binary files (PDFs, images), "utf8" for text files
- Store the renderId - Save the
renderIdin your records for future reference - Choose visibility carefully - Use
isPublic: falsefor sensitive documents - Organize with folders - Use meaningful folder paths for easier file management
- 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¶
-
Minimize Cold Starts
-
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 } }; }; -
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 }; }; -
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¶
-
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 }; }; -
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 }; }; -
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¶
- Function Triggers - Automate function execution
- API Reference - Complete API documentation
- Examples - Real-world examples
- Query Guide - Learn about querying data