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. Custom object events are published to the AWS_SNS_CUSTOM_OBJECT_TOPIC_ARN topic.

Event structure

Custom object events follow a flexible structure to accommodate various custom object types:
interface CustomObjectSNSEvent {
  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:

Message attributes

All custom object events include these platform message 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

Message grouping

Events use a configurable message group ID (defaults to a generated UUID) to ensure proper ordering and deduplication.

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

  1. Schema validation: Always validate custom object data against defined schemas
  2. Event versioning: Consider versioning for custom object event schemas
  3. Error handling: Implement robust error handling for flexible data structures
  4. Performance: Monitor processing times for complex custom objects
  5. Security: Validate permissions for custom object access
  6. 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.