HotCRM Logo

Workflows

How to build automation workflows in HotCRM using *.workflow.ts files

Workflows

Workflows are the rule-based automation layer of HotCRM. They trigger actions automatically when records are created, updated, or on a schedule — without writing imperative code. Every workflow is defined in a *.workflow.ts file.

File Convention

packages/{package}/src/{name}.workflow.ts

Examples from the codebase:

PackageWorkflow FilePurpose
CRMlead.workflow.tsLead auto-assignment, scoring, nurturing
Supportcase_escalation.workflow.tsSLA escalation and auto-resolution
Financepayment_reminder.workflow.tsPayment due date reminders
Marketingcampaign.workflow.tsCampaign lifecycle automation
Productsapproval.workflow.tsQuote/discount approval flows
HRhr.workflow.tsEmployee onboarding and reviews

HotCRM currently has 6 workflow files across the business packages.

Basic Workflow Structure

A workflow defines a trigger, a condition, and a list of actions:

// packages/crm/src/lead.workflow.ts

export const LeadAutoAssignment = {
  name: 'lead_auto_assignment',
  label: 'Auto Assign New Leads',
  object: 'lead',
  description: 'Assign new leads to the next available sales rep',

  // When does it fire?
  triggerType: 'onCreate',

  // Under what conditions?
  condition: 'status = "new" AND owner = NULL',

  // What happens?
  actions: [
    {
      type: 'fieldUpdate',
      field: 'owner_id',
      formula: 'getNextAvailableRep(territory, industry)',
    },
    {
      type: 'fieldUpdate',
      field: 'assigned_date',
      value: 'NOW()',
    },
    {
      type: 'emailAlert',
      template: 'new_lead_assigned',
      recipients: ['owner_id'],
    },
    {
      type: 'taskCreation',
      subject: 'Follow up with new lead: ${name}',
      assignee: '${owner_id}',
      dueDate: 'TODAY() + 1',
      priority: 'high',
    },
  ],

  executionOrder: 1,
  active: true,
};

Trigger Types

Workflows support several trigger types:

Trigger TypeFires WhenUse Case
onCreateA new record is createdLead assignment, welcome emails
onUpdateAn existing record is updatedSLA escalation, status changes
onDeleteA record is deletedCleanup, notifications
scheduledOn a time-based schedulePayment reminders, report generation
onFieldChangeA specific field value changesStage progression, approval requests

Scheduled Trigger Example

// packages/finance/src/payment_reminder.workflow.ts

export const PaymentDueReminder = {
  name: 'payment_due_reminder',
  label: 'Payment Due Reminder',
  object: 'invoice',
  description: 'Send reminders 7 days before invoice due date',

  triggerType: 'scheduled',
  schedule: {
    frequency: 'daily',
    time: '09:00',
    timezone: 'UTC',
  },

  // Find invoices due in 7 days
  condition: 'status = "sent" AND due_date = TODAY() + 7',

  actions: [
    {
      type: 'emailAlert',
      template: 'payment_reminder',
      recipients: ['${contact_id.email}'],
      cc: ['${owner_id.email}'],
    },
    {
      type: 'fieldUpdate',
      field: 'reminder_sent',
      value: true,
    },
    {
      type: 'fieldUpdate',
      field: 'reminder_sent_date',
      value: 'NOW()',
    },
  ],

  active: true,
};

Action Types

Workflow actions define what happens when the workflow fires:

1. Field Update

Set or compute a field value:

// Static value
{ type: 'fieldUpdate', field: 'status', value: 'In Progress' }

// Dynamic value with NOW()
{ type: 'fieldUpdate', field: 'assigned_date', value: 'NOW()' }

// Computed with formula
{
  type: 'fieldUpdate',
  field: 'priority',
  formula: `
    CASE
      WHEN priority = 'low' THEN 'medium'
      WHEN priority = 'medium' THEN 'high'
      WHEN priority = 'high' THEN 'critical'
      ELSE priority
    END
  `
}

2. Email Alert

Send templated email notifications:

{
  type: 'emailAlert',
  template: 'case_sla_escalation',
  recipients: ['${owner_id}'],
  cc: ['${owner_id.manager_email}'],
  description: 'Alert support manager of SLA breach escalation'
}

3. Task Creation

Create follow-up tasks automatically:

{
  type: 'taskCreation',
  subject: 'Follow up with new lead: ${name}',
  description: 'Initial contact and qualification',
  assignee: '${owner_id}',
  dueDate: 'TODAY() + 1',
  priority: 'high',
  status: 'not_started'
}

4. HTTP Call (Webhook)

Call external services:

{
  type: 'httpCall',
  method: 'POST',
  url: '${env.SLACK_WEBHOOK_URL}',
  headers: { 'Content-Type': 'application/json' },
  body: {
    text: '🎯 New lead assigned',
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: '*Lead:* ${name}\n*Company:* ${company}\n*Assigned to:* ${owner.name}'
        }
      }
    ]
  }
}

5. Custom Handler

Call a custom function for complex logic:

{
  type: 'customHandler',
  handler: 'assignTerritoryBasedOwner',
  parameters: {
    territory: '${territory}',
    industry: '${industry}',
  }
}

Real-World Examples

Case SLA Escalation

Automatically escalate support cases when SLA targets are breached:

// packages/support/src/case_escalation.workflow.ts

export const CaseAutoEscalation = {
  name: 'case_auto_escalation',
  label: 'Auto Escalate SLA Breached Cases',
  object: 'case',
  description: 'Escalate case priority when SLA resolution target is exceeded',

  triggerType: 'onUpdate',
  condition:
    'is_sla_violated = true AND status != "closed" AND status != "resolved" AND is_escalated = false',

  actions: [
    // Escalate priority one level up
    {
      type: 'fieldUpdate',
      field: 'priority',
      formula: `
        CASE
          WHEN priority = 'low' THEN 'medium'
          WHEN priority = 'medium' THEN 'high'
          WHEN priority = 'high' THEN 'critical'
          ELSE priority
        END
      `,
    },
    // Mark as escalated
    { type: 'fieldUpdate', field: 'is_escalated', value: true },
    { type: 'fieldUpdate', field: 'escalated_date', value: 'NOW()' },
    { type: 'fieldUpdate', field: 'escalation_reason', value: 'SLA resolution target exceeded' },
    // Notify manager
    {
      type: 'emailAlert',
      template: 'case_sla_escalation',
      recipients: ['${owner_id}'],
      cc: ['${owner_id.manager_email}'],
    },
    // Post to Slack
    {
      type: 'httpCall',
      method: 'POST',
      url: '${env.SLACK_WEBHOOK_URL}',
      headers: { 'Content-Type': 'application/json' },
      body: {
        text: '🚨 Case Escalated - SLA Breach: ${case_number}',
      },
    },
  ],

  executionOrder: 1,
  active: true,
};

Campaign Lifecycle Automation

Automate campaign state transitions and notifications:

// packages/marketing/src/campaign.workflow.ts

export const CampaignAutoActivation = {
  name: 'campaign_auto_activation',
  label: 'Auto Activate Scheduled Campaigns',
  object: 'campaign',
  description: 'Activate campaigns when their start date arrives',

  triggerType: 'scheduled',
  schedule: { frequency: 'hourly' },
  condition: 'status = "scheduled" AND start_date <= NOW()',

  actions: [
    { type: 'fieldUpdate', field: 'status', value: 'active' },
    { type: 'fieldUpdate', field: 'actual_start_date', value: 'NOW()' },
    {
      type: 'emailAlert',
      template: 'campaign_activated',
      recipients: ['${owner_id}'],
    },
  ],

  active: true,
};

export const CampaignBudgetAlert = {
  name: 'campaign_budget_alert',
  label: 'Campaign Budget Threshold Alert',
  object: 'campaign',
  description: 'Alert when campaign spend reaches 80% of budget',

  triggerType: 'onUpdate',
  condition:
    'status = "active" AND actual_cost >= (budgeted_cost * 0.8) AND budget_alert_sent = false',

  actions: [
    {
      type: 'emailAlert',
      template: 'campaign_budget_alert',
      recipients: ['${owner_id}'],
      cc: ['${env.MARKETING_DIRECTOR_EMAIL}'],
    },
    { type: 'fieldUpdate', field: 'budget_alert_sent', value: true },
  ],

  active: true,
};

Quote Approval Process

Multi-step approval flow for high-discount quotes:

// packages/products/src/approval.workflow.ts

export const QuoteDiscountApproval = {
  name: 'quote_discount_approval',
  object: 'quote',
  triggerType: 'onUpdate',
  condition: 'discount_percent > 10 AND status = "draft"',

  initialSubmissionActions: [
    { type: 'fieldUpdate', field: 'approval_status', value: 'pending' },
    {
      type: 'emailAlert',
      template: 'approval_request_submitted',
      recipients: ['${submitter}'],
    },
  ],

  steps: [
    {
      stepNumber: 1,
      name: 'sales_manager_approval',
      approverType: 'user',
      approver: '${owner.manager}',
      skipCondition: 'discount_percent <= 15',
      approvalActions: [
        { type: 'fieldUpdate', field: 'approval_status', value: 'manager_approved' },
      ],
      rejectionActions: [
        { type: 'fieldUpdate', field: 'approval_status', value: 'rejected' },
        { type: 'emailAlert', template: 'approval_rejected', recipients: ['${owner}'] },
      ],
    },
    {
      stepNumber: 2,
      name: 'sales_director_approval',
      approverType: 'role',
      approver: 'sales_director',
      skipCondition: 'discount_percent <= 20',
      approvalActions: [
        { type: 'fieldUpdate', field: 'approval_status', value: 'director_approved' },
        { type: 'fieldUpdate', field: 'status', value: 'approved' },
      ],
      rejectionActions: [
        { type: 'fieldUpdate', field: 'approval_status', value: 'rejected' },
      ],
    },
  ],

  finalApprovalActions: [
    { type: 'emailAlert', template: 'quote_approved', recipients: ['${owner}'] },
  ],
  finalRejectionActions: [
    { type: 'emailAlert', template: 'quote_rejected', recipients: ['${owner}'] },
  ],
};

Registering Workflows in Plugins

Workflows are exported and registered in the package's plugin.ts:

// packages/crm/src/plugin.ts
import { LeadWorkflows } from './lead.workflow';

export const CRMPlugin = {
  name: '@hotcrm/crm',
  // ...
  workflows: [
    LeadWorkflows.LeadAutoAssignment,
    LeadWorkflows.LeadAutoScoring,
    LeadWorkflows.LeadNurturing,
    LeadWorkflows.LeadEnrichment,
  ],
};

Conditions & Formulas

Workflow conditions use a formula language to evaluate when the workflow should fire:

// Simple comparisons
'status = "new"'
'amount > 50000'
'priority = "critical"'

// Compound conditions
'status = "new" AND owner = NULL'
'is_sla_violated = true AND status != "closed"'
'actual_cost >= (budgeted_cost * 0.8) AND budget_alert_sent = false'

// Date comparisons
'due_date = TODAY() + 7'
'start_date <= NOW()'
'close_date >= TODAY()'

// Field change detection (in onUpdate workflows)
'ISCHANGED(stage)'
'ISCHANGED(priority) AND priority = "critical"'

// Null checks
'owner = NULL'
'resolved_date != NULL'

Best Practices

1. Set Execution Order

When multiple workflows can fire on the same object, control the order:

// Assignment first, scoring second
export const LeadAutoAssignment = {
  // ...
  executionOrder: 1,
};

export const LeadAutoScoring = {
  // ...
  executionOrder: 2,
};

2. Use Guard Conditions

Prevent duplicate or recursive execution:

// ✅ Good — check is_escalated flag to prevent re-escalation
condition: 'is_sla_violated = true AND is_escalated = false'

// ❌ Bad — could fire repeatedly
condition: 'is_sla_violated = true'

3. Keep Actions Atomic

Each action should be independent and idempotent when possible:

// ✅ Good — each action is self-contained
actions: [
  { type: 'fieldUpdate', field: 'status', value: 'active' },
  { type: 'fieldUpdate', field: 'actual_start_date', value: 'NOW()' },
  { type: 'emailAlert', template: 'campaign_activated', recipients: ['${owner_id}'] },
]

4. Use Environment Variables for External URLs

Never hardcode webhook URLs or API keys:

// ✅ Good
{ type: 'httpCall', url: '${env.SLACK_WEBHOOK_URL}' }

// ❌ Bad
{ type: 'httpCall', url: 'https://hooks.slack.com/services/T00000/B000/XXXX' }

5. Provide Descriptions

Add clear descriptions for maintainability:

export const PaymentOverdueEscalation = {
  name: 'payment_overdue_escalation',
  label: 'Overdue Payment Escalation',
  description: 'Escalate invoices overdue by 30+ days to finance manager',
  // ...
};

Next Steps

On this page