Automations
Architecture, database schema, API endpoints, and configuration for the AI automations system
Automations
Technical reference for the AI-powered automations system. This documents the three-pillar architecture, database schema, trigger evaluation, AI generation pipeline, and API endpoints.
Architecture Overview
The automations system has three pillars:
Rule Templates (global) Rules (per-workspace) Outbox (unified queue)
┌──────────────────────┐ ┌───────────────────┐ ┌──────────────────────┐
│ Admin-managed │───▶│ Adopted from │────▶│ proposal │
│ definitions │ │ templates or │ │ → generating │
│ (seed defaults) │ │ custom per-ws │ │ → generated │
└──────────────────────┘ └───────────────────┘ │ → approved / skipped │
└──────────────────────┘Deferred AI economics: Proposals are created with zero AI cost when listing events (status changes or price changes) are detected during sync. AI generation only happens when the agent clicks "Generate" in the dashboard. This means a workspace with 100 events per day does not incur 100 API calls — only the ones the agent actually wants.
Database Schema
automation_rule_templates
Global rule templates managed by platform admins.
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
name | TEXT | Template name (e.g., "Just Sold Post") |
description | TEXT | Human-readable description |
category | TEXT | social, email, or notification |
trigger_type | TEXT | status_change, price_change, new_listing, lead_inquiry, lead_idle, scheduled |
trigger_config | JSONB | Trigger-specific config (e.g., {"toStatuses": ["Closed"]}) |
action_type | TEXT | social_post, email_draft, or notification |
action_config | JSONB | Action-specific config (e.g., {"platforms": ["instagram", "facebook", "linkedin"]}) |
prompt_template | TEXT | Custom prompt template (nullable, uses default if null) |
tone | TEXT | Default tone (professional) |
is_active | BOOLEAN | Whether template is available for adoption |
created_at | TIMESTAMPTZ | Creation timestamp |
updated_at | TIMESTAMPTZ | Last update timestamp |
RLS enabled (service role only).
automation_rules
Per-workspace rules, derived from templates or created custom.
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
workspace_id | UUID | FK to workspaces (CASCADE on delete) |
template_id | UUID | FK to automation_rule_templates (SET NULL on delete) |
name | TEXT | Rule name |
description | TEXT | Rule description |
trigger_type | TEXT | Same enum as templates |
trigger_config | JSONB | Trigger configuration |
action_type | TEXT | Same enum as templates |
action_config | JSONB | Action configuration |
prompt_template | TEXT | Custom prompt (nullable) |
tone | TEXT | Tone for generation (default professional) |
model_preference | TEXT | Override AI model (nullable) |
is_enabled | BOOLEAN | Whether rule is active |
priority | INT | Evaluation priority (default 0) |
cooldown_hours | INT | Hours between duplicate triggers (default 24) |
Index: (workspace_id, is_enabled). RLS enabled (service role only).
automation_outbox
The unified queue of AI-drafted content.
| Column | Type | Description |
|---|---|---|
id | UUID | Primary key |
workspace_id | UUID | FK to workspaces (CASCADE) |
rule_id | UUID | FK to automation_rules (SET NULL) |
listing_id | UUID | FK to listings (CASCADE) |
lead_id | UUID | FK to contact_submissions (SET NULL) |
status_change_id | UUID | FK to listing_status_changes (SET NULL) |
trigger_type | TEXT | Which trigger created this item |
trigger_data | JSONB | Snapshot of context at creation |
action_type | TEXT | social_post, email_draft, or notification |
content | JSONB | AI-generated content |
edited_content | JSONB | Agent's edits (if any) |
status | TEXT | proposal, generating, generated, approved, sent, skipped |
badge_text | TEXT | Display badge (e.g., "Just Sold") |
ai_model | TEXT | Model used for generation |
ai_cost_cents | NUMERIC(10,4) | Cost of generation |
fair_housing_passed | BOOLEAN | Fair Housing check result |
flagged_terms | TEXT[] | Terms flagged by Fair Housing check |
suggested_send_at | TIMESTAMPTZ | Suggested optimal send time |
generated_at | TIMESTAMPTZ | When AI content was generated |
approved_at | TIMESTAMPTZ | When agent approved |
approved_by | UUID | FK to auth.users |
skipped_at | TIMESTAMPTZ | When agent skipped |
Indexes: (workspace_id, status, created_at DESC), (listing_id). RLS enabled (service role only).
Seeded Templates
The migrations seed eight rule templates (all with luxury tone):
| Name | Category | Trigger | Config | Action |
|---|---|---|---|---|
| Just Sold Post | social | status_change | toStatuses: ["Closed"] | social_post |
| Just Listed Post | social | status_change | toStatuses: ["Active"] | social_post |
| Under Contract Post | social | status_change | toStatuses: ["Pending", "Active Under Contract"] | social_post |
| Price Improvement Post | social | price_change | direction: "decrease", minPercentChange: 3 | social_post |
| Back on Market Post | social | status_change | toStatuses: ["Active"], fromStatuses: ["Pending", "Withdrawn", "Active Under Contract"] | social_post |
| Just Listed Email | email | status_change | toStatuses: ["Active"] | email_draft |
| Just Sold Email | email | status_change | toStatuses: ["Closed"] | email_draft |
| Price Reduction Email | email | price_change | direction: "decrease", minPercentChange: 3 | email_draft |
Type System
Key types from packages/shared/src/types/automations.ts:
type AutomationTriggerType = 'status_change' | 'price_change' | 'new_listing'
| 'lead_inquiry' | 'lead_idle' | 'scheduled'
type AutomationActionType = 'social_post' | 'email_draft' | 'notification'
type AutomationTone = 'professional' | 'conversational' | 'luxury' | 'casual'
type OutboxStatus = 'proposal' | 'generating' | 'generated'
| 'approved' | 'sent' | 'skipped'
interface SocialPostContent {
instagram: string; facebook: string; linkedin: string; hashtags: string[]
}
interface EmailDraftContent {
subject: string // 40-60 characters
preheader: string // 40-80 characters
headline: string // 6-10 words
body: string // 100-150 words
cta: string // 3-6 words
}
interface StatusChangeTriggerConfig {
toStatuses: string[]
fromStatuses?: string[] // Optional — restricts to transitions from specific statuses
}
interface PriceChangeTriggerConfig {
minPercentChange?: number // e.g., 3 for 3%
direction?: 'increase' | 'decrease' | 'both'
}
// Returned by upsertListings() during sync
interface PriceChange {
workspace_id: string
listing_id: string
previous_price: number
new_price: number
percent_change: number
direction: 'increase' | 'decrease'
}
interface AutomationConfig { // WorkspaceSettings.automations
voiceDescription?: string
defaultTone?: AutomationTone
}
interface AutomationsGlobalSettings { // GlobalSettings.automations
enabled: boolean
maxDailyOutboxPerWorkspace: number // default 50
defaultCooldownHours: number // default 24
}AI Operations: automation_social_post and automation_email_draft (tracked in ai_usage and ai_content_audit tables).
Full record types: AutomationRuleTemplate, AutomationRule, AutomationOutboxItem, AutomationOutboxItemWithListing (with joined listing data).
Trigger Evaluation
File: packages/sync/src/post-sync/evaluate-automation-triggers.ts
Called as a post-sync hook in packages/sync/src/worker.ts, following the exact pattern of create-draft-proposals.ts.
Status Change Evaluation
- Check
features.automationsis enabled for the workspace - Fetch enabled
automation_ruleswheretrigger_type = 'status_change' - For each status change detected during sync:
- Match against each rule's
trigger_config.toStatuses - If rule has
fromStatuses, also check that the previous status is in the set (enables "Back on Market" to trigger only from Pending/Withdrawn → Active, not on initial listing) - Check cooldown: query
automation_outboxfor recent entries with samelisting_idandrule_id - If no recent duplicate exists, insert a proposal row
- Match against each rule's
- Only
personalandteamcategory listings are eligible (notoffice,sold,demo)
Badge text derived from status: "JUST SOLD", "JUST LISTED", "JUST LEASED", "UNDER CONTRACT".
Price Change Evaluation
- Fetch enabled
automation_ruleswheretrigger_type = 'price_change' - For each price change detected during sync:
- Match against rule's
directionfilter (increase, decrease, or both) - Match against
minPercentChangethreshold (default 0) - Same category filtering and cooldown deduplication as status changes
- Insert proposal with
trigger_data: { previousPrice, newPrice, percentChange, direction }
- Match against rule's
Badge text: "PRICE IMPROVEMENT" (decrease) or "PRICE UPDATE" (increase).
Suggested Send Time
All proposals are created with suggested_send_at computed as next business day at 10:00 AM EST (weekends are skipped).
Category Eligibility
Only listings with category = 'personal' or category = 'team' are eligible for automations. Office-wide, sold archive, and demo listings are excluded.
AI Generation Pipeline
Directory: packages/ai/src/automations/
persona.ts
Extracts persona context from workspace settings:
- Agent name, title, company (from
settings.agent) - Brokerage name (from
settings.brokerage) - Team name (from
settings.team) - Voice description and tone (from
settings.automations)
Builds a system prompt preamble that instructs the AI to write as the agent.
prompt-builder.ts
Composes the full prompt:
- Fair Housing system prompt (from
packages/ai/src/safeguards/fair-housing.ts) - Persona prompt (from
persona.ts) - Rule template or default prompt
- Listing context (address, price, beds/baths, property type, remarks)
- Price tier guidance — tier-specific vocabulary based on listing price ($20M+ Trophy, $5M-$20M Ultra-Luxury, $1M-$5M Luxury, under $1M Premium)
- Luxury vocabulary rules — "residence" not "home" for $5M+, specific water types, no clichés
- Status change or price change context
- Output format instructions (JSON for social posts or email drafts)
social-post-generator.ts
Wraps the social post generation flow:
- Strips PII from listing remarks (
stripPII()) - Calls the AI provider with composed prompt
- Parses JSON response into
SocialPostContent - Runs Fair Housing post-check on all three captions
- Returns content, check results, usage stats, and model info
Operation type: automation_social_post
email-draft-generator.ts
Wraps the email draft generation flow (same pattern as social posts):
- Strips PII from listing remarks
- Calls the AI provider with email-specific prompt (subject, preheader, headline, body, CTA)
- Parses JSON response into
EmailDraftContent - Runs Fair Housing post-check on all email fields combined
- Returns content, check results, usage stats, and model info
Operation type: automation_email_draft
Both generators are tracked in ai_usage and ai_content_audit tables.
API Endpoints
Admin API
| Endpoint | Method | Auth | Description |
|---|---|---|---|
/api/admin/automation-templates | GET | Platform Admin | List all global rule templates |
/api/admin/automation-templates | POST | Platform Admin | Create a new template |
/api/admin/automation-templates/[id] | PATCH | Platform Admin | Update a template |
/api/admin/automation-templates/[id] | DELETE | Platform Admin | Delete a template |
/api/admin/workspaces/[id]/automation-rules | GET | Workspace Admin | List workspace rules |
/api/admin/workspaces/[id]/automation-rules | POST | Workspace Admin | Create or adopt a rule |
/api/admin/workspaces/[id]/automation-rules/[rid] | PATCH | Workspace Admin | Update a rule |
/api/admin/workspaces/[id]/automation-rules/[rid] | DELETE | Workspace Admin | Delete a rule |
/api/admin/workspaces/[id]/automation-outbox | GET | Workspace Admin | List outbox items (with listing join) |
/api/admin/workspaces/[id]/automation-stats | GET | Workspace Admin | Outbox stats: counts by status, by rule, weekly/monthly triggers, approval rate, AI cost |
Web Dashboard API
| Endpoint | Method | Description |
|---|---|---|
/api/dashboard/outbox | GET | List outbox items (with listing data) |
/api/dashboard/outbox/count | GET | Pending + generated count (for nav badge) |
/api/dashboard/outbox/[id] | PATCH | Update status, save edited content |
/api/dashboard/outbox/[id]/generate | POST | Trigger AI generation for a proposal |
/api/dashboard/outbox/[id]/regenerate | POST | Regenerate content for an existing item |
/api/dashboard/automations/rules | GET | List workspace rules with per-rule outbox counts |
/api/dashboard/automations/stats | GET | 30-day stats: items by status, per day, per rule, AI cost, recent activity |
Generation and regeneration endpoints support both social_post and email_draft action types. They perform: usage limit check, API key decryption, status transition to generating, AI generation, Fair Housing check, usage recording, audit trail creation, and status transition to generated.
Configuration
Global Settings
Managed by platform admins at Admin > Settings > Automations:
interface AutomationsGlobalSettings {
enabled: boolean // Master kill switch
maxDailyOutboxPerWorkspace: number // Default 50
defaultCooldownHours: number // Default 24
}Stored in global_settings.settings.automations.
Workspace Feature Flag
Enable per workspace via Admin > Workspaces > [name] > Automations tab:
- Toggle
features.automationson/off - Configure voice description and default tone
- Adopt rules from global templates
- View outbox summary stats
Workspace Config
Stored in workspace.settings.automations:
interface AutomationConfig {
voiceDescription?: string // Free-text voice description
defaultTone?: AutomationTone // 'professional' | 'conversational' | 'luxury' | 'casual'
}Backward Compatibility
Automations runs alongside the existing Draft Social Posts system:
features.autoDraftPostscontrols the legacydraft_social_postspipelinefeatures.automationscontrols the newautomation_outboxpipeline- Both can be enabled simultaneously — they use separate tables and trigger independently
- In the sync worker,
evaluateAutomationTriggers()andcreateDraftProposals()run in separate try/catch blocks - The Draft Posts UI shows a banner recommending Automations when both are available
New workspaces should use Automations. Existing workspaces can migrate at their own pace.
Future Phases
Phase 1: Delivery & Notifications
- Direct social media posting via platform APIs
- Email delivery via Resend
- WhatsApp approval notifications via Twilio
- Delivery status tracking
- Scheduled triggers
Phase 2: Lead Triggers
lead_inquirytrigger (from contact form submissions)lead_idletrigger (cron-based idle lead detection)- Automated follow-up sequences
Phase 3: Advanced
- Content variant A/B testing
- Engagement analytics
- Smart send-time optimization based on audience data
Related Documentation
- AI Integration — Provider abstraction, Fair Housing safeguards, usage tracking
- API Reference — Full endpoint specifications
- Database Schema — All table definitions
- Sync — Sync worker and post-sync hooks
- Automations Feature Guide — Agent-facing feature documentation