Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
65 changes: 51 additions & 14 deletions examples/app-crm/src/workflows/high-value-deal.workflow.ts
Original file line numberDiff line numberDiff line change
@@ -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' } },
},
};
97 changes: 63 additions & 34 deletions examples/app-crm/src/workflows/stale-opportunity.workflow.ts
Original file line numberDiff line numberDiff line change
@@ -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' } },
},
};
Loading