1 OAK MLS

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.

ColumnTypeDescription
idUUIDPrimary key
nameTEXTTemplate name (e.g., "Just Sold Post")
descriptionTEXTHuman-readable description
categoryTEXTsocial, email, or notification
trigger_typeTEXTstatus_change, price_change, new_listing, lead_inquiry, lead_idle, scheduled
trigger_configJSONBTrigger-specific config (e.g., {"toStatuses": ["Closed"]})
action_typeTEXTsocial_post, email_draft, or notification
action_configJSONBAction-specific config (e.g., {"platforms": ["instagram", "facebook", "linkedin"]})
prompt_templateTEXTCustom prompt template (nullable, uses default if null)
toneTEXTDefault tone (professional)
is_activeBOOLEANWhether template is available for adoption
created_atTIMESTAMPTZCreation timestamp
updated_atTIMESTAMPTZLast update timestamp

RLS enabled (service role only).

automation_rules

Per-workspace rules, derived from templates or created custom.

ColumnTypeDescription
idUUIDPrimary key
workspace_idUUIDFK to workspaces (CASCADE on delete)
template_idUUIDFK to automation_rule_templates (SET NULL on delete)
nameTEXTRule name
descriptionTEXTRule description
trigger_typeTEXTSame enum as templates
trigger_configJSONBTrigger configuration
action_typeTEXTSame enum as templates
action_configJSONBAction configuration
prompt_templateTEXTCustom prompt (nullable)
toneTEXTTone for generation (default professional)
model_preferenceTEXTOverride AI model (nullable)
is_enabledBOOLEANWhether rule is active
priorityINTEvaluation priority (default 0)
cooldown_hoursINTHours between duplicate triggers (default 24)

Index: (workspace_id, is_enabled). RLS enabled (service role only).

automation_outbox

The unified queue of AI-drafted content.

ColumnTypeDescription
idUUIDPrimary key
workspace_idUUIDFK to workspaces (CASCADE)
rule_idUUIDFK to automation_rules (SET NULL)
listing_idUUIDFK to listings (CASCADE)
lead_idUUIDFK to contact_submissions (SET NULL)
status_change_idUUIDFK to listing_status_changes (SET NULL)
trigger_typeTEXTWhich trigger created this item
trigger_dataJSONBSnapshot of context at creation
action_typeTEXTsocial_post, email_draft, or notification
contentJSONBAI-generated content
edited_contentJSONBAgent's edits (if any)
statusTEXTproposal, generating, generated, approved, sent, skipped
badge_textTEXTDisplay badge (e.g., "Just Sold")
ai_modelTEXTModel used for generation
ai_cost_centsNUMERIC(10,4)Cost of generation
fair_housing_passedBOOLEANFair Housing check result
flagged_termsTEXT[]Terms flagged by Fair Housing check
suggested_send_atTIMESTAMPTZSuggested optimal send time
generated_atTIMESTAMPTZWhen AI content was generated
approved_atTIMESTAMPTZWhen agent approved
approved_byUUIDFK to auth.users
skipped_atTIMESTAMPTZWhen 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):

NameCategoryTriggerConfigAction
Just Sold Postsocialstatus_changetoStatuses: ["Closed"]social_post
Just Listed Postsocialstatus_changetoStatuses: ["Active"]social_post
Under Contract Postsocialstatus_changetoStatuses: ["Pending", "Active Under Contract"]social_post
Price Improvement Postsocialprice_changedirection: "decrease", minPercentChange: 3social_post
Back on Market Postsocialstatus_changetoStatuses: ["Active"], fromStatuses: ["Pending", "Withdrawn", "Active Under Contract"]social_post
Just Listed Emailemailstatus_changetoStatuses: ["Active"]email_draft
Just Sold Emailemailstatus_changetoStatuses: ["Closed"]email_draft
Price Reduction Emailemailprice_changedirection: "decrease", minPercentChange: 3email_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

  1. Check features.automations is enabled for the workspace
  2. Fetch enabled automation_rules where trigger_type = 'status_change'
  3. 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_outbox for recent entries with same listing_id and rule_id
    • If no recent duplicate exists, insert a proposal row
  4. Only personal and team category listings are eligible (not office, sold, demo)

Badge text derived from status: "JUST SOLD", "JUST LISTED", "JUST LEASED", "UNDER CONTRACT".

Price Change Evaluation

  1. Fetch enabled automation_rules where trigger_type = 'price_change'
  2. For each price change detected during sync:
    • Match against rule's direction filter (increase, decrease, or both)
    • Match against minPercentChange threshold (default 0)
    • Same category filtering and cooldown deduplication as status changes
    • Insert proposal with trigger_data: { previousPrice, newPrice, percentChange, direction }

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:

  1. Fair Housing system prompt (from packages/ai/src/safeguards/fair-housing.ts)
  2. Persona prompt (from persona.ts)
  3. Rule template or default prompt
  4. Listing context (address, price, beds/baths, property type, remarks)
  5. Price tier guidance — tier-specific vocabulary based on listing price ($20M+ Trophy, $5M-$20M Ultra-Luxury, $1M-$5M Luxury, under $1M Premium)
  6. Luxury vocabulary rules — "residence" not "home" for $5M+, specific water types, no clichés
  7. Status change or price change context
  8. Output format instructions (JSON for social posts or email drafts)

social-post-generator.ts

Wraps the social post generation flow:

  1. Strips PII from listing remarks (stripPII())
  2. Calls the AI provider with composed prompt
  3. Parses JSON response into SocialPostContent
  4. Runs Fair Housing post-check on all three captions
  5. 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):

  1. Strips PII from listing remarks
  2. Calls the AI provider with email-specific prompt (subject, preheader, headline, body, CTA)
  3. Parses JSON response into EmailDraftContent
  4. Runs Fair Housing post-check on all email fields combined
  5. 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

EndpointMethodAuthDescription
/api/admin/automation-templatesGETPlatform AdminList all global rule templates
/api/admin/automation-templatesPOSTPlatform AdminCreate a new template
/api/admin/automation-templates/[id]PATCHPlatform AdminUpdate a template
/api/admin/automation-templates/[id]DELETEPlatform AdminDelete a template
/api/admin/workspaces/[id]/automation-rulesGETWorkspace AdminList workspace rules
/api/admin/workspaces/[id]/automation-rulesPOSTWorkspace AdminCreate or adopt a rule
/api/admin/workspaces/[id]/automation-rules/[rid]PATCHWorkspace AdminUpdate a rule
/api/admin/workspaces/[id]/automation-rules/[rid]DELETEWorkspace AdminDelete a rule
/api/admin/workspaces/[id]/automation-outboxGETWorkspace AdminList outbox items (with listing join)
/api/admin/workspaces/[id]/automation-statsGETWorkspace AdminOutbox stats: counts by status, by rule, weekly/monthly triggers, approval rate, AI cost

Web Dashboard API

EndpointMethodDescription
/api/dashboard/outboxGETList outbox items (with listing data)
/api/dashboard/outbox/countGETPending + generated count (for nav badge)
/api/dashboard/outbox/[id]PATCHUpdate status, save edited content
/api/dashboard/outbox/[id]/generatePOSTTrigger AI generation for a proposal
/api/dashboard/outbox/[id]/regeneratePOSTRegenerate content for an existing item
/api/dashboard/automations/rulesGETList workspace rules with per-rule outbox counts
/api/dashboard/automations/statsGET30-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.automations on/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.autoDraftPosts controls the legacy draft_social_posts pipeline
  • features.automations controls the new automation_outbox pipeline
  • Both can be enabled simultaneously — they use separate tables and trigger independently
  • In the sync worker, evaluateAutomationTriggers() and createDraftProposals() 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_inquiry trigger (from contact form submissions)
  • lead_idle trigger (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

On this page