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.
| Column | Type | Description |
|---|---|---|
id | uuid | Primary key |
workspace_id | uuid | Tenant isolation — FK → workspaces.id (ON DELETE CASCADE) |
name | text | Human-readable label for the rule |
trigger_type | text | The category of triggering event |
trigger_id | text | Specific identifier of the firing entity (or __any__) |
action_type | text | The category of action to execute |
action_id | text | Specific target entity identifier (or generated UUID) |
action_config | jsonb | Configuration blueprint for tasks/notifications (defaults to '{}') |
is_active | boolean | Flag to enable or disable the rule |
created_at | timestamptz | Record creation timestamp |
updated_at | timestamptz | Last updated timestamp |
Triggers
The server-side processAutomationTrigger function accepts trigger payloads and queries active workspace rules.
Trigger Types and Call Sites
| Trigger Type | trigger_type | Called From |
|---|---|---|
| Form Submitted | form_submission | /api/forms/[id] or /api/forms/verify |
| Event Registration | event_registration | /api/rsvp/[id] or /api/webhooks/eventbrite |
| Product Purchased | product_purchase | /api/webhooks/stripe |
| Product Refunded | product_refunded | /api/webhooks/stripe |
| Tag Applied | tag_applied | Contact tag operations or cascading actions |
| Tag Removed | tag_removed | Contact tag operations or cascading actions |
| Deal Stage Changed | deal_stage_changed | Deal update server actions |
| Deal Status Changed | deal_status_changed | Deal update server actions |
| Contact Birthday | contact_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 Scope | trigger_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 cascadingtag_appliedtrigger is skipped. - Remove Tag: Checks the count of deleted rows. If
0, the cascadingtag_removedtrigger is skipped.
Campaign Enrollment Checks
Enrollment via automations applies safety checks:
- Active Check: Aborts if the contact has an active or processing enrollment in the campaign.
- Trigger Deduplication: If
trigger_event_idmatches a completed enrollment, the send is aborted. - Status Check: The target campaign's
statusmust 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 State | Automation Engine Action |
|---|---|
| Signed (Current Version) | Silently skipped. |
| Signed (Older Version) | Creates a new pending agreement for the current version. |
| Pending | Resends the active signing token (acts as a reminder). |
| No Record | Creates 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_emailsarray via the Resend SMTP engine. - Team Members: Delegates delivery for users listed in the
member_idsarray to the central server-side dispatch engine, routing through in-app and email channels based on theirnotification_preferences. - Dynamic Context Resolution:
- Product Purchased (
product_purchase): Queries thetransactionstable for the most recent record matching the contact and product. It retrieves the actual transactionamount_paid(rendering it asAmount Paid) instead of using the product catalogprice_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.
- Product Purchased (
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
automationstable whereis_active = trueandtrigger_id = componentIdoraction_id = componentId(using Supabase.or()clause). - Triggers: Active rules where
trigger_idmatches the component ID (the component triggers an action). - Targeted By: Active rules where
action_idmatches 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. Forsend_notificationactions, 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) ← ABORTProtection Strategies
| Scenario | Strategy 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
CreateAutomationDialogandAutomationDetailDialogcomponents on demand withssr: falseto 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.
| Table | SELECT | INSERT | UPDATE | DELETE |
|---|---|---|---|---|
automations | Workspace members | Admin+ | Admin+ | Admin+ |