(log: WorkflowExecutionLog)
| 85 | * itself never emit (loop prevention). |
| 86 | */ |
| 87 | export async function emitExecutionCompletedEvent(log: WorkflowExecutionLog): Promise<void> { |
| 88 | try { |
| 89 | if (!log.workflowId) return |
| 90 | if (log.trigger === SIM_TRIGGER_PROVIDER) return |
| 91 | |
| 92 | const workflowContext = await getActiveWorkflowContext(log.workflowId) |
| 93 | if (!workflowContext?.workspaceId) return |
| 94 | |
| 95 | const subscriptions = await fetchSimTriggerSubscriptions(workflowContext.workspaceId) |
| 96 | if (subscriptions.length === 0) return |
| 97 | |
| 98 | const executionData = (log.executionData ?? {}) as Record<string, unknown> |
| 99 | const context: ExecutionEventContext = { |
| 100 | workflowId: log.workflowId, |
| 101 | executionId: log.executionId, |
| 102 | status: log.level === 'error' ? 'error' : 'success', |
| 103 | durationMs: log.totalDurationMs || 0, |
| 104 | cost: (log.cost as { total?: number } | undefined)?.total || 0, |
| 105 | finalOutput: executionData.finalOutput, |
| 106 | } |
| 107 | |
| 108 | for (const subscription of subscriptions) { |
| 109 | const config = parseSubscriptionConfig(subscription.webhook.providerConfig) |
| 110 | if (!config) continue |
| 111 | if (config.eventType === 'workflow_deployed') continue |
| 112 | // no_activity is owned by the inactivity poller and can never fire from |
| 113 | // a completed execution; skip before the rule branch costs a cooldown |
| 114 | // read on this hot path. |
| 115 | if (config.eventType === 'no_activity') continue |
| 116 | |
| 117 | if (subscription.webhook.workflowId === log.workflowId) continue |
| 118 | if (!matchesWorkflowScope(config, log.workflowId)) continue |
| 119 | |
| 120 | if (config.eventType === 'execution_success' && context.status !== 'success') continue |
| 121 | if (config.eventType === 'execution_error' && context.status !== 'error') continue |
| 122 | |
| 123 | if (isSimRuleEventType(config.eventType)) { |
| 124 | const blockKey = subscriptionBlockKey(subscription) |
| 125 | |
| 126 | const lastFiredAt = await readLastFiredAt(subscription.webhook.workflowId, blockKey, '') |
| 127 | if (isWithinCooldown(lastFiredAt, SIM_RULE_COOLDOWN_MS)) continue |
| 128 | |
| 129 | const ruleFired = await evaluateRule(config.eventType, config, context) |
| 130 | if (!ruleFired) continue |
| 131 | |
| 132 | const claimed = await claimCooldown( |
| 133 | subscription.webhook.workflowId, |
| 134 | blockKey, |
| 135 | '', |
| 136 | SIM_RULE_COOLDOWN_MS |
| 137 | ) |
| 138 | if (!claimed) continue |
| 139 | |
| 140 | logger.info(`Sim trigger rule ${config.eventType} fired`, { |
| 141 | subscriberWorkflowId: subscription.webhook.workflowId, |
| 142 | sourceWorkflowId: log.workflowId, |
| 143 | executionId: log.executionId, |
| 144 | }) |
no test coverage detected