Technical Reference
Automations

Automations — Technical Reference

This page covers the data models, backend execution engine, safety mechanisms, and security policies behind the Automations module.


Data Model

automations

Stores centralized automation rules mapping triggers to actions.

ColumnTypeDescription
iduuidPrimary key
workspace_iduuidTenant isolation — FK → workspaces.id (ON DELETE CASCADE)
nametextHuman-readable label for the rule
trigger_typetextThe category of triggering event
trigger_idtextSpecific identifier of the firing entity (or __any__)
action_typetextThe category of action to execute
action_idtextSpecific target entity identifier (or generated UUID)
action_configjsonbConfiguration blueprint for tasks/notifications (defaults to '{}')
is_activebooleanFlag to enable or disable the rule
created_attimestamptzRecord creation timestamp
updated_attimestamptzLast updated timestamp

Triggers

The server-side processAutomationTrigger function accepts trigger payloads and queries active workspace rules.

Trigger Types and Call Sites

Trigger Typetrigger_typeCalled From
Form Submittedform_submission/api/forms/[id] or /api/forms/verify
Event Registrationevent_registration/api/rsvp/[id] or /api/webhooks/eventbrite
Product Purchasedproduct_purchase/api/webhooks/stripe
Product Refundedproduct_refunded/api/webhooks/stripe
Tag Appliedtag_appliedContact tag operations or cascading actions
Tag Removedtag_removedContact tag operations or cascading actions
Deal Stage Changeddeal_stage_changedDeal update server actions
Deal Status Changeddeal_status_changedDeal update server actions
Contact Birthdaycontact_birthday/api/cron/birthday-sweeper

Context-Aware Double Opt-In

For form_submission triggers, existing contact updates are staged in pending_form_submissions to prevent unauthorized overwrites. The trigger only fires after verification is completed via the /api/forms/verify endpoint. New contacts are created and fire the trigger immediately.

Two-Tier Deal Trigger Patterns

Deal triggers represent pipeline actions using specific syntax patterns in the trigger_id column:

Selected Scopetrigger_id Pattern
Any Pipeline / Any Stage (or Status)__any__
Any Pipeline / Won or Lost__any___won or __any___lost
Specific Pipeline / Any Stage{pipelineId}__any_stage__
Specific Pipeline / Any Status{pipelineId}__any_status__
Specific Pipeline / Specific Stage{stageId}
Specific Pipeline / Won or Lost{pipelineId}_won or {pipelineId}_lost

Actions

Idempotent Tag Actions

Tag changes use database constraints to ensure idempotency and prevent duplicate cascade loops:

  • Add Tag: Insert uses ON CONFLICT (contact_id, tag_id) DO NOTHING. If no row is modified, the cascading tag_applied trigger is skipped.
  • Remove Tag: Checks the count of deleted rows. If 0, the cascading tag_removed trigger is skipped.

Campaign Enrollment Checks

Enrollment via automations applies safety checks:

  1. Active Check: Aborts if the contact has an active or processing enrollment in the campaign.
  2. Trigger Deduplication: If trigger_event_id matches a completed enrollment, the send is aborted.
  3. Status Check: The target campaign's status must equal 'active'.

Task Action Configuration

For create_task actions, action_config stores the task template:

{
  "title": "Follow up with new lead",
  "description": "Review details and send email",
  "assigned_to": "user-uuid-or-null",
  "due_offset_hours": 48
}

Agreement Action Configuration

For send_agreement actions, action_config stores the template selection (with a fallback to action_id):

{
  "template_id": "template-uuid"
}

Agreement Action Deduplication

send_agreement actions evaluate the contact's existing relationship state before dispatching:

Contact's Existing Agreement StateAutomation Engine Action
Signed (Current Version)Silently skipped.
Signed (Older Version)Creates a new pending agreement for the current version.
PendingResends the active signing token (acts as a reminder).
No RecordCreates a new pending agreement.

Notification Action Configuration

For send_notification actions, recipients are configured inside the action_config block:

{
  "member_ids": ["user-uuid-1", "user-uuid-2"],
  "external_emails": ["partner@example.com"]
}

Email Context Resolution & Realignment

When executing the notification action, the system splits delivery into two distinct pathways:

  • External Targets: Sends emails directly to addresses in the external_emails array via the Resend SMTP engine.
  • Team Members: Delegates delivery for users listed in the member_ids array to the central server-side dispatch engine, routing through in-app and email channels based on their notification_preferences.
  • Dynamic Context Resolution:
    • Product Purchased (product_purchase): Queries the transactions table for the most recent record matching the contact and product. It retrieves the actual transaction amount_paid (rendering it as Amount Paid) instead of using the product catalog price_cents, ensuring accuracy for products with multiple prices.
    • Task Context Collection: During task-based triggers, the engine dynamically captures the task ID, contact details, due dates, and action links, formatting them in a clean, bordered context table (detailRows) inside fallback email alert payloads.
    • Automation Depth Limits: When loop depth limits are exceeded, notifications include lists of the specific trigger and blocked rules to speed up debugging.

Dependency Tracking

To prevent administrators from accidentally breaking workflows when editing or deleting components, the system tracks dependencies between automation rules and workspace components (tags, forms, campaigns, products, events).

Dependency Resolution (getComponentDependencies)

The server action getComponentDependencies(workspaceId, componentType, componentId) retrieves all active rules targeting or triggered by a specific component:

  • Query Strategy: Queries the automations table where is_active = true and trigger_id = componentId or action_id = componentId (using Supabase .or() clause).
  • Triggers: Active rules where trigger_id matches the component ID (the component triggers an action).
  • Targeted By: Active rules where action_id matches the component ID (an automation action targets this component).
  • Wildcard Rules: The dependency query and list count calculations also evaluate wildcard rules (trigger_id = '__any__') matching the corresponding component type.
  • Name Resolution: The utility dynamically queries the database table corresponding to each related component's type (e.g., tags, forms, events, products, campaigns, agreement_templates) to resolve their human-readable names for display. For send_notification actions, it directly returns "internal notification" without querying other tables.

Dependencies Drawer (DependenciesDrawer)

The dependency relationship list is presented to administrators using a shared drawer component:

  • Static Mounting: To ensure entrance/exit transitions execute correctly and are not abruptly cut off when closed, the drawer is statically mounted inside page views (such as Tags, Products, and Deals pages) rather than conditionally rendered.
  • Layout Padding: The content body is styled with horizontal padding (px-4) to align cards and headings cleanly with the header.
  • Focus Suppression: Bypasses Radix UI close button autofocus on open (onOpenAutoFocus={(e) => e.preventDefault()}) to maintain clean styling boundaries.

Loop Protection

Cascading tag actions represent a potential infinite loop hazard (e.g., Tag A triggers Tag B, which triggers Tag A).

Depth Limiter

The processAutomationTrigger function tracks recursion depth. If an automation fires a cascading tag trigger, depth increments. The engine aborts execution when recursion depth exceeds 3 (i.e. depth > 3).

processAutomationTrigger(..., depth = 0)  ← Original Trigger
  → add_tag action → tag_applied
     processAutomationTrigger(..., depth = 1)  ← Cascade 1
       → add_tag action → tag_applied
          processAutomationTrigger(..., depth = 2)  ← Cascade 2
            → add_tag action → tag_applied
               processAutomationTrigger(..., depth = 3)  ← Cascade 3
                 → add_tag action → tag_applied
                    processAutomationTrigger(..., depth = 4)  ← ABORT

Protection Strategies

ScenarioStrategy 1 (Depth Limiter)Strategy 2 (Idempotency)
A → B → A (circular)Stops at depth 4.Stops at step 2 — Tag A is already present, inserting it is a no-op.
A → B → C → D → E (chain)Stops at depth 4.Does not apply — tags are different.
A → A (self-referencing)Stops at depth 4.Stops immediately — Tag A is already present.

Performance & Hydration Optimization

Server/Client Page Split & Lazy Loading

  • Server Component (pages/automations/page.tsx): Pre-fetches workspace rules and user privileges on the server.
  • Client Component (automations-client.tsx): Manages rule activation toggles, rule deletion, and renaming inputs.
  • Dynamic imports: Lazily imports the large CreateAutomationDialog and AutomationDetailDialog components on demand with ssr: false to reduce bundle weight.
  • State gates: Workspace sync gates (initialWorkspaceIdRef) and mount guards (isFirstQueryRef) keep state clean.

Row-Level Security (RLS)

All tables enforce tenant-level isolation using workspace scoping.

TableSELECTINSERTUPDATEDELETE
automationsWorkspace membersAdmin+Admin+Admin+