Trigger Parameters & Payload Shapes¶
Understanding what data your function receives — and where it comes from — is critical to writing correct compute functions. This page documents every parameter shape for every trigger type.
The Two Globals¶
Every compute function has access to two global objects:
| Global | What It Contains | Who Sets It |
|---|---|---|
triggerParams | Static configuration from the trigger definition | You, when creating/updating the trigger |
executionParams | Runtime data that changes per execution | The system or the caller, depending on trigger type |
async function run() {
// Static config — same every time this trigger fires
const apiKey = triggerParams.apiKey;
// Runtime data — different every execution
const recordId = executionParams.recordId;
return { success: true };
}
Common Mistake
The #1 mistake is looking for runtime data in triggerParams. If your function receives undefined for a value you expect, check whether you're reading from the wrong global.
Where triggerParams Comes From¶
triggerParams always comes from triggerMetadata.params in the trigger definition. This is the same regardless of trigger type.
When you create a trigger:
curl -X POST "$CENTRALI_URL/data/workspace/$WORKSPACE/api/v1/function-triggers" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"name": "my-trigger",
"functionId": "YOUR_FUNCTION_ID",
"executionType": "on-demand",
"triggerMetadata": {
"params": {
"webhookUrl": "https://hooks.slack.com/services/...",
"apiKey": {
"value": "sk_live_abc123",
"encrypt": true
},
"batchSize": 100
}
}
}'
In your function, those values appear as:
async function run() {
triggerParams.webhookUrl // "https://hooks.slack.com/services/..."
triggerParams.apiKey // "sk_live_abc123" (auto-decrypted)
triggerParams.batchSize // 100
return { success: true };
}
triggerParams is always the same every time this trigger fires. It only changes if you update the trigger definition.
Where executionParams Comes From¶
This is where it gets interesting. The shape of executionParams depends entirely on the trigger type.
| Trigger Type | executionParams Source | Shape |
|---|---|---|
| Event-driven | The record event (system-generated) | Varies by event type — see below |
| On-demand | The request body you send to /execute | Whatever you pass |
| HTTP | The incoming HTTP request body | Whatever the caller sends |
| Scheduled | Nothing (no caller) | {} empty object |
Event-Driven: executionParams by Event Type¶
Event-driven triggers are the most complex because the shape of executionParams changes depending on the event type. The critical difference is whether data contains a single record or a { before, after } pair.
Quick Reference¶
| Event | data Shape | User Field |
|---|---|---|
record_created | Full record object | createdBy |
record_updated | { before, after } — two full records | updatedBy |
record_deleted | Full record object (as it was before deletion) | deletedBy |
record_restored | { before, after } — two full records | restoredBy |
record_reverted | { before, after } — two full records | revertedBy |
records_bulk_created | No data field — uses recordIds array instead | createdBy |
record_created¶
data is the full record.
// executionParams shape:
{
event: "record_created",
workspaceSlug: "acme-corp",
recordSlug: "orders", // structure slug
recordId: "550e8400-e29b-...",
data: { // ← the full record
id: "550e8400-e29b-...",
workspaceSlug: "acme-corp",
recordSlug: "orders",
data: { // ← your actual field values
orderNumber: "ORD-2026-001",
customerId: "cust_abc123",
status: "pending",
total: 109.97
},
status: "active",
version: 1, // always 1 for new records
createdAt: "2026-01-15T10:30:00.000Z",
updatedAt: "2026-01-15T10:30:00.000Z",
createdBy: "user_xyz789",
updatedBy: "user_xyz789"
},
timestamp: "2026-01-15T10:30:00.123Z",
createdBy: "user_xyz789"
}
Example function:
async function run() {
const { recordId, recordSlug, data } = executionParams;
// Access your field values through data.data
const orderNumber = data.data.orderNumber; // "ORD-2026-001"
const total = data.data.total; // 109.97
const customerId = data.data.customerId; // "cust_abc123"
// Access record metadata
const version = data.version; // 1
const createdAt = data.createdAt; // "2026-01-15T10:30:00.000Z"
// Send notification using triggerParams for config
await api.httpPost(triggerParams.slackWebhookUrl, {
text: `New order ${orderNumber} for $${total}`
});
return { success: true, orderNumber };
}
Why data.data?
The outer data is the full record object (with id, version, status, etc.). Your actual field values live inside data.data. This is consistent with how records are structured throughout the Centrali API.
record_updated¶
data is { before, after } — two full record snapshots.
This is the most powerful event type because you can compare previous and current state to detect exactly what changed.
// executionParams shape:
{
event: "record_updated",
workspaceSlug: "acme-corp",
recordSlug: "orders",
recordId: "550e8400-e29b-...",
data: {
before: { // ← record BEFORE the update
id: "550e8400-e29b-...",
workspaceSlug: "acme-corp",
recordSlug: "orders",
data: {
orderNumber: "ORD-2026-001",
status: "pending",
total: 109.97
},
status: "active",
version: 1,
createdAt: "2026-01-15T10:30:00.000Z",
updatedAt: "2026-01-15T10:30:00.000Z",
createdBy: "user_xyz789",
updatedBy: "user_xyz789"
},
after: { // ← record AFTER the update
id: "550e8400-e29b-...",
workspaceSlug: "acme-corp",
recordSlug: "orders",
data: {
orderNumber: "ORD-2026-001",
status: "shipped", // ← changed
total: 109.97,
trackingNumber: "1Z999AA1" // ← new field
},
status: "active",
version: 2, // ← incremented
createdAt: "2026-01-15T10:30:00.000Z",
updatedAt: "2026-01-15T14:45:00.000Z", // ← updated
createdBy: "user_xyz789",
updatedBy: "user_admin001" // ← who made the update
}
},
timestamp: "2026-01-15T14:45:00.456Z",
updatedBy: "user_admin001"
}
Example function — detecting a status change:
async function run() {
const { recordId, data } = executionParams;
const { before, after } = data;
const oldStatus = before.data.status; // "pending"
const newStatus = after.data.status; // "shipped"
if (oldStatus === newStatus) {
// Status didn't change — skip
return { success: true, skipped: true };
}
api.log({
message: "Order status changed",
orderId: recordId,
from: oldStatus,
to: newStatus
});
// Only notify when shipped
if (newStatus === "shipped") {
await api.httpPost(triggerParams.notificationEndpoint, {
type: "order_shipped",
orderId: recordId,
trackingNumber: after.data.trackingNumber,
customerEmail: after.data.customerEmail
}, {
headers: { "Authorization": `Bearer ${triggerParams.notificationApiKey}` }
});
}
return { success: true, statusChanged: true };
}
Example function — finding all changed fields:
async function run() {
const { data } = executionParams;
const { before, after } = data;
const changedFields = [];
for (const key in after.data) {
if (JSON.stringify(before.data[key]) !== JSON.stringify(after.data[key])) {
changedFields.push({
field: key,
from: before.data[key],
to: after.data[key]
});
}
}
api.log({ message: "Fields changed", changes: changedFields });
return { success: true, changedFields };
}
record_deleted¶
data is the full record as it was before deletion. This is your last chance to read the record's data — once hard-deleted, it's gone.
// executionParams shape:
{
event: "record_deleted",
workspaceSlug: "acme-corp",
recordSlug: "orders",
recordId: "550e8400-e29b-...",
data: { // ← full record before deletion
id: "550e8400-e29b-...",
workspaceSlug: "acme-corp",
recordSlug: "orders",
data: {
orderNumber: "ORD-2026-001",
status: "cancelled",
total: 109.97,
cancellationReason: "Customer requested"
},
status: "active",
version: 3,
createdAt: "2026-01-15T10:30:00.000Z",
updatedAt: "2026-01-15T16:00:00.000Z",
createdBy: "user_xyz789",
updatedBy: "user_admin001"
},
timestamp: "2026-01-15T16:30:00.789Z",
deletedBy: "user_admin001" // ← note: deletedBy, not updatedBy
}
Example function — archiving before deletion:
async function run() {
const { recordId, data, deletedBy } = executionParams;
// Archive to external system before it's gone
await api.httpPost(triggerParams.archiveEndpoint, {
originalId: recordId,
orderNumber: data.data.orderNumber,
archivedData: data.data,
deletedBy,
deletedAt: executionParams.timestamp
}, {
headers: { "X-API-Key": triggerParams.archiveApiKey }
});
api.log({
message: "Order archived after deletion",
orderNumber: data.data.orderNumber
});
return { success: true };
}
record_restored¶
data is { before, after }. before is the archived/deleted state, after is the restored active state.
// executionParams shape:
{
event: "record_restored",
workspaceSlug: "acme-corp",
recordSlug: "orders",
recordId: "550e8400-e29b-...",
data: {
before: {
// ... full record in archived/deleted state
status: "archived", // ← was archived
version: 3
},
after: {
// ... full record after restoration
status: "active", // ← now active again
version: 4 // ← version incremented
}
},
timestamp: "2026-01-16T09:00:00.000Z",
restoredBy: "user_admin001" // ← note: restoredBy
}
Example function:
async function run() {
const { recordId, data, restoredBy } = executionParams;
// Re-sync restored record with external system
await api.httpPost(triggerParams.syncEndpoint, {
action: "restore",
recordId,
data: data.after.data
});
api.log({ message: "Record restored and re-synced", recordId, restoredBy });
return { success: true };
}
record_reverted¶
data is { before, after }. Similar to record_updated, but also includes revertedToVersion.
// executionParams shape:
{
event: "record_reverted",
workspaceSlug: "acme-corp",
recordSlug: "orders",
recordId: "550e8400-e29b-...",
data: {
before: {
// ... record before revert (latest version)
data: { status: "shipped", total: 109.97 },
version: 5
},
after: {
// ... record after revert (content from older version, new version number)
data: { status: "pending", total: 109.97 },
version: 6 // ← new version, NOT the old version number
}
},
timestamp: "2026-01-16T10:00:00.000Z",
revertedBy: "user_admin001",
revertedToVersion: 2 // ← which version's data was restored
}
Example function:
async function run() {
const { recordId, data, revertedBy, revertedToVersion } = executionParams;
api.log({
message: "Record reverted",
recordId,
revertedBy,
revertedToVersion,
fromVersion: data.before.version,
toVersion: data.after.version
});
return { success: true };
}
records_bulk_created¶
Different from all other events. There is no data field. Instead, you get recordIds — an array of UUIDs. This is for performance reasons; bulk operations can create thousands of records.
// executionParams shape:
{
event: "records_bulk_created",
workspaceSlug: "acme-corp",
recordSlug: "orders",
structureId: "123e4567-e89b-...",
recordIds: [ // ← array of IDs, NOT full records
"550e8400-e29b-41d4-a716-446655440001",
"550e8400-e29b-41d4-a716-446655440002",
"550e8400-e29b-41d4-a716-446655440003"
],
count: 3,
timestamp: "2026-01-15T10:30:00.123Z",
createdBy: "user_xyz789",
schemaDiscoveryMode: "strict"
}
Example function:
async function run() {
const { recordIds, count, recordSlug } = executionParams;
api.log({ message: `Bulk created ${count} ${recordSlug} records` });
// Fetch full records if you need the data (be mindful of large batches)
if (count <= 100) {
for (const id of recordIds) {
const record = await api.fetchRecord(id);
// ... process each record
}
}
return { success: true, processedCount: count };
}
No data field
Unlike record_created, bulk events do NOT include the full record data. You must use api.fetchRecord() if you need field values. Individual record_created events are NOT fired for bulk operations.
Failed Events¶
Failed events fire when a record operation fails. They have a simpler shape with an error string.
// record_created_failed
{
event: "record_created_failed",
workspaceSlug: "acme-corp",
recordSlug: "orders",
data: { /* the data that was attempted */ },
timestamp: "2026-01-15T10:30:00.000Z",
createdBy: "user_xyz789",
error: "Validation failed: 'total' must be a number"
}
// record_updated_failed
{
event: "record_updated_failed",
workspaceSlug: "acme-corp",
recordSlug: "orders",
recordId: "550e8400-e29b-...",
data: { /* the update data that was attempted */ },
timestamp: "2026-01-15T10:30:00.000Z",
updatedBy: "user_xyz789",
error: "Record not found"
}
// record_deleted_failed
{
event: "record_deleted_failed",
workspaceSlug: "acme-corp",
recordSlug: "orders",
recordId: "550e8400-e29b-...",
timestamp: "2026-01-15T10:30:00.000Z",
deletedBy: "user_xyz789",
error: "Insufficient permissions"
}
Example function — alert on failures:
async function run() {
const { event, recordSlug, error, timestamp } = executionParams;
await api.httpPost(triggerParams.alertWebhook, {
severity: "warning",
message: `${event} on ${recordSlug}: ${error}`,
timestamp
});
return { success: true };
}
On-Demand: executionParams Shape¶
For on-demand triggers, the request body you send to /execute becomes executionParams directly. There is no wrapping — your JSON body IS executionParams.
What You Send¶
curl -X POST "$CENTRALI_URL/data/workspace/$WORKSPACE/api/v1/function-triggers/TRIGGER_ID/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"orderId": "550e8400-e29b-...",
"action": "recalculate",
"dryRun": false
}'
What Your Function Receives¶
// executionParams is exactly your request body:
{
orderId: "550e8400-e29b-...",
action: "recalculate",
dryRun: false
}
Example Function¶
async function run() {
// Runtime data from the /execute call
const orderId = executionParams.orderId; // "550e8400-e29b-..."
const action = executionParams.action; // "recalculate"
const dryRun = executionParams.dryRun; // false
// Static config from trigger definition
const batchSize = triggerParams.batchSize; // 1000 (set at trigger creation)
const apiKey = triggerParams.apiKey; // "sk_live_..." (auto-decrypted)
if (!orderId) {
return { success: false, error: "orderId is required" };
}
const order = await api.fetchRecord(orderId);
if (dryRun) {
api.log({ message: "Dry run — no changes made", orderId });
return { success: true, dryRun: true, order: order.data };
}
// ... perform actual work
return { success: true };
}
SDK Equivalent¶
const result = await centrali.invokeFunction('my-trigger-name', {
orderId: '550e8400-e29b-...',
action: 'recalculate',
dryRun: false
});
// The object you pass becomes executionParams in the function
You define the shape
Unlike event-driven triggers where the system defines the shape, on-demand executionParams is whatever you decide to send. Document your expected shape clearly for anyone calling your function.
HTTP Trigger: executionParams Shape¶
For HTTP triggers, the incoming HTTP request body is passed as executionParams. This is what the external service (Stripe, GitHub, etc.) sends to your trigger URL.
What the External Service Sends¶
# Stripe sends a webhook to your HTTP trigger URL:
POST /data/workspace/acme/api/v1/http-trigger/payments/stripe
{
"id": "evt_1234567890",
"type": "payment_intent.succeeded",
"data": {
"object": {
"id": "pi_abc123",
"amount": 10997,
"currency": "usd",
"customer": "cus_xyz789"
}
}
}
What Your Function Receives¶
// executionParams is the request body:
{
id: "evt_1234567890",
type: "payment_intent.succeeded",
data: {
object: {
id: "pi_abc123",
amount: 10997,
currency: "usd",
customer: "cus_xyz789"
}
}
}
Example Function — Stripe Webhook¶
async function run() {
const event = executionParams;
api.log({ message: "Stripe webhook received", type: event.type });
switch (event.type) {
case "payment_intent.succeeded": {
const payment = event.data.object;
// Find the order by Stripe payment ID
const orders = await api.queryRecords("orders", {
filter: { "data.stripePaymentId": payment.id }
});
if (orders.data.length > 0) {
await api.updateRecord(orders.data[0].id, {
paymentStatus: "paid",
paidAmount: payment.amount / 100,
paidAt: new Date().toISOString()
});
}
break;
}
case "charge.refunded": {
const refund = event.data.object;
// ... handle refund
break;
}
default:
api.log({ message: "Unhandled event type", type: event.type });
}
return { success: true };
}
Example Function — GitHub Webhook¶
async function run() {
const payload = executionParams;
// Use the signing secret from triggerParams to verify
// (configured as encrypted param on trigger)
const secret = triggerParams.githubWebhookSecret;
if (payload.action === "opened" && payload.pull_request) {
const pr = payload.pull_request;
await api.createRecord("pull-requests", {
number: pr.number,
title: pr.title,
author: pr.user.login,
repo: payload.repository.full_name,
url: pr.html_url,
status: "open",
createdAt: pr.created_at
});
api.log({ message: "PR tracked", number: pr.number });
}
return { success: true };
}
Scheduled Trigger: executionParams Shape¶
Scheduled triggers have no caller, so executionParams is an empty object. All your configuration should be in triggerParams.
What Your Function Receives¶
Example Function¶
async function run() {
// No executionParams to read — use triggerParams for everything
const reportType = triggerParams.reportType; // "daily_sales"
const recipients = triggerParams.recipients; // ["sales@acme.com"]
const emailApiKey = triggerParams.emailApiKey; // auto-decrypted
// Generate report
const yesterday = new Date();
yesterday.setDate(yesterday.getDate() - 1);
const sales = await api.aggregateRecords("orders", {
dateWindow: {
field: "createdAt",
from: yesterday.toISOString(),
to: new Date().toISOString()
},
operations: {
totalRevenue: { sum: "data.total" },
orderCount: { count: "*" }
}
});
// Send report email
for (const recipient of recipients) {
await api.httpPost("https://api.resend.com/emails", {
from: "reports@acme.com",
to: recipient,
subject: `Daily Sales Report — ${api.formatDate(yesterday, "MMMM D, YYYY")}`,
html: `<p>Orders: ${sales[0].orderCount}</p><p>Revenue: $${sales[0].totalRevenue.toFixed(2)}</p>`
}, {
headers: { "Authorization": `Bearer ${emailApiKey}` }
});
}
return { success: true, orderCount: sales[0].orderCount };
}
Put everything in triggerParams
Since scheduled triggers have no executionParams, store all configuration (endpoints, API keys, filters, recipients) in the trigger's triggerMetadata.params.
Full Comparison: All Trigger Types Side by Side¶
| Event-Driven | On-Demand | HTTP | Scheduled | |
|---|---|---|---|---|
triggerParams source | triggerMetadata.params | triggerMetadata.params | triggerMetadata.params | triggerMetadata.params |
executionParams source | System (event payload) | Your /execute request body | External HTTP request body | Empty {} |
executionParams shape | Varies by event type | Whatever you send | Whatever the caller sends | {} |
Has event field? | Yes | No | No | No |
Has recordId? | Yes (except bulk) | Only if you send it | Only if caller sends it | No |
Has data? | Yes — record or {before, after} | Only if you send it | Only if caller sends it | No |
Pattern: One Function, Multiple Trigger Types¶
When the same function is attached to multiple trigger types, use executionParams to detect which type fired:
async function run() {
// Event-driven triggers always have an 'event' field
if (executionParams.event) {
return await handleEvent(executionParams);
}
// On-demand triggers will have whatever you defined
if (executionParams.action) {
return await handleManual(executionParams);
}
// Scheduled triggers have empty executionParams
if (Object.keys(executionParams).length === 0) {
return await handleScheduled();
}
// HTTP triggers — check for external webhook signatures
if (executionParams.type && executionParams.data?.object) {
return await handleWebhook(executionParams);
}
return { success: false, error: "Unknown trigger source" };
}
async function handleEvent(params) {
const { event, recordId, data } = params;
if (event === "record_created") {
// data is the full record
api.log({ message: "New record", id: recordId, fields: data.data });
} else if (event === "record_updated") {
// data is { before, after }
api.log({ message: "Updated", from: data.before.version, to: data.after.version });
} else if (event === "record_deleted") {
// data is the full record before deletion
api.log({ message: "Deleted", id: recordId, lastState: data.data });
}
return { success: true, source: "event", event };
}
async function handleManual(params) {
api.log({ message: "Manual execution", action: params.action });
return { success: true, source: "on-demand" };
}
async function handleScheduled() {
api.log({ message: "Scheduled execution" });
return { success: true, source: "scheduled" };
}
async function handleWebhook(payload) {
api.log({ message: "Webhook received", type: payload.type });
return { success: true, source: "http" };
}
Common Mistakes¶
Mistake 1: Accessing data.data on an update event¶
// ❌ WRONG — record_updated has { before, after }, not a direct record
const name = executionParams.data.data.name; // TypeError!
// ✅ CORRECT
const name = executionParams.data.after.data.name;
Mistake 2: Expecting data on bulk events¶
// ❌ WRONG — bulk events don't have a data field
const records = executionParams.data; // undefined!
// ✅ CORRECT — use recordIds and fetch individually
const ids = executionParams.recordIds;
for (const id of ids) {
const record = await api.fetchRecord(id);
}
Mistake 3: Expecting executionParams on scheduled triggers¶
// ❌ WRONG — scheduled triggers have empty executionParams
const reportType = executionParams.reportType; // undefined!
// ✅ CORRECT — use triggerParams for scheduled trigger config
const reportType = triggerParams.reportType;
Mistake 4: Putting runtime data in triggerParams¶
// ❌ WRONG — triggerParams is static, set at trigger creation
// You can't change it per execution
const orderId = triggerParams.orderId; // This is the same every time!
// ✅ CORRECT — use executionParams for per-execution data
const orderId = executionParams.orderId;
Mistake 5: Forgetting that deletedBy / restoredBy are top-level¶
// ❌ WRONG — looking for the user in the wrong place
const who = executionParams.data.deletedBy; // undefined
// ✅ CORRECT — it's a top-level field on executionParams
const who = executionParams.deletedBy; // "user_admin001"
Related Documentation¶
- Triggers — Trigger types, when to use each, and best practices
- Compute Functions — Creating and managing functions
- Writing Functions — Function code API reference
- Function Triggers API — API endpoints for creating/managing triggers
- Event Payloads Reference — Full TypeScript type definitions for all events