Skip to content

Conversation

@Railly
Copy link
Contributor

@Railly Railly commented Nov 29, 2025

Summary

  • Adds a complete Clerk integration with 4 workflow actions:
    • Get User: Fetch user details by ID
    • Create User: Create new users with email, names, password, and metadata
    • Update User: Update existing user properties
    • Delete User: Remove users from Clerk
  • Full plugin implementation following the existing plugin architecture
  • Integration form with secret key configuration and validation
  • Connection testing via Clerk API
  • Code generation templates for workflow export
  • Proper credential handling via fetchCredentials
  • Uses User-Agent: workflow-builder.dev header for Clerk BAPI tracking
  • AI assistant can now generate Clerk workflows with proper node references

Test plan

  • Add Clerk integration with valid secret key
  • Test connection verification
  • Create workflow with Get User action
  • Create workflow with Create User action
  • Create workflow with Update User action (with template reference to Create User output)
  • Create workflow with Delete User action
  • AI generates Clerk nodes with correct icons and template references

Adds a complete Clerk integration with 4 workflow actions:
- Get User: Fetch user details by ID
- Create User: Create new users with email, names
- Update User: Update existing user properties
- Delete User: Remove users from Clerk

Features:
- Full plugin implementation following the plugin architecture
- Integration form with secret key configuration
- Connection testing via Clerk API
- Code generation templates for workflow export
- Proper credential handling via fetchCredentials
@vercel
Copy link
Contributor

vercel bot commented Nov 29, 2025

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

A member of the Team first needs to authorize it.

@ctate
Copy link
Collaborator

ctate commented Nov 30, 2025

Hey @Railly, the plugin system was reworked to make things simpler, and your PR needs a few updates to stay compatible. Let me know if you want any help or if you’d prefer I take care of it. I’d really love to get this plugin in. Sorry for the inconvenience.

@Railly
Copy link
Contributor Author

Railly commented Nov 30, 2025

No problem, I'll give it a try tomorrow. I'll let you know if I need any help

- Refactored Clerk plugin to use declarative configFields
- Migrated from nested step structure to flat files
- Added credentials.ts for type definitions
- Updated all 4 actions: get-user, create-user, update-user, delete-user
- Added template variable hint in AI prompt for proper node referencing
- Add publicMetadata and privateMetadata to Clerk code generators
- Remove secretKey from Clerk param builders (handled internally)
- Add template variable hint in AI prompt
@Railly
Copy link
Contributor Author

Railly commented Dec 2, 2025

@ctate updated! let me know if that works!

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 Suggestion:

The Clerk action codegen templates return raw Clerk API responses instead of wrapped success/error objects, inconsistent with the actual step functions that return { success, user/error }.

View Details
📝 Patch Details
diff --git a/lib/codegen-registry.ts b/lib/codegen-registry.ts
index 375bbe0..7174ab8 100644
--- a/lib/codegen-registry.ts
+++ b/lib/codegen-registry.ts
@@ -17,7 +17,7 @@
 export const AUTO_GENERATED_TEMPLATES: Record<string, string> = {
   "ai-gateway/generate-text": `import { createGateway, generateObject, generateText } from "ai";
 import { z } from "zod";
-import { fetchCredentials } from './lib/credential-helper';
+import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -36,10 +36,12 @@ export type GenerateTextCoreInput = {
   aiSchema?: string;
 };
 
-export async function generateTextStep(input: GenerateTextCoreInput): Promise<GenerateTextResult> {
+export async function generateTextStep(
+  input: GenerateTextCoreInput,
+): Promise<GenerateTextResult> {
   "use step";
   const credentials = await fetchCredentials("ai-gateway");
-const apiKey = credentials.AI_GATEWAY_API_KEY;
+  const apiKey = credentials.AI_GATEWAY_API_KEY;
 
   if (!apiKey) {
     return {
@@ -92,14 +94,12 @@ const apiKey = credentials.AI_GATEWAY_API_KEY;
       error: \`Text generation failed: \${message}\`,
     };
   }
-}`,
+}
+`,
 
   "ai-gateway/generate-image": `import type { ImageModelV2 } from "@ai-sdk/provider";
-import {
-  createGateway,
-  experimental_generateImage as generateImage,
-} from "ai";
-import { fetchCredentials } from './lib/credential-helper';
+import { createGateway, experimental_generateImage as generateImage } from "ai";
+import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -115,10 +115,12 @@ export type GenerateImageCoreInput = {
   imagePrompt: string;
 };
 
-export async function generateImageStep(input: GenerateImageCoreInput): Promise<GenerateImageResult> {
+export async function generateImageStep(
+  input: GenerateImageCoreInput,
+): Promise<GenerateImageResult> {
   "use step";
   const credentials = await fetchCredentials("ai-gateway");
-const apiKey = credentials.AI_GATEWAY_API_KEY;
+  const apiKey = credentials.AI_GATEWAY_API_KEY;
 
   if (!apiKey) {
     return {
@@ -158,10 +160,10 @@ const apiKey = credentials.AI_GATEWAY_API_KEY;
       error: \`Image generation failed: \${message}\`,
     };
   }
-}`,
+}
+`,
 
-  "clerk/get-user": `
-import { fetchCredentials } from './lib/credential-helper';
+  "clerk/get-user": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -176,10 +178,12 @@ export type ClerkGetUserCoreInput = {
   userId: string;
 };
 
-export async function clerkGetUserStep(input: ClerkGetUserCoreInput): Promise<GetUserResult> {
+export async function clerkGetUserStep(
+  input: ClerkGetUserCoreInput,
+): Promise<GetUserResult> {
   "use step";
   const credentials = await fetchCredentials("clerk");
-const secretKey = credentials.CLERK_SECRET_KEY;
+  const secretKey = credentials.CLERK_SECRET_KEY;
 
   if (!secretKey) {
     return {
@@ -205,7 +209,7 @@ const secretKey = credentials.CLERK_SECRET_KEY;
           "Content-Type": "application/json",
           "User-Agent": "workflow-builder.dev",
         },
-      }
+      },
     );
 
     if (!response.ok) {
@@ -213,7 +217,8 @@ const secretKey = credentials.CLERK_SECRET_KEY;
       return {
         success: false,
         error:
-          error.errors?.[0]?.message || \`Failed to get user: \${response.status}\`,
+          error.errors?.[0]?.message ||
+          \`Failed to get user: \${response.status}\`,
       };
     }
 
@@ -225,10 +230,10 @@ const secretKey = credentials.CLERK_SECRET_KEY;
       error: \`Failed to get user: \${getErrorMessage(error)}\`,
     };
   }
-}`,
+}
+`,
 
-  "clerk/create-user": `
-import { fetchCredentials } from './lib/credential-helper';
+  "clerk/create-user": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -248,10 +253,12 @@ export type ClerkCreateUserCoreInput = {
   privateMetadata?: string;
 };
 
-export async function clerkCreateUserStep(input: ClerkCreateUserCoreInput): Promise<CreateUserResult> {
+export async function clerkCreateUserStep(
+  input: ClerkCreateUserCoreInput,
+): Promise<CreateUserResult> {
   "use step";
   const credentials = await fetchCredentials("clerk");
-const secretKey = credentials.CLERK_SECRET_KEY;
+  const secretKey = credentials.CLERK_SECRET_KEY;
 
   if (!secretKey) {
     return {
@@ -326,10 +333,10 @@ const secretKey = credentials.CLERK_SECRET_KEY;
       error: \`Failed to create user: \${getErrorMessage(error)}\`,
     };
   }
-}`,
+}
+`,
 
-  "clerk/update-user": `
-import { fetchCredentials } from './lib/credential-helper';
+  "clerk/update-user": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -348,10 +355,12 @@ export type ClerkUpdateUserCoreInput = {
   privateMetadata?: string;
 };
 
-export async function clerkUpdateUserStep(input: ClerkUpdateUserCoreInput): Promise<UpdateUserResult> {
+export async function clerkUpdateUserStep(
+  input: ClerkUpdateUserCoreInput,
+): Promise<UpdateUserResult> {
   "use step";
   const credentials = await fetchCredentials("clerk");
-const secretKey = credentials.CLERK_SECRET_KEY;
+  const secretKey = credentials.CLERK_SECRET_KEY;
 
   if (!secretKey) {
     return {
@@ -403,7 +412,7 @@ const secretKey = credentials.CLERK_SECRET_KEY;
           "User-Agent": "workflow-builder.dev",
         },
         body: JSON.stringify(body),
-      }
+      },
     );
 
     if (!response.ok) {
@@ -424,10 +433,10 @@ const secretKey = credentials.CLERK_SECRET_KEY;
       error: \`Failed to update user: \${getErrorMessage(error)}\`,
     };
   }
-}`,
+}
+`,
 
-  "clerk/delete-user": `
-import { fetchCredentials } from './lib/credential-helper';
+  "clerk/delete-user": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -442,10 +451,12 @@ export type ClerkDeleteUserCoreInput = {
   userId: string;
 };
 
-export async function clerkDeleteUserStep(input: ClerkDeleteUserCoreInput): Promise<DeleteUserResult> {
+export async function clerkDeleteUserStep(
+  input: ClerkDeleteUserCoreInput,
+): Promise<DeleteUserResult> {
   "use step";
   const credentials = await fetchCredentials("clerk");
-const secretKey = credentials.CLERK_SECRET_KEY;
+  const secretKey = credentials.CLERK_SECRET_KEY;
 
   if (!secretKey) {
     return {
@@ -472,7 +483,7 @@ const secretKey = credentials.CLERK_SECRET_KEY;
           "Content-Type": "application/json",
           "User-Agent": "workflow-builder.dev",
         },
-      }
+      },
     );
 
     if (!response.ok) {
@@ -492,10 +503,10 @@ const secretKey = credentials.CLERK_SECRET_KEY;
       error: \`Failed to delete user: \${getErrorMessage(error)}\`,
     };
   }
-}`,
+}
+`,
 
-  "firecrawl/scrape": `
-import { fetchCredentials } from './lib/credential-helper';
+  "firecrawl/scrape": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -512,10 +523,12 @@ export type FirecrawlScrapeCoreInput = {
   formats?: ("markdown" | "html" | "rawHtml" | "links" | "screenshot")[];
 };
 
-export async function firecrawlScrapeStep(input: FirecrawlScrapeCoreInput): Promise<ScrapeResult> {
+export async function firecrawlScrapeStep(
+  input: FirecrawlScrapeCoreInput,
+): Promise<ScrapeResult> {
   "use step";
   const credentials = await fetchCredentials("firecrawl");
-const apiKey = credentials.FIRECRAWL_API_KEY;
+  const apiKey = credentials.FIRECRAWL_API_KEY;
 
   if (!apiKey) {
     throw new Error("Firecrawl API Key is not configured.");
@@ -552,10 +565,10 @@ const apiKey = credentials.FIRECRAWL_API_KEY;
   } catch (error) {
     throw new Error(\`Failed to scrape: \${getErrorMessage(error)}\`);
   }
-}`,
+}
+`,
 
-  "firecrawl/search": `
-import { fetchCredentials } from './lib/credential-helper';
+  "firecrawl/search": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -574,10 +587,12 @@ export type FirecrawlSearchCoreInput = {
   };
 };
 
-export async function firecrawlSearchStep(input: FirecrawlSearchCoreInput): Promise<SearchResult> {
+export async function firecrawlSearchStep(
+  input: FirecrawlSearchCoreInput,
+): Promise<SearchResult> {
   "use step";
   const credentials = await fetchCredentials("firecrawl");
-const apiKey = credentials.FIRECRAWL_API_KEY;
+  const apiKey = credentials.FIRECRAWL_API_KEY;
 
   if (!apiKey) {
     throw new Error("Firecrawl API Key is not configured.");
@@ -614,10 +629,10 @@ const apiKey = credentials.FIRECRAWL_API_KEY;
   } catch (error) {
     throw new Error(\`Failed to search: \${getErrorMessage(error)}\`);
   }
-}`,
+}
+`,
 
-  "linear/create-ticket": `
-import { fetchCredentials } from './lib/credential-helper';
+  "linear/create-ticket": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -633,10 +648,12 @@ export type CreateTicketCoreInput = {
   ticketDescription: string;
 };
 
-export async function createTicketStep(input: CreateTicketCoreInput): Promise<CreateTicketResult> {
+export async function createTicketStep(
+  input: CreateTicketCoreInput,
+): Promise<CreateTicketResult> {
   "use step";
   const credentials = await fetchCredentials("linear");
-const apiKey = credentials.LINEAR_API_KEY;
+  const apiKey = credentials.LINEAR_API_KEY;
   const teamId = credentials.LINEAR_TEAM_ID;
 
   if (!apiKey) {
@@ -653,7 +670,7 @@ const apiKey = credentials.LINEAR_API_KEY;
     if (!targetTeamId) {
       const teamsResult = await linearQuery<TeamsQueryResponse>(
         apiKey,
-        \`query { teams { nodes { id name } } }\`
+        \`query { teams { nodes { id name } } }\`,
       );
 
       if (teamsResult.errors?.length) {
@@ -689,7 +706,7 @@ const apiKey = credentials.LINEAR_API_KEY;
         title: input.ticketTitle,
         description: input.ticketDescription,
         teamId: targetTeamId,
-      }
+      },
     );
 
     if (createResult.errors?.length) {
@@ -719,10 +736,10 @@ const apiKey = credentials.LINEAR_API_KEY;
       error: \`Failed to create ticket: \${getErrorMessage(error)}\`,
     };
   }
-}`,
+}
+`,
 
-  "linear/find-issues": `
-import { fetchCredentials } from './lib/credential-helper';
+  "linear/find-issues": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -740,10 +757,12 @@ export type FindIssuesCoreInput = {
   linearLabel?: string;
 };
 
-export async function findIssuesStep(input: FindIssuesCoreInput): Promise<FindIssuesResult> {
+export async function findIssuesStep(
+  input: FindIssuesCoreInput,
+): Promise<FindIssuesResult> {
   "use step";
   const credentials = await fetchCredentials("linear");
-const apiKey = credentials.LINEAR_API_KEY;
+  const apiKey = credentials.LINEAR_API_KEY;
 
   if (!apiKey) {
     return {
@@ -789,7 +808,7 @@ const apiKey = credentials.LINEAR_API_KEY;
           }
         }
       }\`,
-      { filter: Object.keys(filter).length > 0 ? filter : undefined }
+      { filter: Object.keys(filter).length > 0 ? filter : undefined },
     );
 
     if (result.errors?.length) {
@@ -807,7 +826,7 @@ const apiKey = credentials.LINEAR_API_KEY;
         state: issue.state?.name || "Unknown",
         priority: issue.priority,
         assigneeId: issue.assigneeId || undefined,
-      })
+      }),
     );
 
     return {
@@ -821,10 +840,10 @@ const apiKey = credentials.LINEAR_API_KEY;
       error: \`Failed to find issues: \${getErrorMessage(error)}\`,
     };
   }
-}`,
+}
+`,
 
-  "resend/send-email": `
-import { fetchCredentials } from './lib/credential-helper';
+  "resend/send-email": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -848,10 +867,12 @@ export type SendEmailCoreInput = {
   idempotencyKey?: string;
 };
 
-export async function sendEmailStep(input: SendEmailCoreInput): Promise<SendEmailResult> {
+export async function sendEmailStep(
+  input: SendEmailCoreInput,
+): Promise<SendEmailResult> {
   "use step";
   const credentials = await fetchCredentials("resend");
-const apiKey = credentials.RESEND_API_KEY;
+  const apiKey = credentials.RESEND_API_KEY;
   const fromEmail = credentials.RESEND_FROM_EMAIL;
 
   if (!apiKey) {
@@ -901,7 +922,8 @@ const apiKey = credentials.RESEND_API_KEY;
       const errorData = (await response.json()) as ResendErrorResponse;
       return {
         success: false,
-        error: errorData.message || \`HTTP \${response.status}: Failed to send email\`,
+        error:
+          errorData.message || \`HTTP \${response.status}: Failed to send email\`,
       };
     }
 
@@ -914,10 +936,10 @@ const apiKey = credentials.RESEND_API_KEY;
       error: \`Failed to send email: \${message}\`,
     };
   }
-}`,
+}
+`,
 
-  "slack/send-message": `
-import { fetchCredentials } from './lib/credential-helper';
+  "slack/send-message": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -933,10 +955,12 @@ export type SendSlackMessageCoreInput = {
   slackMessage: string;
 };
 
-export async function sendSlackMessageStep(input: SendSlackMessageCoreInput): Promise<SendSlackMessageResult> {
+export async function sendSlackMessageStep(
+  input: SendSlackMessageCoreInput,
+): Promise<SendSlackMessageResult> {
   "use step";
   const credentials = await fetchCredentials("slack");
-const apiKey = credentials.SLACK_API_KEY;
+  const apiKey = credentials.SLACK_API_KEY;
 
   if (!apiKey) {
     return {
@@ -986,10 +1010,10 @@ const apiKey = credentials.SLACK_API_KEY;
       error: \`Failed to send Slack message: \${getErrorMessage(error)}\`,
     };
   }
-}`,
+}
+`,
 
-  "superagent/guard": `
-import { fetchCredentials } from './lib/credential-helper';
+  "superagent/guard": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -1007,10 +1031,12 @@ export type SuperagentGuardCoreInput = {
   text: string;
 };
 
-export async function superagentGuardStep(input: SuperagentGuardCoreInput): Promise<GuardResult> {
+export async function superagentGuardStep(
+  input: SuperagentGuardCoreInput,
+): Promise<GuardResult> {
   "use step";
   const credentials = await fetchCredentials("superagent");
-const apiKey = credentials.SUPERAGENT_API_KEY;
+  const apiKey = credentials.SUPERAGENT_API_KEY;
 
   if (!apiKey) {
     throw new Error("Superagent API Key is not configured.");
@@ -1039,7 +1065,7 @@ const apiKey = credentials.SUPERAGENT_API_KEY;
 
     if (!content || typeof content !== "object") {
       throw new Error(
-        "Invalid Guard API response: missing or invalid content structure"
+        "Invalid Guard API response: missing or invalid content structure",
       );
     }
 
@@ -1049,7 +1075,7 @@ const apiKey = credentials.SUPERAGENT_API_KEY;
       (classification !== "allow" && classification !== "block")
     ) {
       throw new Error(
-        \`Invalid Guard API response: missing or invalid classification (received: \${JSON.stringify(classification)})\`
+        \`Invalid Guard API response: missing or invalid classification (received: \${JSON.stringify(classification)})\`,
       );
     }
 
@@ -1062,10 +1088,10 @@ const apiKey = credentials.SUPERAGENT_API_KEY;
   } catch (error) {
     throw new Error(\`Failed to analyze text: \${getErrorMessage(error)}\`);
   }
-}`,
+}
+`,
 
-  "superagent/redact": `
-import { fetchCredentials } from './lib/credential-helper';
+  "superagent/redact": `import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -1082,10 +1108,12 @@ export type SuperagentRedactCoreInput = {
   entities?: string[] | string;
 };
 
-export async function superagentRedactStep(input: SuperagentRedactCoreInput): Promise<RedactResult> {
+export async function superagentRedactStep(
+  input: SuperagentRedactCoreInput,
+): Promise<RedactResult> {
   "use step";
   const credentials = await fetchCredentials("superagent");
-const apiKey = credentials.SUPERAGENT_API_KEY;
+  const apiKey = credentials.SUPERAGENT_API_KEY;
 
   if (!apiKey) {
     throw new Error("Superagent API Key is not configured.");
@@ -1138,10 +1166,11 @@ const apiKey = credentials.SUPERAGENT_API_KEY;
   } catch (error) {
     throw new Error(\`Failed to redact text: \${getErrorMessage(error)}\`);
   }
-}`,
+}
+`,
 
   "v0/create-chat": `import { createClient, type ChatsCreateResponse } from "v0-sdk";
-import { fetchCredentials } from './lib/credential-helper';
+import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -1157,10 +1186,12 @@ export type CreateChatCoreInput = {
   system?: string;
 };
 
-export async function createChatStep(input: CreateChatCoreInput): Promise<CreateChatResult> {
+export async function createChatStep(
+  input: CreateChatCoreInput,
+): Promise<CreateChatResult> {
   "use step";
   const credentials = await fetchCredentials("v0");
-const apiKey = credentials.V0_API_KEY;
+  const apiKey = credentials.V0_API_KEY;
 
   if (!apiKey) {
     return {
@@ -1190,10 +1221,11 @@ const apiKey = credentials.V0_API_KEY;
       error: \`Failed to create chat: \${getErrorMessage(error)}\`,
     };
   }
-}`,
+}
+`,
 
   "v0/send-message": `import { createClient, type ChatsSendMessageResponse } from "v0-sdk";
-import { fetchCredentials } from './lib/credential-helper';
+import { fetchCredentials } from "./lib/credential-helper";
 
 function getErrorMessage(error: unknown): string {
   if (error instanceof Error) return error.message;
@@ -1209,10 +1241,12 @@ export type SendMessageCoreInput = {
   message: string;
 };
 
-export async function sendMessageStep(input: SendMessageCoreInput): Promise<SendMessageResult> {
+export async function sendMessageStep(
+  input: SendMessageCoreInput,
+): Promise<SendMessageResult> {
   "use step";
   const credentials = await fetchCredentials("v0");
-const apiKey = credentials.V0_API_KEY;
+  const apiKey = credentials.V0_API_KEY;
 
   if (!apiKey) {
     return {
@@ -1241,7 +1275,8 @@ const apiKey = credentials.V0_API_KEY;
       error: \`Failed to send message: \${getErrorMessage(error)}\`,
     };
   }
-}`,
+}
+`,
 };
 
 /**
diff --git a/plugins/clerk/codegen/create-user.ts b/plugins/clerk/codegen/create-user.ts
index 038bc8a..ecf0f8d 100644
--- a/plugins/clerk/codegen/create-user.ts
+++ b/plugins/clerk/codegen/create-user.ts
@@ -40,8 +40,12 @@ export const createUserCodegenTemplate = `export async function clerkCreateUserS
 
   if (!response.ok) {
     const error = await response.json().catch(() => ({}));
-    throw new Error(error.errors?.[0]?.message || \`Failed to create user: \${response.status}\`);
+    return {
+      success: false,
+      error: error.errors?.[0]?.message || \`Failed to create user: \${response.status}\`,
+    };
   }
 
-  return await response.json();
+  const user = await response.json();
+  return { success: true, user };
 }`;
diff --git a/plugins/clerk/codegen/delete-user.ts b/plugins/clerk/codegen/delete-user.ts
index 68e70ba..d7d5aea 100644
--- a/plugins/clerk/codegen/delete-user.ts
+++ b/plugins/clerk/codegen/delete-user.ts
@@ -27,8 +27,11 @@ export const deleteUserCodegenTemplate = `export async function clerkDeleteUserS
 
   if (!response.ok) {
     const error = await response.json().catch(() => ({}));
-    throw new Error(error.errors?.[0]?.message || \`Failed to delete user: \${response.status}\`);
+    return {
+      success: false,
+      error: error.errors?.[0]?.message || \`Failed to delete user: \${response.status}\`,
+    };
   }
 
-  return { deleted: true, id: input.userId };
+  return { success: true, deleted: true };
 }`;
diff --git a/plugins/clerk/codegen/get-user.ts b/plugins/clerk/codegen/get-user.ts
index 67e91c7..a84b4eb 100644
--- a/plugins/clerk/codegen/get-user.ts
+++ b/plugins/clerk/codegen/get-user.ts
@@ -26,8 +26,12 @@ export const getUserCodegenTemplate = `export async function clerkGetUserStep(in
 
   if (!response.ok) {
     const error = await response.json().catch(() => ({}));
-    throw new Error(error.errors?.[0]?.message || \`Failed to get user: \${response.status}\`);
+    return {
+      success: false,
+      error: error.errors?.[0]?.message || \`Failed to get user: \${response.status}\`,
+    };
   }
 
-  return await response.json();
+  const user = await response.json();
+  return { success: true, user };
 }`;
diff --git a/plugins/clerk/codegen/update-user.ts b/plugins/clerk/codegen/update-user.ts
index 692f771..8a319dc 100644
--- a/plugins/clerk/codegen/update-user.ts
+++ b/plugins/clerk/codegen/update-user.ts
@@ -39,8 +39,12 @@ export const updateUserCodegenTemplate = `export async function clerkUpdateUserS
 
   if (!response.ok) {
     const error = await response.json().catch(() => ({}));
-    throw new Error(error.errors?.[0]?.message || \`Failed to update user: \${response.status}\`);
+    return {
+      success: false,
+      error: error.errors?.[0]?.message || \`Failed to update user: \${response.status}\`,
+    };
   }
 
-  return await response.json();
+  const user = await response.json();
+  return { success: true, user };
 }`;

Analysis

Clerk action codegen templates return inconsistent response format

What fails: Codegen templates for Clerk user actions (clerkCreateUserStep, clerkUpdateUserStep, clerkGetUserStep, clerkDeleteUserStep) return unwrapped raw Clerk API responses or throw errors, while the actual step functions return wrapped { success, data/error } objects.

How to reproduce:

  1. Generate code using plugins/clerk/codegen/create-user.ts (or other Clerk codegen templates)
  2. Export the workflow to a standalone Next.js project using these generated templates
  3. Try to use the result with code that expects { success, user } response format:
const result = await clerkCreateUserStep({ emailAddress: "user@example.com" });
if (result.success) {
  console.log(result.user); // Would fail - result is raw Clerk user object, not wrapped
}

Result: TypeErrors due to missing success field. Exported code has different API than app code.

Expected behavior: Generated code should return the same response format as step functions:

  • Success: { success: true, user: ClerkUser } for create/update/get operations, or { success: true, deleted: true } for delete
  • Failure: { success: false, error: string } for all operations

Note: The step functions are the source of truth for the API contract. All generated code must wrap responses in the same { success, data/error } format to maintain consistency when workflows are exported.

Fix on Vercel

Copy link
Collaborator

@ctate ctate left a comment

Choose a reason for hiding this comment

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

This is looking great so far! There is some code that needs to be removed / added back. Lmk if you have any questions

- Revert AGENTS.md to upstream version
- Remove plugins/clerk/codegen/ folder (auto-generated)
- Remove template variable hint from AI prompt (auto-generated)
- Accept upstream plugin architecture changes
- Keep Clerk plugin files
- Remove auto-generated files (lib/codegen-registry.ts, lib/step-registry.ts, lib/types/integration.ts)
- Merge new plugins (blob, fal, github, perplexity, stripe)
@Railly
Copy link
Contributor Author

Railly commented Dec 2, 2025

@ctate addressed! let me know if there's more to modify!

- Remove codegen imports (auto-generated)
- Add outputFields to all actions
- Add maxRetries = 0 to all step functions
@Railly Railly requested a review from ctate December 2, 2025 23:57

try {
const response = await fetch(
`https://api.clerk.com/v1/users/${input.userId}`,
Copy link
Collaborator

Choose a reason for hiding this comment

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

In get-user.ts, update-user.ts, and delete-user.ts, user-controlled input is directly interpolated into URL paths without sanitization.

If input.userId contains malicious content like:

  • ../../other-endpoint (path traversal)
  • user123?admin=true (query string injection)
  • URL-encoded special characters

The request could be manipulated.

Mitigation Recommendation: Add URL encoding:

const response = await fetch(
  `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`,

Comment on lines 73 to 79
if (input.publicMetadata) {
try {
body.public_metadata = JSON.parse(input.publicMetadata);
} catch {
body.public_metadata = input.publicMetadata;
}
}
Copy link
Collaborator

Choose a reason for hiding this comment

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

In create-user.ts and update-user.ts, if JSON parsing fails, the string is sent directly. Clerk's API expects an object for metadata, so this would likely cause an API error. Consider either:

  • Always requiring valid JSON
  • Returning an error on parse failure instead of silently falling back

Comment on lines 8 to 20
type ClerkUser = {
id: string;
first_name: string | null;
last_name: string | null;
email_addresses: Array<{
id: string;
email_address: string;
}>;
public_metadata: Record<string, unknown>;
private_metadata: Record<string, unknown>;
created_at: number;
updated_at: number;
};
Copy link
Collaborator

Choose a reason for hiding this comment

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

Nit: The ClerkUser type is defined in 3 step files (get-user.ts, create-user.ts, update-user.ts). Could be extracted to a shared types file.

- Add encodeURIComponent to userId in URL paths (security)
- Return explicit errors on invalid JSON metadata instead of silent fallback
- Extract ClerkUser type to shared types.ts file
@Railly
Copy link
Contributor Author

Railly commented Dec 5, 2025

Thanks for the review @ctate! Applied all three fixes.
One thought: for publicMetadata/privateMetadata, a JSON string field works but isn't the best UX. Would it make sense to add a new configField type like keyvalue or json with a visual editor? This could benefit other plugins that need structured data too.

@Railly Railly requested a review from ctate December 5, 2025 17:00
@ctate
Copy link
Collaborator

ctate commented Dec 5, 2025

Thank you @Railly! Changes look great. Will look into expanding the visual editor capabilities.

@ctate ctate merged commit 4ca7a2e into vercel-labs:main Dec 5, 2025
1 of 2 checks passed
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