Skip to content

Make plugin system fully dynamic with auto-generated registries#78

Merged
ctate merged 2 commits intovercel-labs:mainfrom
bensabic:dynamic-plugin-updates
Nov 29, 2025
Merged

Make plugin system fully dynamic with auto-generated registries#78
ctate merged 2 commits intovercel-labs:mainfrom
bensabic:dynamic-plugin-updates

Conversation

@bensabic
Copy link
Contributor

This refactors the codebase to use the plugin registry as the single source of truth,
eliminating hardcoded integration/action data throughout. New plugins now automatically
work everywhere without manual updates to core files.

Key changes:

Auto-generated files (via discover-plugins.ts):

  • lib/types/integration.ts - IntegrationType union and IntegrationConfig types
  • lib/step-registry.ts - Step importers for workflow execution (statically analyzable)
  • plugins/index.ts - Plugin imports and registry exports
  • README.md - Auto-updated plugins list

Dynamic plugin registry usage:

  • workflow-executor.workflow.ts - Uses step-registry for dynamic step imports
  • workflow-codegen-shared.ts - getStepInfo() uses findActionById()
  • workflow-codegen-sdk.ts - loadStepImplementation() uses plugin codegenTemplates
  • workflow-codegen.ts - Helper functions use getStepInfo() consistently
  • credential-fetcher.ts - Uses plugin credentialMapping functions
  • code-generators.ts - Uses getAllActions() from registry
  • action-node.tsx - Uses getAllActions() for action categories
  • integration-form-dialog.tsx - Uses plugin formFields dynamically
  • integrations-manager.tsx - Uses getAllIntegrations()

Removed hardcoding:

  • ~200 lines of hardcoded step mappings in workflow-codegen-sdk.ts
  • ~70 lines of credential mappers in credential-fetcher.ts
  • Hardcoded action type dispatchers in workflow-executor.workflow.ts
  • Hardcoded IntegrationType unions (now auto-generated)

discover-plugins.ts improvements:

  • Converted from .mjs to .ts with proper typing
  • Generates lib/types/integration.ts with dynamic types
  • Generates lib/step-registry.ts with static imports for bundler compatibility
  • Updates README.md with current plugin list

   This refactors the codebase to use the plugin registry as the single source of truth,
   eliminating hardcoded integration/action data throughout. New plugins now automatically
   work everywhere without manual updates to core files.

   Key changes:

   **Auto-generated files (via discover-plugins.ts):**
   - lib/types/integration.ts - IntegrationType union and IntegrationConfig types
   - lib/step-registry.ts - Step importers for workflow execution (statically analyzable)
   - plugins/index.ts - Plugin imports and registry exports
   - README.md - Auto-updated plugins list

   **Dynamic plugin registry usage:**
   - workflow-executor.workflow.ts - Uses step-registry for dynamic step imports
   - workflow-codegen-shared.ts - getStepInfo() uses findActionById()
   - workflow-codegen-sdk.ts - loadStepImplementation() uses plugin codegenTemplates
   - workflow-codegen.ts - Helper functions use getStepInfo() consistently
   - credential-fetcher.ts - Uses plugin credentialMapping functions
   - code-generators.ts - Uses getAllActions() from registry
   - action-node.tsx - Uses getAllActions() for action categories
   - integration-form-dialog.tsx - Uses plugin formFields dynamically
   - integrations-manager.tsx - Uses getAllIntegrations()

   **Removed hardcoding:**
   - ~200 lines of hardcoded step mappings in workflow-codegen-sdk.ts
   - ~70 lines of credential mappers in credential-fetcher.ts
   - Hardcoded action type dispatchers in workflow-executor.workflow.ts
   - Hardcoded IntegrationType unions (now auto-generated)

   **discover-plugins.ts improvements:**
   - Converted from .mjs to .ts with proper typing
   - Generates lib/types/integration.ts with dynamic types
   - Generates lib/step-registry.ts with static imports for bundler compatibility
   - Updates README.md with current plugin list
@vercel
Copy link
Contributor

vercel bot commented Nov 29, 2025

@bensabic is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

Comment on lines -184 to -195
`return (${transformedExpression});`
);
const result = evalFunc(...varValues);
evaluatedCondition = Boolean(result);
} catch (error) {
console.error("[Condition] Failed to evaluate condition:", error);
console.error("[Condition] Expression was:", conditionExpression);
// If evaluation fails, treat as false to be safe
evaluatedCondition = false;
}
} else {
// Coerce to boolean for other types
Copy link
Contributor

Choose a reason for hiding this comment

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

System action step functions are called without checking if they exist in the imported module, which will throw a TypeError if the export is missing. Plugin actions have proper defensive checks, but system actions don't.

View Details
📝 Patch Details
diff --git a/lib/workflow-executor.workflow.ts b/lib/workflow-executor.workflow.ts
index 6df2a66..82cc91f 100644
--- a/lib/workflow-executor.workflow.ts
+++ b/lib/workflow-executor.workflow.ts
@@ -175,13 +175,21 @@ async function executeActionStep(input: {
   if (actionType === "Condition") {
     const systemAction = SYSTEM_ACTIONS.Condition;
     const module = await systemAction.importer();
+    const stepFunction = module[systemAction.stepFunction];
+    if (!stepFunction) {
+      return {
+        success: false,
+        error: `Step function "${systemAction.stepFunction}" not found in module`,
+      };
+    }
+
     const evaluatedCondition = evaluateConditionExpression(
       stepInput.condition,
       outputs
     );
     console.log("[Condition] Final result:", evaluatedCondition);
 
-    return await module[systemAction.stepFunction]({
+    return await stepFunction({
       condition: evaluatedCondition,
       _context: context,
     });
@@ -192,6 +200,12 @@ async function executeActionStep(input: {
   if (systemAction) {
     const module = await systemAction.importer();
     const stepFunction = module[systemAction.stepFunction];
+    if (!stepFunction) {
+      return {
+        success: false,
+        error: `Step function "${systemAction.stepFunction}" not found in module`,
+      };
+    }
     return await stepFunction(stepInput);
   }
 

Analysis

Missing null/undefined checks for system action step functions cause inconsistent error handling

What fails: executeActionStep() in lib/workflow-executor.workflow.ts throws a TypeError when a system action's step function is not found in the imported module, while plugin actions gracefully return an error object.

How to reproduce: Modify the SYSTEM_ACTIONS mapping in lib/workflow-executor.workflow.ts to use an incorrect step function name:

"Database Query": {
  importer: () => import("./steps/database-query") as Promise<any>,
  stepFunction: "wrongFunctionName",  // Changed from "databaseQueryStep"
},

Then execute a workflow with a Database Query action. The code at line 194 will try to call module.wrongFunctionName(), which is undefined, resulting in:

TypeError: stepFunction is not a function

Expected behavior: Should return a graceful error object like plugin actions do:

{
  success: false,
  error: `Step function "wrongFunctionName" not found in module`
}

Root cause:

  • Line 184 (Condition action): Calls module[systemAction.stepFunction] without checking if it exists
  • Lines 194-195 (Database Query/HTTP Request): Calls module[systemAction.stepFunction] without checking if it exists
  • Lines 202-210 (Plugin actions): Already has proper defensive check with if (stepFunction) { ... }

Fix applied: Added null/undefined checks for system action step functions at both locations (Condition action and general system actions), matching the pattern used for plugin actions. This ensures consistent error handling across all action types.

@bensabic
Copy link
Contributor Author

@ctate - I feel like I'm doing something wrong haha. Not seeing anything on my end with that PR Check error, can't seem to replicate it

@ctate
Copy link
Collaborator

ctate commented Nov 29, 2025

THANK YOU. This is so good. I'm AFK atm but will take a look first thing in the morning

   Node 20 + tsx has an issue where side-effect imports in dynamically
   imported modules don't execute properly. This caused discover-plugins
   to fail to register plugins, resulting in only database being
   generated in IntegrationType.

   Node 22 handles this correctly.
@bensabic
Copy link
Contributor Author

THANK YOU. This is so good. I'm AFK atm but will take a look first thing in the morning

Should be thanking you! You set up a great foundation with introducing the plugin system. Also FYI was able to fix that issue by just bumping up the Node verison to 22

@ctate ctate merged commit 6e2320f into vercel-labs:main Nov 29, 2025
2 of 3 checks passed
@bensabic bensabic deleted the dynamic-plugin-updates branch November 30, 2025 05:16
taitsengstock referenced this pull request in techops-services/keeperhub Dec 8, 2025
* Make plugin system fully dynamic with auto-generated registries

   This refactors the codebase to use the plugin registry as the single source of truth,
   eliminating hardcoded integration/action data throughout. New plugins now automatically
   work everywhere without manual updates to core files.

   Key changes:

   **Auto-generated files (via discover-plugins.ts):**
   - lib/types/integration.ts - IntegrationType union and IntegrationConfig types
   - lib/step-registry.ts - Step importers for workflow execution (statically analyzable)
   - plugins/index.ts - Plugin imports and registry exports
   - README.md - Auto-updated plugins list

   **Dynamic plugin registry usage:**
   - workflow-executor.workflow.ts - Uses step-registry for dynamic step imports
   - workflow-codegen-shared.ts - getStepInfo() uses findActionById()
   - workflow-codegen-sdk.ts - loadStepImplementation() uses plugin codegenTemplates
   - workflow-codegen.ts - Helper functions use getStepInfo() consistently
   - credential-fetcher.ts - Uses plugin credentialMapping functions
   - code-generators.ts - Uses getAllActions() from registry
   - action-node.tsx - Uses getAllActions() for action categories
   - integration-form-dialog.tsx - Uses plugin formFields dynamically
   - integrations-manager.tsx - Uses getAllIntegrations()

   **Removed hardcoding:**
   - ~200 lines of hardcoded step mappings in workflow-codegen-sdk.ts
   - ~70 lines of credential mappers in credential-fetcher.ts
   - Hardcoded action type dispatchers in workflow-executor.workflow.ts
   - Hardcoded IntegrationType unions (now auto-generated)

   **discover-plugins.ts improvements:**
   - Converted from .mjs to .ts with proper typing
   - Generates lib/types/integration.ts with dynamic types
   - Generates lib/step-registry.ts with static imports for bundler compatibility
   - Updates README.md with current plugin list

* fix: use Node 22 in CI for tsx compatibility

   Node 20 + tsx has an issue where side-effect imports in dynamically
   imported modules don't execute properly. This caused discover-plugins
   to fail to register plugins, resulting in only database being
   generated in IntegrationType.

   Node 22 handles this correctly.

---------

Co-authored-by: Benjamin Sabic <bensabic@users.noreply.github.com>
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.

2 participants