Automation

Validation rules, flows, scheduled jobs and approvals — the business logic that runs without you.

Automation

Automation is what makes HotCRM do work for you — sending notifications, updating fields, creating tasks, escalating cases, all without anyone clicking a button. Four kinds of automation are available, each suited to a different job.

How HotCRM implements them: validation + field logic run as object hooks (beforeInsert / beforeUpdate); everything multi-step, scheduled, or approval-based runs as flows; and approvals are approval nodes inside a flow (ADR-0019), not a standalone process type. Standalone workflow-rule and approval-process metadata do not exist on the platform either — ADR-0019 and ADR-0020 removed both types.

The four kinds

KindWhen it firesBest for
Validation rulesBefore saveBlocking bad data
FlowsOn screen, on record change, or on a scheduleMulti-step logic, branching, loops, waits, approvals
Scheduled jobsOn a scheduleDaily sweeps, weekly reports
Approval processesInside a flow (approval node)Multi-step sign-off with record locking

Validation rules

The simplest kind. They block a save if a condition is true.

Built-in examples:

  • "End date must be after start date" (on contracts).
  • "List price must be greater than zero" (on products).
  • "A campaign member must have either a Lead or a Contact, not both" (on campaign members).
  • "Discount % must be between 0 and 100" (on quote line items).

To add one — it is a source edit, not a Setup screen. There is no Setup → Object entry: the object roster the Console does show is Studio → Data Model → Objects, and a rule is a validations[] entry on the object itself (src/*/objects/*.object.ts), next to its fields.

  1. Add a validations[] entry to the object (src/*/objects/*.object.ts).
  2. Write the condition (the fail condition).
  3. Write the error message shown to the user.
  4. Deploy — a rule runs unless it carries active: false.

Flows (multi-step)

Looking for "workflow rules"? There is nothing to look for — not in this app, and not on the platform. ObjectStack retired the standalone workflow-rule type (ADR-0019 / ADR-0020): there is no workflow metadata type, no workflows collection on a stack, and no Workflow Rules entry under Studio → Automation — only Flows. A "when this record saves, do that" rule is a record-change flow, and every one this app ships is a row in the table below.

When a single field update isn't enough — branching, loops, waits, multi-object writes, approvals — use a flow. A flow is a visual graph of nodes: get data → decide → create/update records → notify → wait → call a sub-flow.

A flow fires one of three ways, set by its start node:

  • Screen — launched manually from a button/action; collects input on a screen.
  • Record change — fires on insert/update (record-after-create / record-after-update).
  • Schedule — runs on a cron schedule.

Auto-launch needs the triggers capability. Record-change and scheduled flows only fire when the stack's requires list includes triggers — it installs the record-change + schedule trigger providers (schedule triggers also use the job service). Screen flows are always launched manually.

Built-in flows in HotCRM (32). Each row carries the flow's own label — the name listed in Studio → Automation → Flows, and the name you pick from in Studio → Developer → Flow Runs, so a run you are chasing can be looked up here verbatim:

FlowTriggerWhat it does
Lead Conversion ProcessScreenConvert a qualified lead into an account + contact (+ optional opportunity), then notify
Generate Quote from OpportunityScreenBuild a quote from an opportunity and move it to Proposal
Schedule Follow-upScreenCreate the next follow-up task on a lead, already linked to it and owned by the right user
Enroll Members in CampaignScreenBulk-enroll eligible leads or contacts into this campaign, skipping the already-enrolled and the opted-out
Enroll Lead in CampaignSubflowCalled by Enroll Members in Campaign to write one lead membership, including its enrollment date, with system privilege
Enroll Contact in CampaignSubflowCalled by Enroll Members in Campaign to write one contact membership, including its enrollment date, with system privilege
Claim CaseScreenTake an unowned case out of triage by moving it to a status that means you are on it
Escalate CaseScreenCollect an escalation reason, then flag and re-prioritise the case
Stamp Case EscalationSubflowCalled by Escalate Case to write the escalation stamps with system privilege
Close CaseScreenCollect the resolution, then close the case and stop the SLA clock
New Lead Routing & SLARecord change (insert)Stamp a rating-based follow-up SLA and alert the new lead's owner
Contact WelcomeRecord change (insert)Prompt the owner to welcome a newly created contact
Urgent Task AlertRecord change (insert)Notify the owner when a task is created at Urgent
Large Deal ApprovalRecord change (update)Tiered sign-off via approval nodes — Sales Manager ≥ $100K, Sales Director > $500K
Large Deal Approval (on create)Record change (insert)The same intake for opportunities created at or above the threshold
Opportunity Status Change ApprovalRecord change (update)Sign-off a deal needs before it is declared won or lost. Ships switched OFF — it opens nothing until an install arms the gate on the opportunity's Status Change Approval field; armed, the rep sets Requested Status and the stage moves only when the request is approved
Opportunity Qualification ApprovalRecord change (update)The qualification (立项) sign-off a new deal needs before its stage or its won/lost call may change. Ships switched OFF — it opens nothing until an install arms the gate on the opportunity's Qualification Approval field; armed, the rep ticks Request Qualification Approval, and everything else on the deal stays editable while it waits
Lead Conversion ApprovalRecord change (insert)Sign-off a lead needs before it can be converted. Ships switched OFF — it opens nothing until an install arms the gate on the lead's Conversion Approval field
Account ApprovalRecord change (insert)Sign-off a newly created account needs before it counts as established data. Ships switched ON — a new account starts Pending and is decided in the approval inbox; an install that wants no account sign-off changes the default on the account's Approval Status field
Large Deal Won AlertRecord change (update)When an opportunity of $100K or more turns Closed Won, notify the owner — the owner alone, not their manager
Billing Hand-off: Closed WonRecord change (update)On the transition into Closed Won, enqueue one durable POST of the deal + account + line items to the billing endpoint
Billing Hand-off: Contract ActivatedRecord change (update)On the transition into Activated, enqueue one durable POST of the contract + account + the originating deal's line items
Case Escalation ProcessRecord change (update)When a case turns Critical, flag it escalated, hand it to the least-loaded holder of the service_manager position (it stays with its owner while that pool is unstaffed) and notify the agent it came from; escalating also opens an urgent follow-up task for the account owner
Case Escalation Process (on create)Record change (insert)The same escalation for cases created at Critical
Contract Auto-ExpirationSchedule (daily midnight)Expire activated contracts past their end_date and notify the owner
Quote Auto-ExpirationSchedule (daily 1 AM)Expire quotes past their expiration_date that are still open
Campaign Auto-CompletionSchedule (daily 2 AM)Mark in-progress campaigns whose end_date has passed as Completed
Forecast SnapshotSchedule (daily 3 AM)Upsert a current-quarter forecast row per active opportunity owner — pipeline, best case, commit and closed-won totals
Stalled Deal AlertSchedule (daily 7:30 AM)Nudge owners about open opportunities stuck in a stage too long
Contract Renewal ReminderSchedule (daily 8 AM)Open renewal tasks/opportunities for contracts nearing their end_date
Case SLA MonitorSchedule (hourly)Flag and escalate open cases past their SLA due date
Task Due ReminderSchedule (hourly)Notify owners of tasks whose reminder time has arrived

Two entries carry an (on create) twin. Record-change flows subscribe to one trigger type each — record-after-create or record-after-update — so automation that has to catch both a newly created record and a later edit is authored as a pair of flows with the same condition. They are separate rows here because they are separate runs in Flow Runs.

Seeded demo records get their owner from the platform, not from a flow: when the seed data finishes loading, the platform hands every seeded record that has no owner to the first administrator. HotCRM's former Demo Bootstrap flow, which re-checked for ownerless records every ten minutes, has been retired.

Notifications inside flows are delivered by the notify node (inbox + email via the messaging service) — not the legacy script/email step, which is a no-op in 7.4.

See Customization › Extending Objects if you need to build new flows.

Scheduled automation

Time-based automation is implemented as scheduled flows — flows whose start node carries a cron schedule. The eight Schedule rows above are the complete set; there is no separate scheduled-job metadata to look for. They run via the job service, so the triggers capability is paired with job (both ship in the default slate).

From ObjectStack 17.5.0, scheduled flows are off until the deployment turns them on. The platform runs package-authored scheduled work only when the deployment sets OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true (1, on and yes also count). Without it, none of the eight flows above runs — no contract or quote expiry, no SLA monitor, no reminders, no forecast snapshots — and os doctor prints the effective value. Under the isolated tenancy posture a scheduled flow must also name the organization it acts as; these eight do not, so they are not armed there.

Date-driven field logic that needs no orchestration — defaulting a quote's expiration date, freezing an expired/accepted quote, deriving a forecast period — lives in lightweight object hooks (beforeInsert / beforeUpdate) rather than a scheduled sweep.

The division of labour on forecasts is worth calling out, because it is the pattern to copy: the Forecast Snapshot flow decides who gets a snapshot and what the totals are, while the forecast object's hook decides which calendar period the snapshot belongs to. A cron expression can say "every night"; it cannot say "the first day of this quarter", so that boundary is derived once, by the object, for every writer.

Approvals

Since ObjectStack 7.4, approvals are modeled as approval nodes inside a flow (ADR-0019) rather than a standalone approval-process type. On entry the node opens an approval request, locks the record while the step is pending, mirrors the live status onto an approval_status field, and resumes down the approve / reject branch.

HotCRM's built-in Opportunity Approval flow chains two approval nodes for tiered sign-off (manager → director). See Revenue › Approvals for thresholds, what approvers see, and the audit trail.

A second, independent gate — Opportunity Status Change Approval — asks for sign-off before a deal is declared won or lost. It ships switched off and keeps its verdict in its own field, so arming it never changes the amount-based sign-off. A third, Opportunity Qualification Approval, is the qualification (立项) sign-off a new deal needs before its stage or won/lost call may change; it too ships off, and with both armed it comes first. See Sales › Opportunity Qualification.

Order of operations

When a record is saved, the order is fixed:

  1. System runs auto-calculations (formula fields, auto-numbering).
  2. Validation rules run — if any fail, the save is blocked.
  3. The record is saved to the database.
  4. Flow triggers fire — record-after-create or record-after-update, whichever the save was.
  5. notify nodes inside those flows queue their inbox messages and email.

A flow's own writes are ordinary saves, so they re-enter this list and can trigger further flows. There is no fixed re-evaluation budget to rely on: the engine breaks self-trigger loops with a re-entrancy guard — a flow re-entered for the same record while its previous run is still in flight is skipped, and the skip is logged as a warning. Write each record-change flow's start condition so it stops re-firing on its own writes; the guard is a backstop, not a stop condition.

Understanding this order helps debug "why didn't my flow fire?" questions.

Email templates

Most automation actions send email. Templates live in Studio → Integration → Email Templates and support:

  • Merge fields — {{path.to.value}} placeholders, rendered against the data payload passed on that send. Spell every segment the way HotCRM spells it — objects are crm_opportunity and crm_contact, fields are name, owner_id, email — never Salesforce-style Opportunity.Name. What the path is rooted at depends on the payload shape the sender passes. HotCRM's own notification templates (below) are the shape to copy: each reads flat holes such as {{name}} or {{contract_number}}, filled from the values the flow's notify node passes with it. Declare the names your template reads in its variables list and agree the shape with whoever sends the mail.
  • Conditional blocks — show only if a condition is true.
  • HTML + plain text versions.
  • Attachments — quote PDFs, contract PDFs.

HotCRM ships an email template for each of its own automatic notifications, the alerts its flows send to your colleagues (never to customers): an approval decision on an account, a lead conversion or a large deal; a newly routed or converted lead; a new contact to welcome; a stalled deal or a large one won; a task reminder or an urgent task; a new quote; a contract that has expired or is due for renewal; a case that was escalated or missed its SLA. Each template comes in English, Simplified Chinese, Japanese and Spanish, and every recipient gets the copy in their own language, or in English when the app has none for it. They are records of the platform's email-template object, alongside the platform's own authentication mail (invitations, password resets).

Each notify node in the flows listed above names its template and passes the values the template's holes read, so the subject and body text live in the template, not in the flow: to change the wording, change the template. In the source the templates live in an email-templates/ folder in each package whose flows send them (src/sales/, src/service/ and src/revenue/), one file per language. The alerts that come from scheduled flows go out only where the deployment has switched scheduled work on (see Scheduled automation). Contract activation sends nothing: it runs as an object hook (src/revenue/objects/contract.hook.ts) with no notification step, and the contract mail that does go out comes from the Contract Auto-Expiration and Contract Renewal Reminder flows. Author a template in Studio → Integration → Email Templates when you need a templated outbound email of your own.

Where to monitor automation

  • Studio → Developer → Flow Runs — pick a flow from the list, then read its recent runs and each run's status (success / failed / running / skipped). Scheduled flows appear here too: they are flows, so there is no separate scheduled-job history to go looking for.
  • Studio → Automation → Flows — the flow roster itself, when you need to check what a flow is wired to do before reading its runs.
  • Setup → Diagnostics → Audit Logs — what changed on a record and who changed it: one entry per save, with its time, the user who saved, and the old and new value of every field the save changed. A field an object hook filled in on the way appears inside the same entry as the save that set it off. It is an administrator's screen: Setup is not among a sales rep's apps and the audit log refuses a rep's reads, so a rep follows a record's changes on the record itself (see Tips for users below).
  • The run's outcome counters — each run also records Records Selected, Records Acted On, Gate Skips and Uncountable Effects, with a per-node breakdown in Run Summary. These are real columns rather than one JSON blob, so you can filter and alert on them. Terminal runs are kept for 30 days.

A green run and a run that did something are not the same thing: a sweep that selects records and then writes none reports success exactly like a sweep with nothing to do. Records Acted On is what separates them. The alert worth wiring is Records Selected > 0 and Records Acted On = 0 and Uncountable Effects = 0, sustained over consecutive runs — that last term is not optional, because a run that reached something the platform cannot count has an incomplete acted count rather than a zero one. On runs recorded before these counters existed the fields are empty, and empty is not 0.

Read one qualifier alongside it, or HotCRM's own sweeps will trip that alert while working perfectly. Stalled Deal Alert and Contract Renewal Reminder re-select the same records every morning and then gate each one on whether it has already been handled, so a steady state of "everything has already been nudged" also reads as selected-but-not-acted. Run Summary tells the two apart: if the run's Already Nudged? / Already Reminded? lookup found the existing tasks, the gate skips are accounted for and the sweep is healthy; if it found none and the gate closed anyway, the skips are unexplained and the gate is the suspect.

Tips for admins

  • ✅ Validation rules are the cheapest way to enforce data quality — use them liberally.
  • ✅ One flow per condition is easier to maintain than a mega-flow with 10 branches.
  • ✅ Anything beyond a single field write — branching, multi-object writes, waits, approvals — belongs in a flow rather than an object hook: a flow run is visible in Flow Runs, hook code is not.
  • ✅ Test in sandbox before activating in production — bad automation can cascade fast.
  • ✅ Document what each rule does in its description field — your future self will thank you.

Tips for users

If a record isn't behaving as expected:

  1. Check the record's activity — the Activity tab on a case, lead or opportunity, the Discussion feed on other records, and on a lead its History tab too. Each save adds an entry: a change to a field the app tracks shows its old and new value (Status: New → In Progress), and an edit to other fields shows as a plain Updated entry. It does not say whether a flow or an object hook made the change; ask an admin to read the record's entries in Setup → Diagnostics → Audit Logs.
  2. Check the task list — did a flow auto-create a task you missed?
  3. Ask an admin to open Studio → Developer → Flow Runs and read the recent runs of the flow you expected to fire.

On this page