From 7c4bcf5faa4c10f942f89aeebffc820037299529 Mon Sep 17 00:00:00 2001 From: Jack Zhuang <277994282+os-zhuang@users.noreply.github.com> Date: Sun, 31 May 2026 09:33:12 +0800 Subject: [PATCH] fix(example-crm): migrate workflows to state-machine schema The `workflow` metadata type was reclaimed for state machines (`StateMachineSchema`), but app-crm's two example workflows were still declared as the legacy `Automation.WorkflowRule`, so `defineStack` validation rejects them and the example build/boot fails. Migrate both to `StateMachineConfig`, preserving the original intent as guarded transitions: - high-value-deal: `open` --UPDATE[amount>100k]--> `high_value` (email_alert to sales managers); closes won/lost. - stale-opportunity: `active` --STALE_30D[open]--> `flagged` (email_alert + task_creation follow-up); `UPDATED` clears back to `active`; closes won/lost. --- .../src/workflows/high-value-deal.workflow.ts | 65 ++++++++++--- .../workflows/stale-opportunity.workflow.ts | 97 ++++++++++++------- 2 files changed, 114 insertions(+), 48 deletions(-) diff --git a/examples/app-crm/src/workflows/high-value-deal.workflow.ts b/examples/app-crm/src/workflows/high-value-deal.workflow.ts index 4740d624b6..8701a28527 100644 --- a/examples/app-crm/src/workflows/high-value-deal.workflow.ts +++ b/examples/app-crm/src/workflows/high-value-deal.workflow.ts @@ -1,20 +1,57 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Automation } from '@objectstack/spec'; +import type { StateMachineConfig } from '@objectstack/spec/automation'; /** - * Example workflow rule — flag high value deals. - * When a CRM opportunity is created or updated with amount > 100k, - * the rule fires. (No actions configured to keep the example minimal; - * Studio shows the rule entry so admins can add email_alert/field_update.) + * High-Value Deal lifecycle — state-machine workflow. + * + * Migrated from the legacy `WorkflowRule` model (removed in the + * "reclaim `workflow` for state machines" refactor). The original rule + * fired when a `crm_opportunity` was created/updated with `amount > 100k` + * and notified sales managers. That intent is preserved here as a guarded + * transition: while an opportunity is `open`, an UPDATE whose amount clears + * the $100k threshold moves it to the `high_value` state and runs the + * notify action. The deal then closes won/lost like any other. */ -export const HighValueDealWorkflow: Automation.WorkflowRule = { - name: 'crm_high_value_deal_alert', - objectName: 'crm_opportunity', - triggerType: 'on_create_or_update', - description: 'Notify sales managers when a deal larger than $100k is created or updated.', - criteria: 'record.amount > 100000', - active: true, - executionOrder: 100, - reevaluateOnChange: false, +export const HighValueDealWorkflow: StateMachineConfig = { + id: 'crm_high_value_deal_alert', + description: + 'Flags a crm_opportunity as high-value and notifies sales managers when the amount exceeds $100k.', + initial: 'open', + states: { + open: { + meta: { label: 'Open', description: 'Active opportunity below the high-value threshold.' }, + on: { + UPDATE: { + target: 'high_value', + cond: { type: 'expression', params: { source: 'record.amount > 100000' } }, + actions: [ + { + type: 'email_alert', + params: { + template: 'high_value_deal_alert', + recipients: ['{record.owner_manager_email}'], + }, + }, + ], + description: 'Amount cleared the $100k threshold — notify sales managers.', + }, + CLOSE_WON: 'won', + CLOSE_LOST: 'lost', + }, + }, + high_value: { + meta: { + label: 'High Value', + description: 'Opportunity larger than $100k — under sales-manager watch.', + color: '#16a34a', + }, + on: { + CLOSE_WON: 'won', + CLOSE_LOST: 'lost', + }, + }, + won: { type: 'final', meta: { label: 'Closed Won' } }, + lost: { type: 'final', meta: { label: 'Closed Lost' } }, + }, }; diff --git a/examples/app-crm/src/workflows/stale-opportunity.workflow.ts b/examples/app-crm/src/workflows/stale-opportunity.workflow.ts index 355629fd73..8b7259d86c 100644 --- a/examples/app-crm/src/workflows/stale-opportunity.workflow.ts +++ b/examples/app-crm/src/workflows/stale-opportunity.workflow.ts @@ -1,45 +1,74 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import type { Automation } from '@objectstack/spec'; +import type { StateMachineConfig } from '@objectstack/spec/automation'; /** - * Stale Opportunity Alert + * Stale Opportunity lifecycle — state-machine workflow. * - * A time-based workflow rule that fires at a scheduled interval - * and identifies open opportunities that have not been updated - * in 30+ days, then notifies the assigned owner. + * Migrated from the legacy time-based `WorkflowRule` (removed in the + * "reclaim `workflow` for state machines" refactor). The original rule ran + * on a schedule, found open `crm_opportunity` records untouched for 30+ + * days, and (a) emailed the owner and (b) created a follow-up task. * - * Uses `triggerType: 'schedule'` — evaluated on a recurring cadence - * rather than on individual record create/update events. + * Here the scheduled sweep is modelled as a `STALE_30D` event delivered to + * the machine: while `active`, that event (guarded to open opportunities) + * runs the same two actions and moves the record to `flagged`. A later + * UPDATE clears the flag back to `active`; closing the deal ends the + * lifecycle. */ -export const StaleOpportunityWorkflow: Automation.WorkflowRule = { - name: 'crm_stale_opportunity_alert', - objectName: 'crm_opportunity', - triggerType: 'schedule', +export const StaleOpportunityWorkflow: StateMachineConfig = { + id: 'crm_stale_opportunity_alert', description: - 'Flags open opportunities that have not been updated in 30 days and notifies the sales rep.', - criteria: ` - stage != "closed_won" && - stage != "closed_lost" && - now() - updated_at > duration("P30D") - `, - active: true, - executionOrder: 200, - reevaluateOnChange: false, - actions: [ - { - name: 'notify_stale_owner', - type: 'email_alert', - template: 'stale_opportunity_alert', - recipients: ['{record.owner_email}'], + 'Flags open opportunities untouched for 30 days, notifies the sales rep, and opens a follow-up task.', + initial: 'active', + states: { + active: { + meta: { label: 'Active', description: 'Open opportunity within the freshness window.' }, + on: { + STALE_30D: { + target: 'flagged', + cond: { + type: 'expression', + params: { source: 'stage != "closed_won" && stage != "closed_lost"' }, + }, + actions: [ + { + type: 'email_alert', + params: { + template: 'stale_opportunity_alert', + recipients: ['{record.owner_email}'], + }, + }, + { + type: 'task_creation', + params: { + taskObject: 'crm_activity', + subject: 'Follow up on stale opportunity: {record.name}', + description: + 'This opportunity has not been updated in 30+ days. Please review and update.', + dueDate: '{daysFromNow(3)}', + }, + }, + ], + description: 'No update in 30+ days — notify the owner and open a follow-up task.', + }, + CLOSE_WON: 'won', + CLOSE_LOST: 'lost', + }, }, - { - name: 'create_followup_task', - type: 'task_creation', - taskObject: 'crm_activity', - subject: 'Follow up on stale opportunity: {record.name}', - description: 'This opportunity has not been updated in 30+ days. Please review and update.', - dueDate: '{daysFromNow(3)}', + flagged: { + meta: { + label: 'Stale', + description: 'Flagged as stale; awaiting owner action.', + color: '#f59e0b', + }, + on: { + UPDATED: 'active', + CLOSE_WON: 'won', + CLOSE_LOST: 'lost', + }, }, - ], + won: { type: 'final', meta: { label: 'Closed Won' } }, + lost: { type: 'final', meta: { label: 'Closed Lost' } }, + }, };