feat(workflow): add external workflow triggers (webhook, API, events)#400
Conversation
📝 WalkthroughWalkthroughThis 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)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
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 | 🔵 TrivialUpdate stale comment to reflect the new step type.
The comment still references "Trigger - Manual or Scheduled" but the step is now a
startstep 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 | 🔵 TrivialUse explicit type annotation and named export.
Per project conventions for predefined workflows, export as a named constant with an explicit
PredefinedWorkflowDefinitiontype 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 | 🔵 TrivialUse named export with explicit
PredefinedWorkflowDefinitiontype.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 | 🟠 MajorReplace 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 | 🟡 MinorUpdate 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
startstep. The comment should be updated for consistency.📝 Suggested fix
- // Step 1: Trigger - Manual or Scheduled + // Step 1: Start - Workflow entry point
25-28: 🧹 Nitpick | 🔵 TrivialConsider 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 | 🟠 MajorUse named export with explicit
PredefinedWorkflowDefinitiontype.
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 | 🔵 TrivialStale 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 | 🔵 TrivialConsider using a typed const export instead of default export.
Based on learnings, workflows in
predefined_workflows/*.tsshould export as a constant with an explicitPredefinedWorkflowDefinitiontype 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 | 🔵 TrivialStale comment: references "Trigger" but step is now "start".
Update comment from "Step 1: Trigger - Manual or Scheduled" to reflect the new
startstep type.
59-59: 🧹 Nitpick | 🔵 TrivialConsider using a typed const export instead of default export.
Per project convention, predefined workflows should use typed const exports with
PredefinedWorkflowDefinitiontype 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 | 🔵 TrivialStale comment: references "Trigger" but step is now "start".
Update comment from "Step 1: Trigger - Manual" to reflect the new
startstep type.
123-123: 🧹 Nitpick | 🔵 TrivialConsider using a typed const export instead of default export.
Per project convention, predefined workflows should use typed const exports with
PredefinedWorkflowDefinitiontype 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 | 🔵 TrivialStale 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 | 🔵 TrivialAlign 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 | 🔵 TrivialAlign 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 | 🟠 MajorSwitch to a named, explicitly typed workflow export.
This file still default-exports an untyped workflow constant. Please export a named constant with an explicitPredefinedWorkflowDefinitiontype 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 | 🟠 MajorSwitch to a named, explicitly typed workflow export.
This file still default-exports an untyped workflow constant. Please export a named constant with an explicitPredefinedWorkflowDefinitiontype 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 | 🟡 MinorDon’t treat last-triggered update failures as trigger failures.
IfupdateScheduleLastTriggeredthrows afterstartWorkflowsucceeds, the catch logs “Failed to trigger workflow” and skipstriggeredCount, 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 | 🟠 MajorUse 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 | 🟡 MinorKeep a localized label for legacy
triggersteps.If any workflows still contain
trigger, the label now falls back to raw"trigger". Consider keeping atriggerlabel 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 | 🟠 MajorUse 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 | 🔵 TrivialUse 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 aboveBased 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 | 🔵 TrivialUse explicit type annotation and named export per project conventions.
Similar to other predefined workflows, this should use an explicit
PredefinedWorkflowDefinitiontype 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 | 🔵 TrivialUse 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 | 🟡 MinorType/validator mismatch:
agentis in validator but not in interface.The
workflowStepValidatorincludesv.literal('agent')(line 142), but theWorkflowStepinterface (line 54) doesn't include'agent'in itsstepTypeunion. 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 | 🟡 MinorUse 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.
…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.
…n syntax reference
…save_default_workflows
…prototype false positives
…tic output element
…pe in approval card
23741a8 to
f9f63cb
Compare
Closes #322
Summary
triggerstep type withstartstep, separating input schema definition from trigger source configuration. Includes migration for existing workflows/api/workflows/wh/{token}) and API (/api/workflows/trigger) endpoints with HMAC verification, Bearer auth, rate limiting, and idempotency supportactions/toaction_defs/to avoid Convex naming conflicts, consolidate trigger modules, moveimproveMessageto conversations module, remove unused streaming codeTest plan
wfSchedulesrecords and fire on cronstartstep type🤖 Generated with Claude Code
Summary by CodeRabbit
New Features
Refactors