Skip to content

improve plugin registry#84

Merged
ctate merged 21 commits intomainfrom
ctate/better-error
Nov 30, 2025
Merged

improve plugin registry#84
ctate merged 21 commits intomainfrom
ctate/better-error

Conversation

@ctate
Copy link
Collaborator

@ctate ctate commented Nov 29, 2025

No description provided.

@vercel
Copy link
Contributor

vercel bot commented Nov 29, 2025

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Preview Comments Updated (UTC)
workflow-builder Ready Ready Preview Comment Nov 30, 2025 4:23am

@ctate ctate changed the title better error for missing plugin improve plugin registry Nov 30, 2025
Copy link
Contributor

@vercel vercel bot left a comment

Choose a reason for hiding this comment

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

Additional Suggestions:

  1. Hardcoded checks for old action type names will fail for new workflows created with the refactored plugin system. New workflows store action types in the namespaced format (e.g., "ai-gateway/generate-text") but these checks still look for the old format (e.g., "Generate Text").
View Details
📝 Patch Details
diff --git a/components/workflow/nodes/action-node.tsx b/components/workflow/nodes/action-node.tsx
index b6a5f8c..cd76b15 100644
--- a/components/workflow/nodes/action-node.tsx
+++ b/components/workflow/nodes/action-node.tsx
@@ -268,11 +268,14 @@ export const ActionNode = memo(({ data, selected, id }: ActionNodeProps) => {
   const actionType = (data.config?.actionType as string) || "";
   const status = data.status;
 
+  // Get action info for type checking (handles both legacy labels and new namespaced IDs)
+  const actionInfo = findActionById(actionType);
+
   // Check if this node has a generated image from the selected execution
   const nodeLog = executionLogs[id];
   const hasGeneratedImage =
     selectedExecutionId &&
-    actionType === "Generate Image" &&
+    (actionType === "Generate Image" || actionInfo?.slug === "generate-image") &&
     nodeLog?.output &&
     isBase64ImageOutput(nodeLog.output);
 
@@ -310,7 +313,6 @@ export const ActionNode = memo(({ data, selected, id }: ActionNodeProps) => {
   }
 
   // Get human-readable label from registry if no custom label is set
-  const actionInfo = findActionById(actionType);
   const displayTitle = data.label || actionInfo?.label || actionType;
   const displayDescription =
     data.description || getIntegrationFromActionType(actionType);
@@ -325,10 +327,10 @@ export const ActionNode = memo(({ data, selected, id }: ActionNodeProps) => {
 
   // Get model for AI nodes
   const getAiModel = (): string | null => {
-    if (actionType === "Generate Text") {
+    if (actionType === "Generate Text" || actionInfo?.slug === "generate-text") {
       return (data.config?.aiModel as string) || "meta/llama-4-scout";
     }
-    if (actionType === "Generate Image") {
+    if (actionType === "Generate Image" || actionInfo?.slug === "generate-image") {
       return (
         (data.config?.imageModel as string) || "google/imagen-4.0-generate"
       );

Analysis

Hardcoded action type checks fail for new workflows with namespaced action IDs

What fails: ActionNode component in components/workflow/nodes/action-node.tsx doesn't display AI model badges and generated image thumbnails for new workflows that use namespaced action type IDs (e.g., ai-gateway/generate-text) instead of legacy label format (e.g., Generate Text).

How to reproduce:

  1. Create a new workflow through the UI using ActionGrid component
  2. Select "Generate Text" or "Generate Image" action
  3. Execute the workflow with an AI model selected
  4. The AI model badge won't appear on the action node
  5. For "Generate Image" actions, generated image thumbnails won't display

Root cause: ActionGrid component passes namespaced action IDs from getAllActions() (e.g., ai-gateway/generate-image) to handleUpdateConfig, which stores them directly. However, hardcoded string comparisons in action-node.tsx at lines 275, 328, and 331 only checked for legacy label format (actionType === "Generate Image"), causing the checks to fail.

Expected behavior: The checks should handle both legacy labels (for backward compatibility with old workflows) and new namespaced IDs (for new workflows). This is achievable using the findActionById() function which already supports both formats via LEGACY_ACTION_MAPPINGS in plugins/legacy-mappings.ts.

Files modified:

  • components/workflow/nodes/action-node.tsx: Updated lines 275, 328, and 331 to check both formats using findActionById() helper
2. Hardcoded checks for old action type names will fail for new workflows created with the refactored plugin system\. These checks return field information for template variables\, which won\'t work when action types are stored in the new namespaced format\.
View Details
📝 Patch Details
diff --git a/components/ui/template-autocomplete.tsx b/components/ui/template-autocomplete.tsx
index e87e9e6..00f4a0d 100644
--- a/components/ui/template-autocomplete.tsx
+++ b/components/ui/template-autocomplete.tsx
@@ -5,6 +5,7 @@ import { Check } from "lucide-react";
 import { useEffect, useRef, useState } from "react";
 import { createPortal } from "react-dom";
 import { cn } from "@/lib/utils";
+import { findActionById } from "@/plugins";
 import { edgesAtom, nodesAtom, type WorkflowNode } from "@/lib/workflow-store";
 
 type TemplateAutocompleteProps = {
@@ -88,27 +89,9 @@ const schemaToFields = (
 
 // Get common fields based on node action type
 const getCommonFields = (node: WorkflowNode) => {
-  const actionType = node.data.config?.actionType;
+  const actionType = node.data.config?.actionType as string | undefined;
 
-  if (actionType === "Find Issues") {
-    return [
-      { field: "issues", description: "Array of issues found" },
-      { field: "count", description: "Number of issues" },
-    ];
-  }
-  if (actionType === "Send Email") {
-    return [
-      { field: "id", description: "Email ID" },
-      { field: "status", description: "Send status" },
-    ];
-  }
-  if (actionType === "Create Ticket") {
-    return [
-      { field: "id", description: "Ticket ID" },
-      { field: "url", description: "Ticket URL" },
-      { field: "number", description: "Ticket number" },
-    ];
-  }
+  // Handle system actions by label
   if (actionType === "HTTP Request") {
     return [
       { field: "data", description: "Response data" },
@@ -136,60 +119,93 @@ const getCommonFields = (node: WorkflowNode) => {
       { field: "count", description: "Number of rows" },
     ];
   }
-  if (actionType === "Generate Text") {
-    const aiFormat = node.data.config?.aiFormat as string | undefined;
-    const aiSchema = node.data.config?.aiSchema as string | undefined;
+  if (actionType === "Condition") {
+    return [{ field: "data", description: "Output data" }];
+  }
 
-    // If format is object and schema is defined, show schema fields
-    if (aiFormat === "object" && aiSchema) {
-      try {
-        const schema = JSON.parse(aiSchema) as SchemaField[];
-        if (schema.length > 0) {
-          return schemaToFields(schema);
+  // Handle plugin actions by looking them up in the registry
+  // findActionById handles both new format (e.g., "resend/send-email") and legacy format (e.g., "Send Email")
+  const action = findActionById(actionType);
+  if (action) {
+    const slug = action.slug;
+
+    // Match on action slug for robustness
+    if (slug === "find-issues") {
+      return [
+        { field: "issues", description: "Array of issues found" },
+        { field: "count", description: "Number of issues" },
+      ];
+    }
+    if (slug === "send-email") {
+      return [
+        { field: "id", description: "Email ID" },
+        { field: "status", description: "Send status" },
+      ];
+    }
+    if (slug === "create-ticket") {
+      return [
+        { field: "id", description: "Ticket ID" },
+        { field: "url", description: "Ticket URL" },
+        { field: "number", description: "Ticket number" },
+      ];
+    }
+    if (slug === "generate-text") {
+      const aiFormat = node.data.config?.aiFormat as string | undefined;
+      const aiSchema = node.data.config?.aiSchema as string | undefined;
+
+      // If format is object and schema is defined, show schema fields
+      if (aiFormat === "object" && aiSchema) {
+        try {
+          const schema = JSON.parse(aiSchema) as SchemaField[];
+          if (schema.length > 0) {
+            return schemaToFields(schema);
+          }
+        } catch {
+          // If schema parsing fails, fall through to default fields
         }
-      } catch {
-        // If schema parsing fails, fall through to default fields
       }
-    }
 
-    // Default fields for text format or when no schema
-    return [
-      { field: "text", description: "Generated text" },
-      { field: "model", description: "Model used" },
-    ];
-  }
-  if (actionType === "Generate Image") {
-    return [
-      { field: "base64", description: "Base64 image data" },
-      { field: "model", description: "Model used" },
-    ];
-  }
-  if (actionType === "Scrape") {
-    return [
-      { field: "markdown", description: "Scraped content as markdown" },
-      { field: "metadata.url", description: "Page URL" },
-      { field: "metadata.title", description: "Page title" },
-      { field: "metadata.description", description: "Page description" },
-      { field: "metadata.language", description: "Page language" },
-      { field: "metadata.favicon", description: "Favicon URL" },
-    ];
-  }
-  if (actionType === "Search") {
-    return [{ field: "web", description: "Array of search results" }];
-  }
-  if (actionType === "Create Chat") {
-    return [
-      { field: "chatId", description: "v0 chat ID" },
-      { field: "url", description: "v0 chat URL" },
-      { field: "demoUrl", description: "Demo preview URL" },
-    ];
-  }
-  if (actionType === "Send Message") {
-    return [
-      { field: "chatId", description: "v0 chat ID" },
-      { field: "demoUrl", description: "Demo preview URL" },
-    ];
+      // Default fields for text format or when no schema
+      return [
+        { field: "text", description: "Generated text" },
+        { field: "model", description: "Model used" },
+      ];
+    }
+    if (slug === "generate-image") {
+      return [
+        { field: "base64", description: "Base64 image data" },
+        { field: "model", description: "Model used" },
+      ];
+    }
+    if (slug === "scrape") {
+      return [
+        { field: "markdown", description: "Scraped content as markdown" },
+        { field: "metadata.url", description: "Page URL" },
+        { field: "metadata.title", description: "Page title" },
+        { field: "metadata.description", description: "Page description" },
+        { field: "metadata.language", description: "Page language" },
+        { field: "metadata.favicon", description: "Favicon URL" },
+      ];
+    }
+    if (slug === "search") {
+      return [{ field: "web", description: "Array of search results" }];
+    }
+    if (slug === "create-chat") {
+      return [
+        { field: "chatId", description: "v0 chat ID" },
+        { field: "url", description: "v0 chat URL" },
+        { field: "demoUrl", description: "Demo preview URL" },
+      ];
+    }
+    if (slug === "send-message") {
+      return [
+        { field: "chatId", description: "v0 chat ID" },
+        { field: "demoUrl", description: "Demo preview URL" },
+      ];
+    }
   }
+
+  // Handle trigger nodes
   if (node.data.type === "trigger") {
     const triggerType = node.data.config?.triggerType as string | undefined;
     const webhookSchema = node.data.config?.webhookSchema as string | undefined;

Analysis

Template autocomplete fails for new workflows with namespaced action types

What fails: The getCommonFields() function in components/ui/template-autocomplete.tsx checks for hardcoded action type names (e.g., "Find Issues", "Send Email") but workflows created with the new plugin system store action types in namespaced format (e.g., "linear/find-issues", "resend/send-email"). This causes template variable autocomplete to fail for new workflows, showing only generic "data" field instead of specific output fields.

How to reproduce:

  1. Create a workflow using the new plugin system (e.g., by having AI generate one, which uses namespaced action IDs from generateAIActionPrompts())
  2. Add a "Send Email" action node - it will be stored as "resend/send-email"
  3. In a subsequent action, attempt to use template autocomplete to reference fields from the Send Email node
  4. The autocomplete shows only generic "data" field instead of specific fields like "id" and "status"

Result: Hardcoded checks for old format ("Send Email") fail to match new format ("resend/send-email"), causing getCommonFields() to fall through to default return value of [{ field: "data", description: "Output data" }]

Expected: Should return specific field suggestions like "id" and "status" regardless of whether action type is stored as legacy label "Send Email" or new format "resend/send-email"

Root cause: The system supports two action type formats:

  • Legacy format: action label strings (e.g., "Send Email", "Find Issues")
  • New format: namespaced action IDs from plugin registry (e.g., "resend/send-email", "linear/find-issues")

The ActionConfig component normalizes selection to new format when users select actions (using action.id from getActionsByCategory()). Workflows created via AI generation use the new format directly (per generateAIActionPrompts()). However, getCommonFields() only checks for old format, breaking autocomplete for new workflows.

Fix: Updated getCommonFields() to use findActionById() registry lookup instead of hardcoded string checks. The registry function handles both formats automatically, returning an action with slug property that can be reliably matched regardless of input format.

Fix on Vercel

@bensabic
Copy link
Contributor

@ctate - I wonder if it's worth adding a 'System' plugin. After reading these changes and making changes in my two PRs, alot of exceptions or special cases are needed to handle the system logic plus dynamic logic. If a new system action is added, it'd also need a fair number of updates across different files. Happy to submit a PR for that if that's the direction you want to take

@ctate
Copy link
Collaborator Author

ctate commented Nov 30, 2025

@bensabic That makes sense for Database and HTTP Request - those should probably become their own plugins (ie. Native, Postgres, Mongo, etc.).

Condition however is likely an exception. It will probably evolve into a completely separate node type since it's handled differently from other steps, and that divergence will only increase over time.

The goal of this PR is to make creating new plugins as simple as possible. Ultimately, these plugins will live in their own registry (likely a shadcn registry) in a separate package and/or repository. That way, you can choose exactly which plugins to include in your own Workflow Builder, especially as the ecosystem grows - we may eventually have 1,000+ possible steps, which would otherwise add significant weight and dependencies.

@ctate ctate merged commit ee698fe into main Nov 30, 2025
4 checks passed
@bensabic
Copy link
Contributor

@ctate Yeah that's a very good point, especially regarding the Condition action.

I love what you've done in terms of simplifying the plugin structure so that it's alot more standardised. I did realise after the Resend plugin updates, it would have opened the door to plugins varying too much in their configuration and causing a big mess.

taitsengstock referenced this pull request in techops-services/keeperhub Dec 8, 2025
* better error for missing plugin

* more dynamic registry work

* fixes legacy mappings

* fix errors

* fix legacy mappings

* fix line bug

* use same icon

* get rid of settings.tsx

* fix error

* combine env vars

* config fields

* fixes

* better icon

* more fixes

* flatten steps

* rename to ts

* add examples

* required fields

* switch to props tab

* combine errors

* fix button
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