Documentation Index
Fetch the complete documentation index at: https://docs.thena.ai/llms.txt
Use this file to discover all available pages before exploring further.
Custom object events are published when custom objects are created, updated, or deleted within the platform. These events enable integrations to stay synchronized with custom business data and workflows. These events are delivered through the platform events system.
Developer quickstart
Minimal handler (custom objects only)
app.post("/webhook/platform-events", async (req, res) => {
const event = req.body;
if (!event?.eventId || !event?.eventType?.startsWith("custom-object:")) {
return res.status(200).send("OK");
}
res.status(200).send("OK");
const [, , objectType, action] = event.eventType.split(":");
switch (action) {
case "created":
await onCustomObjectCreated(objectType, event.eventData);
break;
case "updated":
await onCustomObjectUpdated(objectType, event.eventData);
break;
case "deleted":
await onCustomObjectDeleted(objectType, event.eventData);
break;
default:
break;
}
});
Checklist
- Validate
eventData against your configured schema per organization.
- Track field-level changes for analytics and workflow triggers.
- Use idempotency when syncing to CRMs/ERPs to avoid duplicates.
Event structure
Custom object events follow a flexible structure to accommodate various custom object types:
interface CustomObjectEventEnvelope {
eventId: string;
eventType: string; // Custom object specific event type
timestamp: number; // Unix timestamp
orgId: string;
actor: {
id: string;
type: string;
email: string;
};
eventData: unknown; // Flexible payload based on custom object type
metadata?: Record<string, unknown>;
}
Event publishing
Custom object events are published through a dedicated platform publisher service with the following characteristics:
Event attributes
All custom object events include these platform event attributes:
event_name - The specific event type
event_id - Unique event identifier (or generated UUID if not provided)
event_timestamp - Unix timestamp of the event
context_user_id - ID of the user who triggered the event
context_user_type - Type of the user (AGENT, CUSTOMER, etc.)
context_organization_id - Organization ID
Ordering
Where applicable, events are ordered per organization or configured scope.
Event types
Since custom objects are flexible by nature, the specific event types depend on the custom object definitions. However, common patterns include:
Lifecycle events
custom-object:{type}:created
Triggered when a new custom object is created.
Example for a “Deal” custom object:
{
eventType: "custom-object:deal:created",
eventData: {
deal: {
id: "deal-123",
name: "Enterprise Software License",
value: 50000,
stage: "proposal",
accountId: "account-456",
ownerId: "user-789",
customFields: {
"expected_close_date": "2024-03-15",
"deal_source": "inbound"
},
createdAt: "2024-01-15T10:30:00Z",
updatedAt: "2024-01-15T10:30:00Z"
}
}
}
custom-object:{type}:updated
Triggered when a custom object is updated.
Example payload:
{
eventType: "custom-object:deal:updated",
eventData: {
deal: {
// Updated deal data
},
previousDeal: {
// Previous state of the deal
},
changedFields: ["stage", "value"]
}
}
custom-object:{type}:deleted
Triggered when a custom object is deleted.
Example payload:
{
eventType: "custom-object:deal:deleted",
eventData: {
previousDeal: {
// Deleted deal data
}
}
}
Field-specific events
custom-object:{type}:field:{field_name}:changed
Triggered when specific important fields change.
Example for stage changes:
{
eventType: "custom-object:deal:field:stage:changed",
eventData: {
deal: {
// Current deal data
},
fieldChange: {
fieldName: "stage",
previousValue: "proposal",
newValue: "negotiation",
changedAt: "2024-01-16T14:20:00Z"
}
}
}
Integration examples
CRM synchronization
function handleCustomObjectEvent(payload) {
const { eventType, eventData } = payload;
// Parse event type to determine object type and action
const [, , objectType, action] = eventType.split(":");
switch (action) {
case "created":
syncToExternalCRM("create", objectType, eventData);
break;
case "updated":
syncToExternalCRM("update", objectType, eventData);
trackFieldChanges(objectType, eventData.changedFields);
break;
case "deleted":
syncToExternalCRM("delete", objectType, eventData);
break;
default:
// Handle custom field-specific events
if (action === "field") {
handleFieldSpecificEvent(objectType, eventData);
}
}
}
function syncToExternalCRM(action, objectType, data) {
switch (objectType) {
case "deal":
syncDealToCRM(action, data.deal || data.previousDeal);
break;
case "contact":
syncContactToCRM(action, data.contact || data.previousContact);
break;
case "opportunity":
syncOpportunityToCRM(
action,
data.opportunity || data.previousOpportunity,
);
break;
default:
// Generic custom object sync
syncGenericObjectToCRM(action, objectType, data);
}
}
Workflow automation
function handleDealStageChange(payload) {
const { eventData } = payload;
const { deal, fieldChange } = eventData;
if (fieldChange?.fieldName === "stage") {
switch (fieldChange.newValue) {
case "closed_won":
// Deal won workflow
triggerDealWonWorkflow(deal);
createSuccessStoryTask(deal);
notifySuccessTeam(deal);
break;
case "closed_lost":
// Deal lost workflow
triggerDealLostWorkflow(deal);
scheduleFollowUpActivity(deal);
updateLossReasonAnalytics(deal);
break;
case "negotiation":
// Entering negotiation
assignLegalReview(deal);
prepareContractTemplates(deal);
break;
default:
// Standard stage progression
updateDealPipeline(deal);
notifyDealOwner(deal, fieldChange);
}
}
}
function triggerDealWonWorkflow(deal) {
// Create onboarding tasks
createTask({
title: `Onboard new customer: ${deal.name}`,
accountId: deal.accountId,
assignedTo: deal.ownerId,
dueDate: addDays(new Date(), 7),
priority: "high",
});
// Update account status
updateAccount(deal.accountId, {
status: "customer",
lastDealWonDate: new Date(),
totalDealValue: incrementDealValue(deal.value),
});
// Send congratulations
sendCongratulationsEmail(deal.ownerId, deal);
}
Analytics and reporting
function trackCustomObjectMetrics(payload) {
const { eventType, eventData, actor } = payload;
const [, , objectType, action] = eventType.split(":");
// Track object lifecycle metrics
trackEvent(`custom_object_${action}`, {
objectType,
organizationId: payload.orgId,
userId: actor.id,
timestamp: payload.timestamp,
});
// Object-specific metrics
if (objectType === "deal") {
trackDealMetrics(action, eventData);
} else if (objectType === "project") {
trackProjectMetrics(action, eventData);
}
// User activity metrics
updateUserActivityScore(actor.id, {
action: `${objectType}_${action}`,
weight: getActionWeight(action),
timestamp: payload.timestamp,
});
}
function trackDealMetrics(action, eventData) {
const deal = eventData.deal || eventData.previousDeal;
switch (action) {
case "created":
trackMetric("deal_created", {
value: deal.value,
stage: deal.stage,
source: deal.customFields?.deal_source,
});
break;
case "updated":
if (eventData.changedFields?.includes("stage")) {
trackMetric("deal_stage_changed", {
fromStage: eventData.previousDeal?.stage,
toStage: deal.stage,
value: deal.value,
});
}
break;
case "deleted":
trackMetric("deal_deleted", {
stage: deal.stage,
value: deal.value,
reason: eventData.deletionReason,
});
break;
}
}
Real-time notifications
function handleCustomObjectNotifications(payload) {
const { eventType, eventData, actor } = payload;
const [, , objectType] = eventType.split(":");
// Get object watchers/subscribers
const watchers = getObjectWatchers(objectType, getObjectId(eventData));
// Create notifications for each watcher
watchers.forEach((watcher) => {
if (watcher.id !== actor.id) {
// Don't notify the actor
const notification = createObjectNotification(
watcher,
eventType,
eventData,
actor,
);
sendRealtimeNotification(watcher.id, notification);
}
});
// Send team notifications for important events
if (isImportantEvent(eventType, eventData)) {
const team = getObjectTeam(objectType, getObjectId(eventData));
sendTeamNotification(team, eventType, eventData, actor);
}
}
function isImportantEvent(eventType, eventData) {
// Define what constitutes an important event
if (eventType.includes("deleted")) return true;
if (eventType.includes("field:stage:changed")) return true;
if (eventType.includes("created") && eventData.deal?.value > 10000)
return true;
return false;
}
Event processing considerations
Payload flexibility
Since custom objects can have varying structures, event processing should be robust:
function processCustomObjectEvent(payload) {
try {
// Validate basic event structure
validateEventStructure(payload);
// Extract object type from event type
const objectType = extractObjectType(payload.eventType);
// Get object schema for validation
const schema = getObjectSchema(objectType, payload.orgId);
if (schema) {
// Validate against schema
validateEventData(payload.eventData, schema);
}
// Process event
processEvent(payload);
} catch (error) {
handleEventProcessingError(payload, error);
}
}
function validateEventData(eventData, schema) {
// Validate required fields
schema.requiredFields?.forEach((field) => {
if (!hasField(eventData, field)) {
throw new Error(`Missing required field: ${field}`);
}
});
// Validate field types
schema.fields?.forEach((fieldDef) => {
if (hasField(eventData, fieldDef.name)) {
validateFieldType(getField(eventData, fieldDef.name), fieldDef);
}
});
}
Error handling
function handleCustomObjectEventError(payload, error) {
switch (error.type) {
case "INVALID_OBJECT_TYPE":
logError("Unknown custom object type", { payload, error });
// Don't retry for invalid object types
break;
case "SCHEMA_VALIDATION_ERROR":
logError("Event data validation failed", { payload, error });
// May indicate schema changes, alert admin
alertSchemaValidationFailure(payload, error);
break;
case "EXTERNAL_SERVICE_ERROR":
// Retry for external service errors
scheduleRetry(payload, { delay: calculateBackoff() });
break;
default:
logError("Unexpected custom object event error", { payload, error });
scheduleRetry(payload);
}
}
Best practices
- Schema validation: Always validate custom object data against defined schemas
- Event versioning: Consider versioning for custom object event schemas
- Error handling: Implement robust error handling for flexible data structures
- Performance: Monitor processing times for complex custom objects
- Security: Validate permissions for custom object access
- Documentation: Document custom object event schemas for each organization
Configuration
Custom object events can be configured per organization:
// Example configuration
{
"customObjectEvents": {
"enabled": true,
"objectTypes": {
"deal": {
"events": ["created", "updated", "deleted", "stage_changed"],
"fieldEvents": ["stage", "value", "owner"],
"notificationSettings": {
"highValueThreshold": 10000,
"criticalFields": ["stage", "close_date"]
}
},
"project": {
"events": ["created", "updated", "status_changed"],
"fieldEvents": ["status", "budget", "deadline"],
"notificationSettings": {
"criticalFields": ["status", "deadline"]
}
}
}
}
}
This configuration allows organizations to customize which events are published and how they’re processed, providing flexibility while maintaining consistency in the event structure.