Hypertext Rails

Documentation

Getting Started

Communication Center

Procore / Project Groups

Other Features

Automation Workflow Flow Documentation

This document explains the complete flow of what happens when an automation workflow is activated, from user action to email delivery.

📚 For detailed information about trigger events, rate limiting, cadence rules, and how the 5-minute cron job works, see: TRIGGER_EVENTS_AND_CADENCE_RULES.md

Overview

There are three types of automation workflows: 1. Scheduled - Runs on a recurring schedule (daily/weekly/monthly) 2. Trigger - Activates when specific events occur (morale drop, kiosk offline, etc.) 3. Persona - Similar to scheduled but persona-based

Flow Diagram

User Activates Workflow
    ↓
Controller: create_deliveries (DeliveriesConcern)
    ↓
DeliveryScheduleService.create_deliveries
    ↓
Creates CommsInstance + CommsDelivery records
    ↓
CommsDelivery.after_create callback
    ↓
schedule_variable_replacement_job
    ↓
TemplateVariableReplacementJob (scheduled for send_at time)
    ↓
TemplateVariableResolverService.resolve_and_render
    ↓
[If email + has dynamic components]
    CompiledTemplateImageService.generate_compiled_image_url
    ↓
CommsDeliverySendJob
    ↓
ApplicationMailer.delivery_email
    ↓
Email Sent

Detailed Step-by-Step Flow

1. User Activation (Entry Point)

File: app/controllers/concerns/communication_center/deliveries_concern.rb Method: create_deliveries

What happens: - User clicks "Activate" button in the UI - POST request to /communication_center/workflows/:id/create_deliveries - Controller finds the workflow and determines send_at time

Special handling for trigger workflows: - If workflow_type == 'trigger', workflow is just activated (status set to 'active') - No deliveries created immediately - they're created later when triggers fire via cron job - Returns early with success message

For scheduled/persona workflows: - Calculates send_at time based on: - params[:send_at] if provided - workflow.calculate_next_run_at for scheduled workflows - workflow.next_run_at if already set

2. Delivery Creation Service

File: app/services/delivery_schedule_service.rb Method: create_deliveries

What happens:

2.1 Validation

  • Validates workflow exists and has a template
  • Validates workflow status is 'active' or 'paused'
  • Validates send_at time (for trigger workflows, must be within 5 seconds of now)

2.2 Audience Resolution

  • Calls resolve_audience to get matching stakeholders
  • Filters by:
    • Persona (if configured)
    • Project inclusion/exclusion (if configured)
  • Returns array of Stakeholder objects

2.3 Channel Detection

  • Determines available channels from template's channel_variants
  • Checks for emailhtml, smstext, teams_text content

2.4 Create CommsInstance

  • Creates a CommsInstance record that groups all deliveries for this workflow run
  • Stores audience snapshot (stakeholder IDs, count, personas)

2.5 Create Deliveries for Each Stakeholder

  • For each stakeholder:
    • Checks cadence rules (cooldown, daily/weekly/monthly limits, type limits)
    • If cadence allows: creates delivery with original send_at
    • If cadence blocks: queues delivery with calculated next_available_at
    • Determines project contexts (if workflow filters by projects)
    • Creates one delivery per project context

2.6 Cadence Checking

File: app/services/delivery_schedule_service.rb Method: can_send_to_stakeholder?

Checks multiple limits in priority order: 1. Cooldown hours - Minimum time between any communications 2. Type daily limit - Max per day for specific template types (report, alert, etc.) 3. Daily limit - Max communications per day 4. Weekly limit - Max communications per week 5. Monthly limit - Max communications per month

If any limit is exceeded, calculates next_available_at time and queues the delivery.

2.7 Generate Initial Payload

  • Calls generate_merged_payload for each stakeholder
  • Extracts template content for the stakeholder's preferred channel
  • Creates initial JSON payload with:
    • subject (for email)
    • body (channel-specific content)
    • recipient info (name, email, stakeholderid, projectid)

2.8 Update Workflow

  • Updates workflow with:
    • last_run_at = Time.current
    • next_run_at (calculated for scheduled workflows)
    • status = 'active'

2.9 Create Next Scheduled Delivery (if applicable)

  • For scheduled workflows, calls workflow.create_next_scheduled_delivery
  • This creates deliveries for the next scheduled run time
  • Prevents duplicate deliveries by checking existing ones

3. Delivery Record Creation

File: app/models/comms_delivery.rb

What happens: - CommsDelivery.create! is called with: - comms_instance_id - stakeholder_id - project_id - channel (email/sms/teams) - send_at (original or queued time) - status ('scheduled' or 'pending') - merged_payload (initial JSON payload with unresolved variables)

Status determination: - 'pending' if trigger workflow and send_at <= now + 5 seconds - 'scheduled' otherwise

4. After Create Callback

File: app/models/comms_delivery.rb Method: schedule_variable_replacement_job

What happens: - Automatically called after CommsDelivery.create! - Schedules TemplateVariableReplacementJob to run at send_at time - Uses Delayed::Job (not ActiveJob) for precise timing - If send_at <= now + 5 seconds, schedules immediately - Otherwise schedules for future send_at time

Important: This callback is what triggers the variable resolution process!

5. Variable Replacement Job (Runs at send_at time)

File: app/jobs/template_variable_replacement_job.rb Method: perform

What happens:

5.1 Validation

  • Checks delivery exists and isn't already processed
  • Skips if status is 'paused', 'sent', 'delivered', or 'cancelled'

5.2 Check if Resolution Needed

  • Checks if payload contains {{variable}} placeholders
  • If no placeholders, skips to email send

5.3 Resolve Variables

File: app/services/template_variable_resolver_service.rb Method: resolve_and_render

Process: 1. Extracts all variables from payload (subject and body) 2. Resolves each variable using resolve_used_variables - Looks up variable in database - Fetches data based on variable type - Returns resolved value 3. Replaces {{variable}} placeholders with actual values

5.4 Handle Dynamic Components (Email Only)

File: app/services/template_variable_resolver_service.rb Method: replace_with_compiled_image

If email channel + has dynamic components (GAUGE, CHART, LIST, THEME, etc.):

  1. Generate Compiled Image

    • Calls CompiledTemplateImageService.generate_compiled_image_url
    • Extracts all dynamic component tokens from body
    • Categorizes components (gauges, charts, lists, themes, spotchecks)
    • Renders HTML using _compiled_template.html.erb partial
    • Converts HTML to PNG image using HtmlToImageService
    • Stores image and returns URL
  2. Replace in Body

    • Removes all dynamic component tokens from body
    • Inserts <img src="[image_url]"> tag
    • Updates delivery's merged_payload with new body

If no dynamic components: - Just replaces regular variables normally

5.5 Update Delivery Payload

  • Updates delivery.merged_payload with resolved JSON
  • All {{variables}} are now replaced with actual values

5.6 Trigger Email Send

  • Calls CommsDeliverySendJob.perform_later(delivery.id)
  • This queues the actual email sending

6. Email Send Job

File: app/jobs/comms_delivery_send_job.rb Method: perform

What happens:

6.1 Validation

  • Checks delivery exists and isn't processed
  • Validates channel is 'email'
  • If send_at is in future, reschedules job

6.2 Send Email

  • Updates status to 'pending'
  • Calls ApplicationMailer.delivery_email(delivery).deliver_now
  • Mailer uses resolved payload (subject + body with compiled image)

6.3 Update Status

  • On success: updates status to 'sent'
  • Creates CommsEvent record with 'sent' event
  • On failure: updates status to 'failed' with error reason

7. Scheduled Workflow Automation

File: lib/tasks/automation_workflows.rake Task: automation_workflows:process_due

What happens: - Cron job runs periodically (e.g., every 5 minutes) - Finds workflows where next_run_at <= Time.current - For each due workflow: - Calls DeliveryScheduleService.create_deliveries - Creates deliveries for all matching stakeholders - Calculates and sets next next_run_at

8. Trigger Workflow Automation

File: lib/tasks/automation_workflows.rake Task: automation_workflows:process_triggers

What happens: - Cron job runs periodically (typically every 5 minutes) - Finds active trigger workflows - Calls TriggerEvaluatorService.evaluate_all - For each workflow: - Checks if trigger conditions are met - Rate Limit Check (Layer 1): Checks if enough time has passed since last trigger send - Uses cooldown_hours from persona cadence rules (or defaults to 1 hour) - If within rate limit → Skips creating deliveries (prevents spam) - If rate limit allows → Continues to create deliveries - If triggered AND rate limit allows: - Calls DeliveryScheduleService.create_deliveries - Creates deliveries immediately (sendat = now) - Updates `lasttriggersentat` for this trigger event type - Note: Rate limiting is workflow-level, not kiosk-specific or issue-specific

For detailed explanation of rate limiting, cadence rules, and how they work together, see: TRIGGER_EVENTS_AND_CADENCE_RULES.md

Key Files and Their Roles

Controllers

  • app/controllers/concerns/communication_center/deliveries_concern.rb
    • Entry point for workflow activation
    • Handles preview and download of compiled templates

Services

  • app/services/delivery_schedule_service.rb

    • Core service for creating deliveries
    • Handles audience resolution, cadence checking, delivery creation
  • app/services/template_variable_resolver_service.rb

    • Resolves template variables
    • Handles compiled image generation for dynamic components
  • app/services/compiled_template_image_service.rb

    • Generates single compiled image from all dynamic components
    • Renders HTML using _compiled_template.html.erb
  • app/services/trigger_evaluator_service.rb

    • Evaluates trigger conditions for trigger workflows
    • Creates deliveries when triggers fire

Models

  • app/models/automation_workflow.rb

    • Workflow configuration and scheduling logic
    • Callbacks for managing scheduled deliveries
  • app/models/comms_delivery.rb

    • Individual delivery record
    • After_create callback schedules variable replacement
  • app/models/comms_instance.rb

    • Groups deliveries from a single workflow run

Jobs

  • app/jobs/template_variable_replacement_job.rb

    • Resolves variables and generates compiled images
    • Runs at send_at time
  • app/jobs/comms_delivery_send_job.rb

    • Sends the actual email
    • Runs after variable replacement
  • app/jobs/compiled_template_image_job.rb

    • (Not currently used in main flow - may be legacy)

Views

  • app/views/communication_center/dynamic_components/_compiled_template.html.erb

    • Template for rendering compiled template HTML
    • Used by CompiledTemplateImageService to generate image
  • app/views/communication_center/delivery_schedule/_preview_compiled_template.html.erb

    • Preview view for compiled templates in UI

Rake Tasks

  • lib/tasks/automation_workflows.rake
    • Cron tasks for processing scheduled and trigger workflows

Important Callbacks and Hooks

  1. CommsDelivery.after_create

    • Automatically schedules TemplateVariableReplacementJob
    • This is critical - without this, variables won't be resolved!
  2. AutomationWorkflow.after_update

    • ensure_next_delivery_for_scheduled_workflow - Creates next scheduled delivery
    • cancel_future_deliveries_on_schedule_change - Cancels deliveries when schedule changes
    • cancel_deliveries_outside_active_period - Cancels deliveries outside start/end dates

Data Flow Summary

  1. Workflow → Contains template, schedule, audience config
  2. CommsInstance → Groups deliveries from one workflow run
  3. CommsDelivery → Individual delivery to one stakeholder
  4. merged_payload → JSON with subject/body (starts with unresolved variables)
  5. TemplateVariableReplacementJob → Resolves variables, generates compiled image
  6. merged_payload (updated) → Now has resolved content + image tag
  7. CommsDeliverySendJob → Sends email using resolved payload

Common Issues and Debugging

Deliveries not being created

  • Check workflow status is 'active'
  • Check audience config matches stakeholders
  • Check template has channel content configured
  • Check cadence rules aren't blocking all stakeholders

Variables not being resolved

  • Check TemplateVariableReplacementJob is scheduled (check delayed_jobs table)
  • Check delivery has send_at set
  • Check delivery status isn't 'paused' or 'cancelled'
  • Check payload contains {{variable}} placeholders

Compiled images not generating

  • Check body contains dynamic component tokens (GAUGE, CHART, etc.)
  • Check CompiledTemplateImageService.has_dynamic_components? returns true
  • Check HtmlToImageService is working (may need external service)
  • Check logs for image generation errors

Emails not sending

  • Check CommsDeliverySendJob is queued after variable replacement
  • Check delivery status is 'scheduled' or 'pending'
  • Check channel is 'email'
  • Check mailer configuration
  • Check delivery has resolved payload with subject/body