diff --git a/app/javascript/dashboard/i18n/locale/en/automation.json b/app/javascript/dashboard/i18n/locale/en/automation.json
index 6fdc55314..9c3f9f0f1 100644
--- a/app/javascript/dashboard/i18n/locale/en/automation.json
+++ b/app/javascript/dashboard/i18n/locale/en/automation.json
@@ -68,6 +68,10 @@
"ACTIONS": "Actions"
},
"404": "No automation rules found",
+ "SECTIONS": {
+ "INSTANT": "Automations",
+ "DELAYED": "Delayed (Wait)"
+ },
"DELAY_BADGE": "Runs after {delay}",
"DELAY_DISABLED_BANNER": "Waits are disabled for this account. Rules with a wait won't run until it is enabled again."
},
diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/AutomationRuleForm.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/AutomationRuleForm.vue
index 10fe92b9d..b3b604ad1 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/automation/AutomationRuleForm.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/automation/AutomationRuleForm.vue
@@ -80,8 +80,14 @@ const DELAY_UNITS = [
];
const MIN_DELAY_MINUTES = 10;
const MAX_DELAY_MINUTES = 43200; // 30 days
-// Conversation-level delayed rules can filter on status plus immutable attributes (inbox).
-const DELAYED_CONVERSATION_ATTRS = ['status', 'inbox_id'];
+// The only valid delayed (wait) rule shapes: each supported event maps to the conditions that
+// stay meaningful across the wait — status / message_type re-checked at fire time, plus the
+// immutable inbox for scoping. Every other event/condition is hidden while the wait is on.
+const DELAYED_EVENT_ATTRS = {
+ conversation_updated: ['status', 'inbox_id'],
+ message_created: ['message_type', 'inbox_id'],
+};
+const DELAYED_EVENTS = Object.keys(DELAYED_EVENT_ATTRS);
const { t } = useI18n();
const { isCloudFeatureEnabled } = useAccount();
@@ -129,11 +135,10 @@ const filterTypes = computed(() => {
if (!event || !props.automationTypes[event]) return [];
let attributes = getTranslatedAttributes(props.automationTypes, event);
- // A delayed conversation-level rule can filter only on status and immutable attributes (inbox).
- if (isDelayed.value && event !== 'message_created') {
- attributes = attributes.filter(attr =>
- DELAYED_CONVERSATION_ATTRS.includes(attr.key)
- );
+ // A delayed rule can only filter on the attributes that stay meaningful across the wait.
+ if (isDelayed.value) {
+ const allowed = DELAYED_EVENT_ATTRS[event] || [];
+ attributes = attributes.filter(attr => allowed.includes(attr.key));
}
return attributes.map(attr => {
@@ -174,12 +179,15 @@ const filterTypes = computed(() => {
});
});
-const automationRuleEvents = computed(() =>
- AUTOMATION_RULE_EVENTS.map(event => ({
+const automationRuleEvents = computed(() => {
+ const events = isDelayed.value
+ ? AUTOMATION_RULE_EVENTS.filter(event => DELAYED_EVENTS.includes(event.key))
+ : AUTOMATION_RULE_EVENTS;
+ return events.map(event => ({
...event,
value: t(`AUTOMATION.EVENTS.${event.value}`),
- }))
-);
+ }));
+});
const hasAutomationMutated = computed(() => {
return Boolean(
@@ -221,15 +229,15 @@ const delayRestrictionReason = computed(() => {
) {
return 'ATTRIBUTE_CHANGED';
}
+ const allowed = DELAYED_EVENT_ATTRS[eventName.value];
+ if (!allowed) return 'UNSUPPORTED_EVENT';
if (
- eventName.value !== 'message_created' &&
conditions.some(
condition =>
- condition.attribute_key &&
- !DELAYED_CONVERSATION_ATTRS.includes(condition.attribute_key)
+ condition.attribute_key && !allowed.includes(condition.attribute_key)
)
) {
- return 'CONVERSATION_NON_STATUS';
+ return 'UNSUPPORTED_CONDITION';
}
return null;
});
@@ -311,6 +319,16 @@ const resetToSupportedCondition = () => {
];
};
+// A delay narrows the event list, so if the wait is turned on while an unsupported event is
+// selected (e.g. conversation_opened), switch to a meaningful default and reset its conditions
+// and actions the same way the event dropdown would.
+watch(isDelayed, delayed => {
+ if (!delayed || !automation.value) return;
+ if (DELAYED_EVENTS.includes(automation.value.event_name)) return;
+ automation.value.event_name = DELAYED_EVENTS[0];
+ props.onEventChange();
+});
+
// A delay narrows the condition options, so whenever the rule becomes unsupported while the
// wait is on — toggling it on, or switching to an event whose default condition isn't allowed
// (e.g. conversation_opened defaults to browser_language) — reset to a supported default.
diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue
index 490624b2f..8ff93b0fb 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue
@@ -35,6 +35,31 @@ const filteredRecords = computed(() => {
if (!query) return records.value;
return picoSearch(records.value, query, ['name', 'description']);
});
+
+// Delayed (wait) rules run on a different lifecycle, so list them in their own section.
+const hasDelayedRecords = computed(() =>
+ records.value.some(automation => automation.execution_delay)
+);
+
+const sections = computed(() => {
+ const instant = [];
+ const delayed = [];
+ filteredRecords.value.forEach(automation =>
+ (automation.execution_delay ? delayed : instant).push(automation)
+ );
+ return [
+ {
+ key: 'instant',
+ label: t('AUTOMATION.LIST.SECTIONS.INSTANT'),
+ items: instant,
+ },
+ {
+ key: 'delayed',
+ label: t('AUTOMATION.LIST.SECTIONS.DELAYED'),
+ items: delayed,
+ },
+ ].filter(section => section.items.length);
+});
const uiFlags = computed(() => getters['automations/getUIFlags'].value);
const accountId = computed(() => getters.getCurrentAccountId.value);
@@ -226,25 +251,43 @@ const tableHeaders = computed(() => {
>
{{ $t('AUTOMATION.LIST.DELAY_DISABLED_BANNER') }}
+
+
+
+ {{ section.label }}
+
+
+
+
+
+
+
+
-
-
-
+
diff --git a/app/models/automation_rule.rb b/app/models/automation_rule.rb
index e4aa56da6..ae0bb948b 100644
--- a/app/models/automation_rule.rb
+++ b/app/models/automation_rule.rb
@@ -23,9 +23,13 @@ class AutomationRule < ApplicationRecord
include Reauthorizable
EXECUTION_DELAY_RANGE = (10..43_200) # minutes: 10 min to 30 days
- # Conversation-level delayed rules key their episode on status; only status and attributes
- # that never change after the delay (inbox) are safe to also filter on.
- DELAYED_CONVERSATION_ATTRIBUTES = %w[status inbox_id].freeze
+ # The only valid delayed (wait) rule shapes: each supported event maps to the conditions that
+ # stay meaningful across the wait — status / message_type re-checked at fire time, plus the
+ # immutable inbox for scoping. Every other event or condition is rejected for delayed rules.
+ DELAYED_EVENT_ATTRIBUTES = {
+ 'conversation_updated' => %w[status inbox_id],
+ 'message_created' => %w[message_type inbox_id]
+ }.freeze
belongs_to :account
has_many :pending_executions, class_name: 'AutomationRulePendingExecution', dependent: :delete_all
@@ -116,13 +120,16 @@ class AutomationRule < ApplicationRecord
errors.add(:execution_delay, 'cannot be used with attribute_changed conditions.')
end
- # Conversation-level episodes key on status_changed_at alone. Mutable attributes would collapse
- # distinct periods into one episode, so only status and immutable filters (inbox) are allowed.
+ # Delayed rules are limited to the shapes whose episode keys stay meaningful across the wait:
+ # conversation_updated on status/inbox, and message_created on message_type/inbox.
def execution_delay_supported_event
- return if execution_delay.blank? || conditions.blank? || event_name == 'message_created'
- return if conditions.all? { |obj| DELAYED_CONVERSATION_ATTRIBUTES.include?(obj['attribute_key']) }
+ return if execution_delay.blank?
- errors.add(:execution_delay, 'only supports status and inbox conditions for conversation-level events.')
+ allowed = DELAYED_EVENT_ATTRIBUTES[event_name]
+ return errors.add(:execution_delay, 'is only supported for conversation_updated and message_created events.') if allowed.nil?
+ return if conditions.blank? || conditions.all? { |obj| allowed.include?(obj['attribute_key']) }
+
+ errors.add(:execution_delay, "only supports #{allowed.join(' and ')} conditions for #{event_name} events.")
end
def execution_config_changed?