Skip to content

feat(workflow): add external workflow triggers (webhook, API, events)#400

Merged
larryro merged 42 commits into
mainfrom
feat/322-external-workflow-triggers
Feb 8, 2026
Merged

feat(workflow): add external workflow triggers (webhook, API, events)#400
larryro merged 42 commits into
mainfrom
feat/322-external-workflow-triggers

Conversation

@larryro
Copy link
Copy Markdown
Collaborator

@larryro larryro commented Feb 8, 2026

Closes #322

Summary

  • Trigger system: Add webhook, API key, schedule, and event-based triggers that attach to workflow root versions (version-agnostic) and always execute the active version
  • Start step refactor: Replace trigger step type with start step, separating input schema definition from trigger source configuration. Includes migration for existing workflows
  • Triggers UI: Full management interface for schedules (with AI cron generation), webhooks, API keys, and event subscriptions with secret reveal dialogs
  • HTTP endpoints: Webhook (/api/workflows/wh/{token}) and API (/api/workflows/trigger) endpoints with HMAC verification, Bearer auth, rate limiting, and idempotency support
  • Event system: Internal event-based triggers with subscription management and event type registry
  • Workflow lifecycle: Add unpublish/republish actions and paginated automations list
  • Refactors: Rename actions/ to action_defs/ to avoid Convex naming conflicts, consolidate trigger modules, move improveMessage to conversations module, remove unused streaming code

Test plan

  • Verify schedule triggers create wfSchedules records and fire on cron
  • Test webhook flow: create webhook, POST with valid HMAC → execution starts; invalid signature → 401
  • Test API key flow: create key, POST with Bearer token → execution starts; invalid key → 401
  • Test event triggers: subscribe to event type, emit event → workflow executes
  • Verify version-agnostic behavior: publish new version → triggers execute latest active version
  • Verify rate limiting returns 429 when exceeded
  • Test unpublish/republish actions from automation row actions menu
  • Verify triggers UI: create/delete schedules, webhooks, API keys, event subscriptions
  • Verify existing predefined workflows still work with start step type

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added automation publish/unpublish (activate/deactivate) capabilities.
    • Added comprehensive trigger management: schedules, webhooks, events, and API keys for automations.
    • Added event-driven workflow automation support.
    • Added date picker preset ranges for filters.
    • Added multi-select toggle per filter for more flexible data table filtering.
  • Refactors

    • Renamed workflow step types for improved clarity in automation UI.
    • Enhanced automation listing with paginated results and dynamic status resolution.
    • Simplified execution filtering with optimized query indexes.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Feb 8, 2026

📝 Walkthrough

Walkthrough

This PR implements a comprehensive workflow trigger system overhaul. It renames the "trigger" step type to "start" throughout the codebase and adds support for external workflow triggers (scheduled, webhooks, API keys) via new database tables (wfSchedules, wfWebhooks, wfApiKeys, wfEventSubscriptions, wfTriggerLogs). HTTP endpoints are added for webhook and API-based trigger execution with rate-limiting and idempotency support. The scheduler is refactored to read from wfSchedules instead of embedded trigger configs. UI components are provided for managing schedules, webhooks, and event subscriptions. Supporting infrastructure includes event emission for workflow lifecycle and entity changes, new validation schemas for the start step, and security helpers for token/key generation and hashing.

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.44% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title clearly and concisely describes the main feature addition: external workflow triggers (webhook, API, events), which directly aligns with the primary objective of the changeset.
Linked Issues check ✅ Passed The PR implements all major requirements from issue #322: start step refactor, version-agnostic triggers (webhook, API, schedules), new trigger DB tables with proper indexes, HTTP endpoints with auth/idempotency/rate-limiting, CRUD mutations, security helpers, event-based triggers, and unpublish/republish workflow actions.
Out of Scope Changes check ✅ Passed The PR includes several changes beyond the core trigger system scope: renaming actions/ to action_defs/, moving improveMessage to conversations module, removing HTTP streaming, and consolidating trigger modules. While these are refactoring/cleanup changes, they are closely related to the core trigger implementation and appear intentional based on the PR objectives summary.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/322-external-workflow-triggers

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 37

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (27)
services/platform/convex/predefined_workflows/website_scan.ts (2)

38-46: 🧹 Nitpick | 🔵 Trivial

Update stale comment to reflect the new step type.

The comment still references "Trigger - Manual or Scheduled" but the step is now a start step with no trigger configuration. Update the comment to match the new semantics.

📝 Proposed fix
-    // Step 1: Trigger - Manual or Scheduled
+    // Step 1: Start - Workflow Entry Point
     {
       stepSlug: 'start',
       name: 'start',
       stepType: 'start',
       order: 1,
       config: {},
       nextSteps: { success: 'fetch_main_page' },
     },

16-16: 🧹 Nitpick | 🔵 Trivial

Use explicit type annotation and named export.

Per project conventions for predefined workflows, export as a named constant with an explicit PredefinedWorkflowDefinition type rather than using a default export. This reduces the need for import-site casts.

♻️ Proposed fix
-const websiteScanWorkflow = {
+import { PredefinedWorkflowDefinition } from '../types';
+
+export const websiteScanWorkflow: PredefinedWorkflowDefinition = {

At the end of the file:

-export default websiteScanWorkflow;

Based on learnings: "In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports."

Also applies to: 293-293

services/platform/convex/predefined_workflows/loopi_customer_status_assessment.ts (1)

36-247: 🧹 Nitpick | 🔵 Trivial

Use named export with explicit PredefinedWorkflowDefinition type.

The workflow uses a default export, but the project convention is to export predefined workflows as named constants with an explicit type annotation.

♻️ Proposed fix
+import type { PredefinedWorkflowDefinition } from '../workflows/types';
+
-const loopiCustomerStatusAssessmentWorkflow = {
+export const loopiCustomerStatusAssessmentWorkflow: PredefinedWorkflowDefinition = {
   workflowConfig: {
     // ... rest of config
   },
   stepsConfig: [
     // ... steps
   ],
 };
-
-export default loopiCustomerStatusAssessmentWorkflow;

Based on learnings: "In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports."

services/platform/convex/predefined_workflows/general_product_recommendation.ts (1)

3-3: 🛠️ Refactor suggestion | 🟠 Major

Replace default export with named export.
Predefined workflows in this directory should be exported as named constants to avoid import-site casts and align with repo conventions.

♻️ Proposed refactor
-const generalProductRecommendationWorkflow: PredefinedWorkflowDefinition = {
+export const generalProductRecommendationWorkflow: PredefinedWorkflowDefinition = {
   workflowConfig: {
     name: 'General Product Recommendation',
     description:
       'Generate AI-powered product recommendations for customers using their full customer record and product relationships',
     workflowType: 'predefined',
     version: '1.0.0',
     config: {
       timeout: 300000,
       retryPolicy: { maxRetries: 2, backoffMs: 3000 },
       variables: {
         organizationId: 'org_demo',
         workflowId: 'general-product-recommendation',
         backoffHours: 168,
       },
     },
   },
   stepsConfig: [
     {
       stepSlug: 'start',
       name: 'Start',
       stepType: 'start',
       order: 1,
       config: {},
       nextSteps: { success: 'find_unprocessed_customer' },
     },
     ...
   ],
 };
-
-export default generalProductRecommendationWorkflow;

Based on learnings, in any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports.

Also applies to: 279-279

services/platform/convex/predefined_workflows/product_relationship_analysis.ts (2)

44-52: ⚠️ Potential issue | 🟡 Minor

Update stale comment to reflect the new step type.

The comment on line 44 still references "Trigger - Manual or Scheduled" but this step has been migrated to a start step. The comment should be updated for consistency.

📝 Suggested fix
-    // Step 1: Trigger - Manual or Scheduled
+    // Step 1: Start - Workflow entry point

25-28: 🧹 Nitpick | 🔵 Trivial

Consider using a named export with an explicit type annotation.

The current default export pattern doesn't align with the project convention for predefined workflows.

♻️ Suggested refactor

At line 25, add the type annotation:

+import type { PredefinedWorkflowDefinition } from '../types';
+
-const productRelationshipAnalysisWorkflow = {
+export const productRelationshipAnalysisWorkflow: PredefinedWorkflowDefinition = {

At line 528, remove the default export:

-export default productRelationshipAnalysisWorkflow;

Based on learnings: "In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports."

Also applies to: 528-528

services/platform/convex/predefined_workflows/loopi_product_recommendation.ts (1)

33-33: 🛠️ Refactor suggestion | 🟠 Major

Use named export with explicit PredefinedWorkflowDefinition type.
This file still default-exports and lacks the explicit type, which conflicts with the predefined workflow export policy.

Suggested update
+import type { PredefinedWorkflowDefinition } from './types'; // adjust path to match existing predefined workflows
-const loopiProductRecommendationWorkflow = {
+export const loopiProductRecommendationWorkflow: PredefinedWorkflowDefinition = {
   workflowConfig: {
     name: 'Product Recommendation',
     description:
       'Generate AI-powered product recommendations for active customers based on their subscription history and product relationships',
     workflowType: 'predefined', // Predefined workflow - developer-defined, user provides credentials
     version: '2.0.0',
     config: {
       timeout: 300000, // 5 minutes total timeout
       retryPolicy: { maxRetries: 2, backoffMs: 3000 },
       variables: {
         organizationId: 'org_demo',
         workflowId: 'product-recommendation',
         backoffHours: 168, // Process each customer once per week (7 days * 24 hours)
       },
     },
   },
   stepsConfig: [
     // ...
   ],
 };
-
-export default loopiProductRecommendationWorkflow;

Based on learnings: In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports.

Also applies to: 533-533

services/platform/convex/predefined_workflows/email_sync_imap.ts (2)

52-52: 🧹 Nitpick | 🔵 Trivial

Stale comment: references "Trigger" but step is now "start".

The comment says "Step 1: Trigger - Manual" but the step type has been changed to start. Consider updating to "Step 1: Start" for consistency.


243-245: 🧹 Nitpick | 🔵 Trivial

Consider using a typed const export instead of default export.

Based on learnings, workflows in predefined_workflows/*.ts should export as a constant with an explicit PredefinedWorkflowDefinition type and avoid default exports. This reduces the need for import-site casts and aligns with the project's no-casting policy.

♻️ Proposed refactor
+import type { PredefinedWorkflowDefinition } from '../types';
+
-const emailSyncImapWorkflow = {
+export const emailSyncImapWorkflow: PredefinedWorkflowDefinition = {
  // ... workflow definition
};

-export default emailSyncImapWorkflow;
services/platform/convex/predefined_workflows/workflow_rag_sync.ts (2)

29-29: 🧹 Nitpick | 🔵 Trivial

Stale comment: references "Trigger" but step is now "start".

Update comment from "Step 1: Trigger - Manual or Scheduled" to reflect the new start step type.


59-59: 🧹 Nitpick | 🔵 Trivial

Consider using a typed const export instead of default export.

Per project convention, predefined workflows should use typed const exports with PredefinedWorkflowDefinition type annotation. Based on learnings, this reduces import-site casts and aligns with the project's no-casting policy.

services/platform/convex/predefined_workflows/customer_rag_sync.ts (2)

32-32: 🧹 Nitpick | 🔵 Trivial

Stale comment: references "Trigger" but step is now "start".

Update comment from "Step 1: Trigger - Manual" to reflect the new start step type.


123-123: 🧹 Nitpick | 🔵 Trivial

Consider using a typed const export instead of default export.

Per project convention, predefined workflows should use typed const exports with PredefinedWorkflowDefinition type annotation. Based on learnings: "In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports."

services/platform/convex/workflow_engine/helpers/validation/constants.ts (1)

4-5: 🧹 Nitpick | 🔵 Trivial

Stale comment: references "trigger types" but those were removed.

The comment mentions "step types and trigger types" but the trigger type utilities (VALID_TRIGGER_TYPES, TriggerType, isValidTriggerType) were removed. Update to reflect the current scope.

📝 Proposed fix
- * This is the single source of truth for step types and trigger types.
+ * This is the single source of truth for step types.
services/platform/convex/predefined_workflows/website_pages_rag_sync.ts (1)

16-113: 🧹 Nitpick | 🔵 Trivial

Align predefined workflow exports with the repo convention.

Export a typed const and avoid default exports to remove downstream casting pressure.

Proposed fix
+import type { PredefinedWorkflowDefinition } from '../workflows/definitions/types';
-
-const websitePagesRagSyncWorkflow = {
+export const websitePagesRagSyncWorkflow: PredefinedWorkflowDefinition = {
   workflowConfig: {
     name: 'Website Pages RAG Sync',
@@
   ],
 };
-
-export default websitePagesRagSyncWorkflow;

Based on learnings: In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports.

services/platform/convex/predefined_workflows/circuly_sync_customers.ts (1)

16-236: 🧹 Nitpick | 🔵 Trivial

Align predefined workflow exports with the repo convention.

Export a typed const and avoid default exports to remove downstream casting pressure.

Proposed fix
+import type { PredefinedWorkflowDefinition } from '../workflows/definitions/types';
-
-const circulySyncCustomersWorkflow = {
+export const circulySyncCustomersWorkflow: PredefinedWorkflowDefinition = {
   workflowConfig: {
     name: 'Circuly Customers Sync',
@@
   ],
 };
-
-export default circulySyncCustomersWorkflow;

Based on learnings: In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports.

services/platform/convex/predefined_workflows/circuly_sync_products.ts (1)

17-17: 🛠️ Refactor suggestion | 🟠 Major

Switch to a named, explicitly typed workflow export.
This file still default-exports an untyped workflow constant. Please export a named constant with an explicit PredefinedWorkflowDefinition type to avoid import-site casts.

♻️ Suggested export shape
-const circulySyncProductsWorkflow = {
+export const circulySyncProductsWorkflow: PredefinedWorkflowDefinition = {
   ...
-};
-
-export default circulySyncProductsWorkflow;
+};

Based on learnings: In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports. This reduces the need for import-site casts and aligns with the project’s no-casting policy.

Also applies to: 239-239

services/platform/convex/predefined_workflows/email_sync_sent_imap.ts (1)

22-22: 🛠️ Refactor suggestion | 🟠 Major

Switch to a named, explicitly typed workflow export.
This file still default-exports an untyped workflow constant. Please export a named constant with an explicit PredefinedWorkflowDefinition type to avoid import-site casts.

♻️ Suggested export shape
-const emailSyncSentImapWorkflow = {
+export const emailSyncSentImapWorkflow: PredefinedWorkflowDefinition = {
   ...
-};
-
-export default emailSyncSentImapWorkflow;
+};

Based on learnings: In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports. This reduces the need for import-site casts and aligns with the project’s no-casting policy.

Also applies to: 262-262

services/platform/convex/workflow_engine/helpers/scheduler/scan_and_trigger.ts (1)

60-80: ⚠️ Potential issue | 🟡 Minor

Don’t treat last-triggered update failures as trigger failures.
If updateScheduleLastTriggered throws after startWorkflow succeeds, the catch logs “Failed to trigger workflow” and skips triggeredCount, which misreports successful triggers. Consider isolating the update in its own try/catch and reusing a single timestamp for both calls.

🔧 Suggested change
-        if (shouldTrigger) {
+        if (shouldTrigger) {
+          const triggeredAt = Date.now();
           debugLog(
             `Triggering scheduled workflow: ${name} (${wfDefinitionId})`,
           );

           await ctx.runMutation(internal.workflow_engine.internal_mutations.startWorkflow, {
             organizationId,
             wfDefinitionId,
             input: {},
             triggeredBy: 'schedule',
             triggerData: {
               triggerType: 'schedule',
               schedule,
-              timestamp: Date.now(),
+              timestamp: triggeredAt,
             },
           });

-          await ctx.runMutation(
-            internal.workflows.triggers.internal_mutations.updateScheduleLastTriggered,
-            { scheduleId, lastTriggeredAt: Date.now() },
-          );
-
           triggeredCount++;
+          try {
+            await ctx.runMutation(
+              internal.workflows.triggers.internal_mutations.updateScheduleLastTriggered,
+              { scheduleId, lastTriggeredAt: triggeredAt },
+            );
+          } catch (error) {
+            console.warn(
+              `Failed to update lastTriggeredAt for schedule ${scheduleId}:`,
+              error,
+            );
+          }
         }
services/platform/convex/predefined_workflows/shopify_sync_products.ts (1)

16-17: 🛠️ Refactor suggestion | 🟠 Major

Use a typed named export for predefined workflows.

Please export this workflow as a PredefinedWorkflowDefinition-typed constant and avoid default exports to prevent import-site casts and stay consistent with repo conventions.

Based on learnings: In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports.

Also applies to: 236-238

services/platform/app/features/automations/components/automation-step.tsx (1)

72-80: ⚠️ Potential issue | 🟡 Minor

Keep a localized label for legacy trigger steps.

If any workflows still contain trigger, the label now falls back to raw "trigger". Consider keeping a trigger label mapping (or explicitly map to start) to avoid unlocalized UI.

✅ Suggested update
-    const labels: Record<string, string> = {
-      start: t('stepTypes.start'),
+    const labels: Record<string, string> = {
+      start: t('stepTypes.start'),
+      trigger: t('stepTypes.trigger'),
services/platform/convex/predefined_workflows/product_rag_sync.ts (1)

15-16: 🛠️ Refactor suggestion | 🟠 Major

Use a typed named export for predefined workflows.

Please export this workflow as a PredefinedWorkflowDefinition-typed constant and avoid default exports to keep imports cast-free and consistent.

Based on learnings: In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports.

Also applies to: 123-123

services/platform/convex/predefined_workflows/conversation_auto_archive.ts (1)

25-25: 🧹 Nitpick | 🔵 Trivial

Use explicit type annotation and named export per project conventions.

The workflow should be explicitly typed and use a named export to align with the codebase conventions and avoid import-site casts.

♻️ Proposed fix
-const conversationAutoArchiveWorkflow: PredefinedWorkflowDefinition = {
+import type { PredefinedWorkflowDefinition } from '../workflows/definitions/types';
+
+export const conversationAutoArchiveWorkflow: PredefinedWorkflowDefinition = {

At the end of the file:

-export default conversationAutoArchiveWorkflow;
+// Remove default export - use named export above

Based on learnings: "In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports."

Also applies to: 171-171

services/platform/convex/predefined_workflows/shopify_sync_customers.ts (1)

16-16: 🧹 Nitpick | 🔵 Trivial

Use explicit type annotation and named export per project conventions.

Similar to other predefined workflows, this should use an explicit PredefinedWorkflowDefinition type and named export.

♻️ Proposed fix
+import type { PredefinedWorkflowDefinition } from '../workflows/definitions/types';
+
-const shopifySyncCustomersWorkflow = {
+export const shopifySyncCustomersWorkflow: PredefinedWorkflowDefinition = {

At end of file:

-export default shopifySyncCustomersWorkflow;

Based on learnings: "In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports."

Also applies to: 223-223

services/platform/convex/predefined_workflows/conversation_auto_reply.ts (1)

29-29: 🧹 Nitpick | 🔵 Trivial

Use explicit type annotation and named export per project conventions.

Consistent with other predefined workflows, this should use explicit typing and named export.

♻️ Proposed fix
+import type { PredefinedWorkflowDefinition } from '../workflows/definitions/types';
+
-const conversationAutoReplyWorkflow = {
+export const conversationAutoReplyWorkflow: PredefinedWorkflowDefinition = {

At end of file:

-export default conversationAutoReplyWorkflow;

Based on learnings: "In any file under services/platform/convex/predefined_workflows with a .ts extension, export each workflow as a constant with an explicit type of PredefinedWorkflowDefinition and avoid default exports."

Also applies to: 587-587

services/platform/convex/workflow_engine/types/workflow.ts (1)

134-152: ⚠️ Potential issue | 🟡 Minor

Type/validator mismatch: agent is in validator but not in interface.

The workflowStepValidator includes v.literal('agent') (line 142), but the WorkflowStep interface (line 54) doesn't include 'agent' in its stepType union. This inconsistency could cause runtime values to pass validation but fail TypeScript type checking.

🔧 Either add 'agent' to the interface or remove from validator

Option 1: Add to interface (if agent steps are supported)

 export interface WorkflowStep {
   _id: string;
   wfDefinitionId: string;
   stepSlug: string;
   name: string;
-  stepType: 'start' | 'trigger' | 'llm' | 'condition' | 'action' | 'loop';
+  stepType: 'start' | 'trigger' | 'agent' | 'llm' | 'condition' | 'action' | 'loop';
   order: number;

Option 2: Remove from validator (if agent steps are not used)

   stepType: v.union(
     v.literal('start'),
     v.literal('trigger'),
-    v.literal('agent'),
     v.literal('llm'),
     v.literal('condition'),
services/platform/app/features/automations/components/automation-navigation.tsx (1)

244-247: ⚠️ Potential issue | 🟡 Minor

Use the automations‑navigation archived label key.

This label should use the automations navigation string to keep context-specific wording consistent.

🔧 Suggested change
-                    {version.status === 'archived' &&
-                      tCommon('status.archived')}
+                    {version.status === 'archived' &&
+                      t('navigation.archived')}

Based on learnings: In automations-related UI components, prefer using the navigation-specific localization key t('navigation.archived') instead of a generic status key like tCommon('status.archived') when the wording conveys a contextual meaning unique to automations navigation.

🤖 Fix all issues with AI agents
In
`@services/platform/app/features/automations/components/automation-navigation.tsx`:
- Around line 184-187: handleUnpublish currently silently returns when
automationId or user?.userId is missing; update the function to show the same
destructive toast used in the publish/draft flows instead of doing a no-op.
Locate handleUnpublish in automation-navigation.tsx and, when either
automationId or user?.userId is falsy, call the existing destructive toast
helper (the same one used by the publish/draft handlers) with a clear message
like "Unable to unpublish: missing automation or user" and then return; keep the
current early-return behavior otherwise.

In
`@services/platform/app/features/automations/components/automations-client.tsx`:
- Around line 60-62: The archived option in the select options uses the generic
key tCommon('status.archived'); update it to use the automations
navigation-specific localization key instead (call the component's i18n function
t with 'navigation.archived' or the module's navigation key as used elsewhere)
so the third option becomes { value: 'archived', label: t('navigation.archived')
} (ensure you use the same t import/namespace as other navigation labels in
automations-client.tsx).

In
`@services/platform/app/features/automations/triggers/components/collapsible-section.tsx`:
- Around line 26-44: The <h3 id={headingId}> must not be nested inside the
<button> in CollapsibleSection; move the heading element to wrap the button and
render the title as a non-heading inline element (e.g. a <span>) inside the
button. Specifically, in services/platform/.../collapsible-section.tsx adjust
the markup so the outer element is <h3 id={headingId}> (keeping the same
headingId), the <button> with aria-expanded/aria-controls/onClick (using isOpen
and setIsOpen) lives inside that h3, and the visible title text uses a span
instead of <h3>; keep ChevronRight, Icon, and all ARIA attributes unchanged so
accessibility semantics are preserved.

In
`@services/platform/app/features/automations/triggers/components/event-create-dialog.tsx`:
- Around line 279-294: The FilterFieldInput currently returns null for
unsupported field.inputType (the final return) which silently ignores
misconfigured fields; update the FilterFieldInput component to handle unknown
inputType by either rendering a simple fallback input (e.g., a plain
TextInput/select placeholder) and/or logging a warning in development mode;
specifically add a branch after the known cases that calls console.warn or a
logger with the field key/label and inputType (reference FilterFieldInput,
handleChange, selectValue, and field) so misconfigurations are visible while
preserving existing behavior in production.
- Around line 61-66: The current useEffect only initializes state when editing
is truthy, so opening the dialog in create mode can retain stale values; update
the component to also reset form state when open becomes true and editing is
falsy by adding logic (either in the existing useEffect or a new useEffect that
watches [open, editing]) to call setSelectedEventType(defaultValue) and
setFilterValues({}) (or the component's initial defaults) whenever open === true
&& !editing; reference the existing useEffect, the handleOpenChange handler, and
the state setters setSelectedEventType and setFilterValues to implement the
reset.

In
`@services/platform/app/features/automations/triggers/components/events-section.tsx`:
- Around line 108-116: Replace the hardcoded toast title in the catch block of
the delete handler in events-section.tsx with the i18n call (use the injected t
function) — e.g., change toast({ title: 'Failed to delete event subscription',
... }) to toast({ title: t('automations.events.deleteFailed'), variant:
'destructive' }); and add the corresponding translation key/value to the
appropriate locale file; ensure the catch remains unchanged otherwise and that t
is imported/available in the component.
- Around line 88-96: Replace the hardcoded English toast title inside the catch
block with a translated string using the existing i18n function t(); locate the
catch where toast(...) is called (the closure that depends on
toggleSubscription, toast, t) and change the title to call t with an appropriate
translation key (e.g. t('automations.events.toggle_subscription_failed') or the
project’s existing key) so the error toast uses i18n consistently while keeping
the same variant and payload shape.

In
`@services/platform/app/features/automations/triggers/components/schedule-create-dialog.tsx`:
- Around line 70-83: The current client-side validation in the schema created
inside useMemo (z.object with cronExpression) only checks for five
whitespace-separated fields and allows invalid values like "99 99 99 99 99";
replace this weak regex by either integrating a cron validation library (e.g.,
cron-validate or cron-parser) or adding a stricter validation step: keep the
z.string().trim().min(1, ...) then use z.string().refine(async/ sync -> boolean,
'Invalid cron expression') to call the chosen validator, referencing the
cronExpression field and rejecting values outside valid ranges; update any
imports and error message text accordingly so the user receives accurate
client-side feedback before submission.
- Around line 217-225: Replace the non-semantic <p> used to show cronDescription
with a semantic <output> in the ScheduleCreateDialog component: locate the JSX
that renders {cronDescription} and change the <p className="text-xs
text-muted-foreground" role="status" aria-live="polite"> to an <output> element
(e.g., <output className="text-xs text-muted-foreground" aria-live="polite">) so
the description is exposed as a result of user action; keep the same className
and aria-live, and remove or adjust the explicit role="status" since <output> is
the correct semantic element for result content.
- Around line 185-188: The component schedule-create-dialog.tsx is using unsafe
casts like `'triggers.schedules.form.ai.label' as any` for translation keys
(used in props label and placeholder), which indicates the keys are missing from
the i18n types; add these keys to the automations namespace type definitions
(the i18n locale/type file used by your project) so the keys are recognized,
then remove the `as any` casts from the ScheduleCreateDialog usages (label and
placeholder) so the t(...) calls are type-checked against the new definitions;
ensure the exact keys 'triggers.schedules.form.ai.label' and
'triggers.schedules.form.ai.placeholder' (or the correct key names in use) are
present in the type and translations.

In
`@services/platform/app/features/automations/triggers/components/schedules-section.tsx`:
- Around line 54-57: The catch blocks in schedules-section.tsx currently call
toast with hardcoded English strings ("Failed to toggle schedule" and the other
error toast) instead of using i18n; update both error toast calls to use the
translation function t with appropriate keys (e.g.,
t('automations.schedule.toggleFailed') or existing keys used for success
messages) so the messages are localized, preserving the same toast variant and
placement and keeping the calls inside the same catch blocks where
toggleSchedule, toast, and t are in scope.
- Around line 140-156: The Button aria-labels are hardcoded English strings;
update the two Buttons that call setEditSchedule and setDeleteTarget to use the
i18n translation function (t) instead of literal text — e.g. replace
aria-label="Edit schedule" and aria-label="Delete schedule" with
aria-label={t('triggers.schedules.editTitle')} and
aria-label={t('triggers.schedules.deleteTitle')} (or reuse the existing events
keys like t('triggers.events.editTitle')/t('triggers.events.deleteTitle') if
appropriate), ensuring the t function/hook already used elsewhere in this file
is imported/available.

In
`@services/platform/app/features/automations/triggers/components/secret-reveal-dialog.tsx`:
- Around line 75-83: The JSX uses a <label> inside the secrets.map rendering
(refer to secrets.map, secret.label in secret-reveal-dialog.tsx) without an
associated form control, which trips a11y linting; replace the <label
className="text-sm font-medium text-foreground"> element with a non-label
semantic element (e.g., <div> or <span>) keeping the same classes and content,
or alternatively associate it with a control by adding htmlFor and a matching id
on a control, so screen readers are not confused.

In
`@services/platform/app/features/automations/triggers/components/webhooks-section.tsx`:
- Around line 63-70: The client currently passes createdBy into createWebhook
from handleCreate; remove createdBy from the payload in webhooks-section.tsx and
stop sending any user identity from the frontend, then update the Convex
mutation (createWebhook handler) to derive the creator from the authenticated
session/context there (read the authenticated user in the Convex server function
and ignore any client-provided createdBy), ensuring createWebhook's server-side
implementation uses the session user id/email when creating the webhook record.

In `@services/platform/app/features/automations/triggers/triggers-client.tsx`:
- Around line 40-41: Remove the unnecessary type cast on workflowRootId: replace
the expression that casts the nullish-coalesced value to Id<'wfDefinitions'> and
instead rely on TypeScript's narrowing of (workflow.rootVersionId ??
workflow._id). Update the variable initialization where workflowRootId is
assigned (referencing workflow.rootVersionId and workflow._id) to remove the "as
Id<'wfDefinitions'>" cast so the code matches other backend usages.

In
`@services/platform/app/features/chat/components/workflow-creation-approval-card.tsx`:
- Around line 40-44: The switch in getStepTypeBadgeVariant currently maps legacy
step types like 'start' to 'blue' but misses mapping 'trigger', causing older
approvals to fall back to 'outline'; update getStepTypeBadgeVariant to treat
'trigger' the same as 'start' (return 'blue' for 'trigger') so legacy approvals
render with the intended blue badge variant.

In `@services/platform/convex/agent_tools/workflows/helpers/syntax_reference.ts`:
- Around line 58-66: The "start" step in the example skeleton currently includes
a full schedule config which conflicts with the new model where "start" is
inputSchema-only and trigger sources are configured elsewhere; update the
example JSON so the step with stepSlug "start" and stepType "start" has an empty
config (or only an inputSchema) instead of the embedded schedule object to
prevent agents from generating invalid scheduled configs.

In `@services/platform/convex/conversations/actions.ts`:
- Around line 15-21: The handler currently returns a success-shaped payload on
auth failure; change it to throw when unauthenticated: in the handler function
call authComponent.getAuthUser(ctx) and if it returns falsy throw an Error (e.g.
new Error('Unauthenticated')) instead of returning { improvedMessage, error };
then proceed to call improveMessageHandler(ctx, args) for authenticated users.
This update should be applied to the same handler where
authComponent.getAuthUser and improveMessageHandler are used so unauthenticated
calls fail at the action boundary.

In `@services/platform/convex/email_providers/save_related_workflows.ts`:
- Around line 109-112: The current mapping for steps replaces step.config for
'start'/'trigger' steps and discards existing fields like inputSchema; update
the transform so it merges schedule and timezone into the existing config
instead of replacing it (e.g., for steps where step.stepType === 'start' ||
step.stepType === 'trigger', set step.config = { ...step.config, type:
'scheduled', schedule, timezone } to preserve inputSchema and other config
fields); apply this change in the mapping that processes steps (the code
handling step and step.stepType).

In `@services/platform/convex/organizations/save_default_workflows.ts`:
- Around line 58-61: The transformation is incorrectly reintroducing schedule
config into start steps by setting start.config = { type: 'scheduled', ... };
update the mapper in save_default_workflows (the step => step.stepType ===
'start' || step.stepType === 'trigger' ... block) to avoid touching
start.config: only assign the scheduled config for trigger steps (step.stepType
=== 'trigger') and leave start steps' config untouched (or explicitly
remove/normalize legacy schedule shapes if you intentionally accept them);
instead, create schedule records during provisioning code that handles schedules
rather than injecting them into start step definitions.

In `@services/platform/convex/wf_definitions/queries.ts`:
- Around line 57-68: The listAutomationRoots query currently uses a hardcoded
take(200) which silently truncates results; update the queryWithRLS args and
handler to accept optional pagination parameters (e.g., limit:number and
afterCursor?:string) instead of the hardcoded cap, validate/clip limit (max
e.g., 200), use the provided limit in place of take(200) and return a pagination
envelope including items and a nextCursor; modify the unique symbols
listAutomationRoots, its args block, and the handler to implement cursor-based
pagination (or explicitly document the 200 cap and expose limit/cursor fields if
you prefer offset-style pagination).

In `@services/platform/convex/workflow_engine/execution/dry_run_executor.ts`:
- Around line 40-41: findStartStep currently returns the first step whose
stepType is 'start' or 'trigger', which makes selection order-dependent; update
the function (findStartStep, operating on StepDef[]) to first look specifically
for a step with stepType === 'start' and return it if found, otherwise fall back
to searching for stepType === 'trigger', and return null if neither exists so
the choice is deterministic and prefers 'start'.

In `@services/platform/convex/workflow_engine/helpers/validation/steps/start.ts`:
- Around line 45-47: The current validation only checks that schema.required is
an array; extend the check in the Start step validator where schema.required is
inspected to also validate each entry is a string (e.g., iterate over
schema.required and if any item typeof !== 'string' push an error). Update the
error message (for the check around schema.required) to reflect "must be an
array of strings if provided" and add a specific error when non-string items are
found so invalid JSON Schema can't be produced later.

In
`@services/platform/convex/workflow_engine/helpers/validation/validate_workflow_definition.ts`:
- Around line 146-147: The hasTrigger check accesses step.stepType directly and
can throw if stepsConfig contains null/non-object entries; update the predicate
used in the Array.some call (the hasTrigger computation) to first guard that
step is a non-null object (e.g., typeof step === 'object' && step !== null) or
use safe optional chaining (step?.stepType) before comparing to 'start' or
'trigger' so the check returns false for invalid entries instead of throwing.

In `@services/platform/convex/workflow_engine/types/nodes.ts`:
- Around line 215-235: The startNodeConfigValidator's inputSchema currently only
allows primitive types and flat property definitions; extend it to accept JSON
Schema null, nested array items, and nested object properties by updating
startNodeConfigValidator to broaden the property schema: allow v.literal('null')
in the type union, add an optional items field (reusing the same property-type
shape or a recursive schema) for arrays, and allow properties for object types
(i.e., make the value schema for properties recursive so nested properties and
their descriptions/required arrays are validated); reference
startNodeConfigValidator, inputSchema, properties, items, and required when
implementing the recursive/extended schema.

In `@services/platform/convex/workflows/definitions/save_manual_configuration.ts`:
- Line 22: Replace the hardcoded union for stepType with the canonical type from
the workflow schema: import and use Doc<'wfStepDefs'>['stepType'] (instead of
the literal 'start' | 'trigger' | 'llm' | 'condition' | 'action' | 'loop') so
stepType is derived from the schema; update the type declaration that currently
contains "stepType: 'start' | 'trigger' | 'llm' | 'condition' | 'action' |
'loop';" to reference Doc<'wfStepDefs'>['stepType'] and add the necessary import
for Doc where this type is defined.

In `@services/platform/convex/workflows/triggers/actions.ts`:
- Around line 32-61: The AI may return invalid cron strings; after calling
generateObject (the result variable from generateObject with openai(model)),
validate result.object.cronExpression using the existing
CronExpressionParser.parse() utility before returning; if parse throws or
indicates invalid syntax, throw a clear user-facing error (so downstream
shouldTriggerWorkflow won't silently fail) and only return the cronExpression
and description when validation succeeds.

In `@services/platform/convex/workflows/triggers/api_http.ts`:
- Around line 156-161: Change the HTTP response for accepted async executions to
use status code 202 instead of 200: update the call site where jsonResponse is
invoked returning { status: 'accepted', executionId, workflowRootId:
apiKeyRecord.workflowRootId, versionId: activeVersionId } to pass 202 as the
response code (the jsonResponse call in api_http.ts that builds the "accepted"
response).
- Around line 20-24: The jsonResponse function is missing CORS response headers
so browser clients are blocked; update jsonResponse to include the same CORS
headers sent for preflight (at minimum add "Access-Control-Allow-Origin": "*" or
echo the request Origin) and, if your preflight allows credentials, also add
"Access-Control-Allow-Credentials": "true" (and any other headers like
"Access-Control-Expose-Headers" you rely on) so every JSON Response returned by
jsonResponse includes CORS headers.

In `@services/platform/convex/workflows/triggers/event_types.ts`:
- Around line 86-90: The isValidEventType function currently uses the "in"
operator which can return true for prototype properties; change its check to use
Object.prototype.hasOwnProperty.call(EVENT_TYPES, type) to ensure only own keys
are validated (refer to isValidEventType and EVENT_TYPES). Also confirm
VALID_EVENT_TYPES (export const VALID_EVENT_TYPES = Object.keys(EVENT_TYPES))
remains correct or regenerate it from Object.keys(EVENT_TYPES) if needed to
avoid including prototype keys.

In `@services/platform/convex/workflows/triggers/helpers/crypto.ts`:
- Around line 10-29: The generateToken() and generateApiKey() functions use
crypto.getRandomValues() (non-deterministic) but are invoked from Convex
mutations (createWebhook and createApiKey), which breaks Convex determinism;
move the randomness into Convex actions (or create new action functions) that
call generateToken()/generateApiKey() and return the generated token, then
update the mutation handlers (createWebhook and createApiKey in mutations.ts) to
accept the token from the action and perform only deterministic DB operations;
ensure generateToken and generateApiKey are only referenced from action handlers
and not directly from mutation code.

In `@services/platform/convex/workflows/triggers/http_actions.ts`:
- Around line 21-25: The jsonResponse function currently returns JSON without
CORS headers; update jsonResponse to include the necessary CORS headers (at
minimum "Access-Control-Allow-Origin": "*", matching the API trigger behavior)
in the Response headers so browser requests succeed after preflight; modify the
headers object returned by jsonResponse (and any callers relying on it) to
include the CORS keys such as Access-Control-Allow-Origin and, if your API
trigger uses them, Access-Control-Allow-Methods and Access-Control-Allow-Headers
for parity.

In `@services/platform/convex/workflows/triggers/mutations.ts`:
- Around line 6-31: The handler for createSchedule must not trust
caller-supplied organizationId/createdBy; instead derive the organization and
user from the authenticated context (e.g. ctx.auth.userId and
ctx.auth.organizationId) and enforce membership/role before proceeding. Update
the mutation to stop using args.organizationId and args.createdBy in the
handler: fetch orgId/userId from ctx.auth, verify rootDef.organizationId ===
orgId, then perform an authorization check (e.g. query an org membership/roles
table or call an existing helper like ensureOrgMember/ensureOrgAdmin) to confirm
the user can create schedules for that org. Finally, when inserting into
'wfSchedules' set organizationId and createdBy to the values from ctx.auth (not
args) and remove/ignore those fields from the public args.
- Around line 239-248: The insert/patch calls are passing eventFilter even when
cleanFilter is undefined; change both create and update flows (the ctx.db.insert
call that uses cleanFilter and the updateEventSubscription patch logic that
currently sets eventFilter) to build the payload object conditionally and strip
undefined values before calling db: construct an object with eventFilter only
when cleanFilter !== undefined (e.g., const payload = { ..., ...(cleanFilter !==
undefined && { eventFilter: cleanFilter }) }) or use the existing
utility/pattern in the codebase to filter undefined entries, then pass that
filtered payload into ctx.db.insert and ctx.db.patch so optional.v.record fields
are omitted when undefined.

In `@services/platform/convex/workflows/triggers/process_event.ts`:
- Around line 42-78: The loop over subscriptions in process_event.ts can
overwhelm the system for high-volume events; modify the processing in the
for-await block (where you call getActiveWorkflowVersion,
ctx.scheduler.runAfter, ctx.db.patch, and
internal.workflows.triggers.internal_mutations.createTriggerLog) to enforce
batching or rate-limiting: introduce a configurable limit (e.g.,
MAX_TRIGGERS_PER_EVENT) and stop or defer further processing after hitting it,
or process subscriptions in fixed-size chunks and await a short pause between
chunks (or stagger ctx.scheduler.runAfter delays) to throttle scheduling; ensure
the same filtering steps (isSelfTrigger, matchesFilter) remain but count
accepted triggers and apply the limit/pauses so you don’t schedule/process
unlimited workflows in one mutation.

In `@services/platform/convex/workflows/triggers/queries.ts`:
- Around line 85-99: The handler in getEventSubscriptions manually accumulates
results using a for-await loop; replace that with the query.collect() helper for
consistency and brevity: after building the
ctx.db.query('wfEventSubscriptions').withIndex('by_workflowRoot', ...) call,
call .collect() to get an array and return it (remove the results array and the
for-await loop). Keep the same args shape (workflowRootId) and preserve the
index usage in the query.

In `@services/proxy/Caddyfile`:
- Around line 119-123: The catch-all Caddy handler "handle /api/*" currently
reverse_proxies to platform:3211 and incorrectly routes the /api/health probe
away from the Express health endpoint; add an explicit handler for "/api/health"
placed before the "handle /api/*" block that reverse_proxies to the Express port
(port 3000) — or, if the health endpoint has been moved, point it to the correct
port (e.g., 3210/3211) — ensuring the "/api/health" handler appears above the
existing "handle /api/*" rule so health checks hit the intended server.

Comment thread services/platform/convex/workflows/triggers/mutations.ts
Comment thread services/platform/convex/workflows/triggers/mutations.ts
Comment thread services/platform/convex/workflows/triggers/process_event.ts
Comment thread services/platform/convex/workflows/triggers/queries.ts
Comment thread services/proxy/Caddyfile
…d compatibility

Introduces 'start' as the new step type for workflow entry points.
The 'start' node defines only input schema, while trigger sources
(schedule, webhook, API) will be configured separately in dedicated tables.
Existing 'trigger' step type is preserved for backward compatibility.

Refs #322
… and logs

Creates version-agnostic trigger infrastructure:
- wfSchedules: cron-based schedule configs attached to rootVersionId
- wfWebhooks: webhook tokens/secrets for HTTP triggers
- wfApiKeys: workflow-level API keys with hashed storage
- wfTriggerLogs: audit trail for all trigger invocations

Refs #322
- Adds getActiveWorkflowVersion helper to resolve active version
  from rootVersionId
- Creates schedule CRUD mutations (create, update, toggle, delete)
- Updates scheduler to read from both wfSchedules table and legacy
  trigger step configs for backward compatibility
- Adds queries for schedules, webhooks, API keys, and trigger logs
- Updates lastTriggeredAt on schedule record after successful trigger

Refs #322
- Adds crypto helpers for token generation, HMAC-SHA256 signing,
  and constant-time signature comparison
- Creates webhook CRUD mutations (create, regenerate secret, delete, toggle)
- Implements POST /api/workflows/wh/{token} HTTP endpoint with:
  - Rate limiting by IP
  - HMAC-SHA256 signature verification
  - Idempotency key support
  - Active version resolution
  - Trigger audit logging
- Adds validation helpers for signature extraction and idempotency checks

Refs #322
- Creates API key CRUD mutations (create, revoke, delete) with
  wfk_ prefixed keys and SHA-256 hashed storage
- Implements POST /api/workflows/trigger HTTP endpoint with:
  - Bearer token authentication
  - Rate limiting by IP
  - API key expiration checking
  - Workflow root ID validation
  - Idempotency key support
  - Active version resolution
  - Trigger audit logging

Refs #322
- workflow:webhook: 60/min with burst capacity of 100
- workflow:api: 100/min with burst capacity of 150

Refs #322
Migrate all 22 predefined workflow definitions from stepType 'trigger'
to 'start', aligning with the new start step type introduced in #322.
Includes regenerated Convex api.d.ts with trigger infrastructure types.
…to token-based

Add frontend triggers page with schedule and webhook management sections.
Remove HMAC signature verification in favor of URL token authentication,
where the unique token in the webhook URL acts as the credential.
Merge schedules, webhooks, and api_keys into unified mutations/queries/actions
modules with proper internal separation. Add natural language to cron expression
generation using OpenAI. Update HTTP handlers to use internal mutations for
workflow execution. Switch webhook URLs to use site URL context.
…alidation

Remove backward-compatible trigger step config scanning from the scheduler,
making wfSchedules the sole source for schedule triggers. Add dedicated
validation for 'start' step type and move migration scripts to migrations/.
…and use agent component

Replace direct OpenAI fetch with @convex-dev/agent generateText, and relocate
from standalone improve_message/ directory into conversations/ domain.
…gement

Introduce event subscriptions that let workflows trigger on domain events
(conversations, customers, workflow lifecycle). Includes emit/process pipeline,
CRUD mutations, UI management tab, and inline start node execution.
…ions list

- Add unpublish (deactivate) and republish mutations with UI controls
- Migrate automations list from useQuery to usePaginatedQuery with client-side filtering
- Add by_org_versionNumber index for efficient root-version pagination
- Add listAutomationRoots query for lightweight workflow selects in triggers
- Replace rollback terminology with activate/deactivate
…eaming

- Rename webhook_http to http_actions for consistency
- Move processEvent mutation into internal_mutations
- Extract processEventHandler as reusable function
- Remove unused http/streaming HTTP action
The trigger step type was redundant with start. This removes all trigger-specific
code (executor, validator, config processing) and simplifies start steps to use
an empty config with optional inputSchema. Trigger sources (schedules, webhooks,
events) are now configured separately from the step definition.
…aming conflict

The `actions` directory name conflicted with the Convex `actions` module concept.
Renaming to `action_defs` disambiguates workflow action definitions from Convex
runtime actions and updates all import paths across the codebase.
Update step slug regex to accept lowercase letters, digits, and
underscores (e.g., "step_1") instead of only lowercase letters.
@larryro larryro force-pushed the feat/322-external-workflow-triggers branch from 23741a8 to f9f63cb Compare February 8, 2026 10:49
@larryro larryro merged commit c21e8fd into main Feb 8, 2026
3 checks passed
@larryro larryro deleted the feat/322-external-workflow-triggers branch February 8, 2026 11:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(workflow): Add external workflow triggers (webhook, API, schedule refactor)

1 participant