From 9819bc73b23c9800bb2b712e0604b9efb4dcadf7 Mon Sep 17 00:00:00 2001
From: Railly
Date: Fri, 28 Nov 2025 18:40:35 -0500
Subject: [PATCH 1/6] feat: Add Clerk integration plugin
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
---
app/api/ai/generate/route.ts | 4 +
.../[integrationId]/test/route.ts | 53 +++++++
.../settings/integration-form-dialog.tsx | 27 ++++
components/settings/integrations-manager.tsx | 1 +
components/workflow/nodes/action-node.tsx | 15 ++
components/workflow/utils/code-generators.ts | 13 ++
lib/api-client.ts | 4 +-
lib/credential-fetcher.ts | 12 ++
lib/db/integrations.ts | 5 +-
lib/db/schema.ts | 1 +
lib/workflow-codegen-sdk.ts | 72 ++++++++++
lib/workflow-codegen-shared.ts | 17 +++
lib/workflow-codegen.ts | 131 ++++++++++++++++++
lib/workflow-executor.workflow.ts | 30 ++++
plugins/clerk/codegen/create-user.ts | 47 +++++++
plugins/clerk/codegen/delete-user.ts | 34 +++++
plugins/clerk/codegen/get-user.ts | 33 +++++
plugins/clerk/codegen/update-user.ts | 46 ++++++
plugins/clerk/icon.tsx | 21 +++
plugins/clerk/index.tsx | 115 +++++++++++++++
plugins/clerk/settings.tsx | 47 +++++++
plugins/clerk/steps/create-user/config.tsx | 110 +++++++++++++++
plugins/clerk/steps/create-user/step.ts | 115 +++++++++++++++
plugins/clerk/steps/delete-user/config.tsx | 36 +++++
plugins/clerk/steps/delete-user/step.ts | 69 +++++++++
plugins/clerk/steps/get-user/config.tsx | 36 +++++
plugins/clerk/steps/get-user/step.ts | 84 +++++++++++
plugins/clerk/steps/update-user/config.tsx | 97 +++++++++++++
plugins/clerk/steps/update-user/step.ts | 119 ++++++++++++++++
plugins/clerk/test.ts | 43 ++++++
plugins/index.ts | 3 +-
31 files changed, 1437 insertions(+), 3 deletions(-)
create mode 100644 plugins/clerk/codegen/create-user.ts
create mode 100644 plugins/clerk/codegen/delete-user.ts
create mode 100644 plugins/clerk/codegen/get-user.ts
create mode 100644 plugins/clerk/codegen/update-user.ts
create mode 100644 plugins/clerk/icon.tsx
create mode 100644 plugins/clerk/index.tsx
create mode 100644 plugins/clerk/settings.tsx
create mode 100644 plugins/clerk/steps/create-user/config.tsx
create mode 100644 plugins/clerk/steps/create-user/step.ts
create mode 100644 plugins/clerk/steps/delete-user/config.tsx
create mode 100644 plugins/clerk/steps/delete-user/step.ts
create mode 100644 plugins/clerk/steps/get-user/config.tsx
create mode 100644 plugins/clerk/steps/get-user/step.ts
create mode 100644 plugins/clerk/steps/update-user/config.tsx
create mode 100644 plugins/clerk/steps/update-user/step.ts
create mode 100644 plugins/clerk/test.ts
diff --git a/app/api/ai/generate/route.ts b/app/api/ai/generate/route.ts
index 5717b894..a92882d6 100644
--- a/app/api/ai/generate/route.ts
+++ b/app/api/ai/generate/route.ts
@@ -198,6 +198,10 @@ Action types:
- Condition: {"actionType": "Condition", "condition": "{{@nodeId:Label.field}} === 'value'"}
- Create Chat (v0): {"actionType": "Create Chat", "message": "Create a line graph showing DAU over time", "system": "You are an expert coder"} - Use v0 for generating UI components, visualizations (charts, graphs, dashboards), landing pages, or any React/Next.js code. PREFER v0 over Generate Text/Image for any visual output like charts, graphs, or UI.
- Send Message (v0): {"actionType": "Send Message", "chatId": "{{@nodeId:Label.chatId}}", "message": "Add dark mode"} - Use this to continue a v0 chat conversation
+- Get User (Clerk): {"actionType": "Get User", "userId": "user_xxx"} - Fetch a Clerk user by ID. Returns {success, user: {id, first_name, last_name, email_addresses, ...}}.
+- Create User (Clerk): {"actionType": "Create User", "emailAddress": "user@example.com", "firstName": "John", "lastName": "Doe"} - Create a new user in Clerk. Returns {success, user: {id, ...}}. Password is optional (min 8 chars if provided, or omit to let user set their own).
+- Update User (Clerk): {"actionType": "Update User", "userId": "{{@create-user-node-id:Create User.user.id}}", "firstName": "Jane"} - Update an existing Clerk user. IMPORTANT: Use exact format {{@actualNodeId:Node Label.user.id}} where actualNodeId is the node's ID from the addNode operation.
+- Delete User (Clerk): {"actionType": "Delete User", "userId": "{{@create-user-node-id:Create User.user.id}}"} - Delete a user from Clerk. IMPORTANT: Use exact format {{@actualNodeId:Node Label.user.id}} where actualNodeId is the node's ID from the addNode operation.
CRITICAL ABOUT CONDITION NODES:
- Condition nodes evaluate a boolean expression
diff --git a/app/api/integrations/[integrationId]/test/route.ts b/app/api/integrations/[integrationId]/test/route.ts
index 0247e731..1368a6f7 100644
--- a/app/api/integrations/[integrationId]/test/route.ts
+++ b/app/api/integrations/[integrationId]/test/route.ts
@@ -68,6 +68,11 @@ export async function POST(
integration.config.firecrawlApiKey
);
break;
+ case "clerk":
+ result = await testClerkConnection(
+ integration.config.clerkSecretKey
+ );
+ break;
default:
return NextResponse.json(
{ error: "Invalid integration type" },
@@ -281,3 +286,51 @@ async function testFirecrawlConnection(
};
}
}
+
+async function testClerkConnection(
+ secretKey?: string
+): Promise {
+ try {
+ if (!secretKey) {
+ return {
+ status: "error",
+ message: "Secret key is required",
+ };
+ }
+
+ // Validate key format
+ if (!secretKey.startsWith("sk_live_") && !secretKey.startsWith("sk_test_")) {
+ return {
+ status: "error",
+ message: "Invalid secret key format. Must start with sk_live_ or sk_test_",
+ };
+ }
+
+ // Test by fetching users list
+ const response = await fetch("https://api.clerk.com/v1/users?limit=1", {
+ headers: {
+ Authorization: `Bearer ${secretKey}`,
+ "Content-Type": "application/json",
+ "User-Agent": "workflow-builder.dev",
+ },
+ });
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ return {
+ status: "error",
+ message: error.errors?.[0]?.message || "Authentication failed",
+ };
+ }
+
+ return {
+ status: "success",
+ message: "Connection successful",
+ };
+ } catch (error) {
+ return {
+ status: "error",
+ message: error instanceof Error ? error.message : "Connection failed",
+ };
+ }
+}
diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx
index 862de062..b3de0e34 100644
--- a/components/settings/integration-form-dialog.tsx
+++ b/components/settings/integration-form-dialog.tsx
@@ -41,6 +41,7 @@ type IntegrationFormData = {
const INTEGRATION_TYPES: IntegrationType[] = [
"ai-gateway",
+ "clerk",
"database",
"firecrawl",
"linear",
@@ -57,6 +58,7 @@ const INTEGRATION_LABELS: Record = {
"ai-gateway": "AI Gateway",
firecrawl: "Firecrawl",
v0: "v0",
+ clerk: "Clerk",
};
export function IntegrationFormDialog({
@@ -316,6 +318,31 @@ export function IntegrationFormDialog({
);
+ case "clerk":
+ return (
+
+
+
updateConfig("clerkSecretKey", e.target.value)}
+ placeholder="sk_live_... or sk_test_..."
+ type="password"
+ value={formData.config.clerkSecretKey || ""}
+ />
+
+ Get your secret key from{" "}
+
+ dashboard.clerk.com
+
+ {" "}under API Keys.
+
+
+ );
default:
return null;
}
diff --git a/components/settings/integrations-manager.tsx b/components/settings/integrations-manager.tsx
index 388a6efe..aeac4360 100644
--- a/components/settings/integrations-manager.tsx
+++ b/components/settings/integrations-manager.tsx
@@ -27,6 +27,7 @@ const INTEGRATION_TYPE_LABELS: Record = {
"ai-gateway": "AI Gateway",
firecrawl: "Firecrawl",
v0: "v0",
+ clerk: "Clerk",
};
type IntegrationsManagerProps = {
diff --git a/components/workflow/nodes/action-node.tsx b/components/workflow/nodes/action-node.tsx
index a12817b5..611d7f1e 100644
--- a/components/workflow/nodes/action-node.tsx
+++ b/components/workflow/nodes/action-node.tsx
@@ -82,6 +82,11 @@ const getIntegrationFromActionType = (actionType: string): string => {
Condition: "Condition",
"Create Chat": "v0",
"Send Message": "v0",
+ // Clerk
+ "Get User": "Clerk",
+ "Create User": "Clerk",
+ "Update User": "Clerk",
+ "Delete User": "Clerk",
};
return integrationMap[actionType] || "System";
};
@@ -111,6 +116,11 @@ const requiresIntegration = (actionType: string): boolean => {
"Search",
"Create Chat",
"Send Message",
+ // Clerk
+ "Get User",
+ "Create User",
+ "Update User",
+ "Delete User",
];
return requiresIntegrationActions.includes(actionType);
};
@@ -147,6 +157,11 @@ const getProviderLogo = (actionType: string) => {
case "Create Chat":
case "Send Message":
return ;
+ case "Get User":
+ case "Create User":
+ case "Update User":
+ case "Delete User":
+ return ;
default:
return ;
}
diff --git a/components/workflow/utils/code-generators.ts b/components/workflow/utils/code-generators.ts
index a7bb03e8..5eebf817 100644
--- a/components/workflow/utils/code-generators.ts
+++ b/components/workflow/utils/code-generators.ts
@@ -7,6 +7,10 @@ import databaseQueryTemplate from "@/lib/codegen-templates/database-query";
import httpRequestTemplate from "@/lib/codegen-templates/http-request";
import { generateImageCodegenTemplate } from "@/plugins/ai-gateway/codegen/generate-image";
import { generateTextCodegenTemplate } from "@/plugins/ai-gateway/codegen/generate-text";
+import { createUserCodegenTemplate } from "@/plugins/clerk/codegen/create-user";
+import { deleteUserCodegenTemplate } from "@/plugins/clerk/codegen/delete-user";
+import { getUserCodegenTemplate } from "@/plugins/clerk/codegen/get-user";
+import { updateUserCodegenTemplate } from "@/plugins/clerk/codegen/update-user";
import { scrapeCodegenTemplate } from "@/plugins/firecrawl/codegen/scrape";
import { searchCodegenTemplate } from "@/plugins/firecrawl/codegen/search";
import { createTicketCodegenTemplate } from "@/plugins/linear/codegen/create-ticket";
@@ -90,6 +94,15 @@ export async function POST(request: NextRequest) {
return createChatCodegenTemplate;
case "Send Message":
return sendMessageCodegenTemplate;
+ // Clerk
+ case "Get User":
+ return getUserCodegenTemplate;
+ case "Create User":
+ return createUserCodegenTemplate;
+ case "Update User":
+ return updateUserCodegenTemplate;
+ case "Delete User":
+ return deleteUserCodegenTemplate;
default:
return `async function actionStep(input: Record) {
"use step";
diff --git a/lib/api-client.ts b/lib/api-client.ts
index b808bbc4..dc2ef337 100644
--- a/lib/api-client.ts
+++ b/lib/api-client.ts
@@ -318,7 +318,8 @@ export type IntegrationType =
| "database"
| "ai-gateway"
| "firecrawl"
- | "v0";
+ | "v0"
+ | "clerk";
export type IntegrationConfig = {
apiKey?: string;
@@ -327,6 +328,7 @@ export type IntegrationConfig = {
url?: string;
openaiApiKey?: string;
firecrawlApiKey?: string;
+ clerkSecretKey?: string;
};
export type Integration = {
diff --git a/lib/credential-fetcher.ts b/lib/credential-fetcher.ts
index 3e4c9e86..64955718 100644
--- a/lib/credential-fetcher.ts
+++ b/lib/credential-fetcher.ts
@@ -27,6 +27,7 @@ export type WorkflowCredentials = {
DATABASE_URL?: string;
FIRECRAWL_API_KEY?: string;
V0_API_KEY?: string;
+ CLERK_SECRET_KEY?: string;
};
function mapResendConfig(config: IntegrationConfig): WorkflowCredentials {
@@ -91,6 +92,14 @@ function mapV0Config(config: IntegrationConfig): WorkflowCredentials {
return creds;
}
+function mapClerkConfig(config: IntegrationConfig): WorkflowCredentials {
+ const creds: WorkflowCredentials = {};
+ if (config.clerkSecretKey) {
+ creds.CLERK_SECRET_KEY = config.clerkSecretKey;
+ }
+ return creds;
+}
+
/**
* Map integration config to WorkflowCredentials format
*/
@@ -119,6 +128,9 @@ function mapIntegrationConfig(
if (integrationType === "v0") {
return mapV0Config(config);
}
+ if (integrationType === "clerk") {
+ return mapClerkConfig(config);
+ }
return {};
}
diff --git a/lib/db/integrations.ts b/lib/db/integrations.ts
index 598fc8cb..1056fc07 100644
--- a/lib/db/integrations.ts
+++ b/lib/db/integrations.ts
@@ -101,7 +101,8 @@ export type IntegrationType =
| "database"
| "ai-gateway"
| "firecrawl"
- | "v0";
+ | "v0"
+ | "clerk";
export type IntegrationConfig = {
// Resend
@@ -117,6 +118,8 @@ export type IntegrationConfig = {
// Firecrawl
firecrawlApiKey?: string;
// v0 (uses apiKey)
+ // Clerk
+ clerkSecretKey?: string;
};
export type DecryptedIntegration = {
diff --git a/lib/db/schema.ts b/lib/db/schema.ts
index 843cccc8..3822a4f6 100644
--- a/lib/db/schema.ts
+++ b/lib/db/schema.ts
@@ -92,6 +92,7 @@ export const integrations = pgTable("integrations", {
| "ai-gateway"
| "firecrawl"
| "v0"
+ | "clerk"
>(),
// biome-ignore lint/suspicious/noExplicitAny: JSONB type - encrypted credentials stored as JSON
config: jsonb("config").notNull().$type(),
diff --git a/lib/workflow-codegen-sdk.ts b/lib/workflow-codegen-sdk.ts
index a2a324c3..a2116df3 100644
--- a/lib/workflow-codegen-sdk.ts
+++ b/lib/workflow-codegen-sdk.ts
@@ -2,6 +2,10 @@ import "server-only";
import { generateImageCodegenTemplate } from "../plugins/ai-gateway/codegen/generate-image";
import { generateTextCodegenTemplate } from "../plugins/ai-gateway/codegen/generate-text";
+import { createUserCodegenTemplate } from "../plugins/clerk/codegen/create-user";
+import { deleteUserCodegenTemplate } from "../plugins/clerk/codegen/delete-user";
+import { getUserCodegenTemplate } from "../plugins/clerk/codegen/get-user";
+import { updateUserCodegenTemplate } from "../plugins/clerk/codegen/update-user";
import { scrapeCodegenTemplate } from "../plugins/firecrawl/codegen/scrape";
import { searchCodegenTemplate } from "../plugins/firecrawl/codegen/search";
import { createTicketCodegenTemplate } from "../plugins/linear/codegen/create-ticket";
@@ -46,6 +50,11 @@ function loadStepImplementation(actionType: string): string | null {
Condition: conditionTemplate,
"Create Chat": createChatCodegenTemplate,
"Send Message": sendMessageCodegenTemplate,
+ // Clerk
+ "Get User": getUserCodegenTemplate,
+ "Create User": createUserCodegenTemplate,
+ "Update User": updateUserCodegenTemplate,
+ "Delete User": deleteUserCodegenTemplate,
};
const template = templateMap[actionType];
@@ -619,6 +628,64 @@ export function generateWorkflowSDKCode(
"apiKey: process.env.V0_API_KEY!",
];
}
+
+ function buildClerkGetUserParams(config: Record): string[] {
+ return [
+ `userId: \`${convertTemplateToJS((config.userId as string) || "")}\``,
+ "secretKey: process.env.CLERK_SECRET_KEY!",
+ ];
+ }
+
+ function buildClerkCreateUserParams(config: Record): string[] {
+ const params = [
+ `emailAddress: \`${convertTemplateToJS((config.emailAddress as string) || "")}\``,
+ "secretKey: process.env.CLERK_SECRET_KEY!",
+ ];
+ if (config.password) {
+ params.push(`password: \`${convertTemplateToJS(config.password as string)}\``);
+ }
+ if (config.firstName) {
+ params.push(`firstName: \`${convertTemplateToJS(config.firstName as string)}\``);
+ }
+ if (config.lastName) {
+ params.push(`lastName: \`${convertTemplateToJS(config.lastName as string)}\``);
+ }
+ if (config.publicMetadata) {
+ params.push(`publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\``);
+ }
+ if (config.privateMetadata) {
+ params.push(`privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\``);
+ }
+ return params;
+ }
+
+ function buildClerkUpdateUserParams(config: Record): string[] {
+ const params = [
+ `userId: \`${convertTemplateToJS((config.userId as string) || "")}\``,
+ "secretKey: process.env.CLERK_SECRET_KEY!",
+ ];
+ if (config.firstName) {
+ params.push(`firstName: \`${convertTemplateToJS(config.firstName as string)}\``);
+ }
+ if (config.lastName) {
+ params.push(`lastName: \`${convertTemplateToJS(config.lastName as string)}\``);
+ }
+ if (config.publicMetadata) {
+ params.push(`publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\``);
+ }
+ if (config.privateMetadata) {
+ params.push(`privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\``);
+ }
+ return params;
+ }
+
+ function buildClerkDeleteUserParams(config: Record): string[] {
+ return [
+ `userId: \`${convertTemplateToJS((config.userId as string) || "")}\``,
+ "secretKey: process.env.CLERK_SECRET_KEY!",
+ ];
+ }
+
function buildStepInputParams(
actionType: string,
config: Record
@@ -636,6 +703,11 @@ export function generateWorkflowSDKCode(
Search: () => buildFirecrawlParams(actionType, config),
"Create Chat": () => buildV0CreateChatParams(config),
"Send Message": () => buildV0SendMessageParams(config),
+ // Clerk
+ "Get User": () => buildClerkGetUserParams(config),
+ "Create User": () => buildClerkCreateUserParams(config),
+ "Update User": () => buildClerkUpdateUserParams(config),
+ "Delete User": () => buildClerkDeleteUserParams(config),
};
const builder = paramBuilders[actionType];
diff --git a/lib/workflow-codegen-shared.ts b/lib/workflow-codegen-shared.ts
index 05a0a3cc..da843ea4 100644
--- a/lib/workflow-codegen-shared.ts
+++ b/lib/workflow-codegen-shared.ts
@@ -337,6 +337,23 @@ export function getStepInfo(actionType: string): {
functionName: "sendMessageStep",
importPath: "./steps/v0",
},
+ // Clerk
+ "Get User": {
+ functionName: "clerkGetUserStep",
+ importPath: "./steps/clerk",
+ },
+ "Create User": {
+ functionName: "clerkCreateUserStep",
+ importPath: "./steps/clerk",
+ },
+ "Update User": {
+ functionName: "clerkUpdateUserStep",
+ importPath: "./steps/clerk",
+ },
+ "Delete User": {
+ functionName: "clerkDeleteUserStep",
+ importPath: "./steps/clerk",
+ },
};
return (
diff --git a/lib/workflow-codegen.ts b/lib/workflow-codegen.ts
index fee5823c..bda9946c 100644
--- a/lib/workflow-codegen.ts
+++ b/lib/workflow-codegen.ts
@@ -592,6 +592,121 @@ export function generateWorkflowCode(
return lines;
}
+ // Clerk action generators
+ function generateClerkGetUserActionCode(
+ node: WorkflowNode,
+ indent: string,
+ varName: string
+ ): string[] {
+ const stepInfo = getStepInfo("Get User");
+ imports.add(
+ `import { ${stepInfo.functionName} } from '${stepInfo.importPath}';`
+ );
+
+ const config = node.data.config || {};
+ const userId = (config.userId as string) || "";
+
+ const lines = [
+ `${indent}const ${varName} = await ${stepInfo.functionName}({`,
+ `${indent} userId: ${formatTemplateValue(userId)},`,
+ `${indent}});`,
+ ];
+
+ return lines;
+ }
+
+ function generateClerkCreateUserActionCode(
+ node: WorkflowNode,
+ indent: string,
+ varName: string
+ ): string[] {
+ const stepInfo = getStepInfo("Create User");
+ imports.add(
+ `import { ${stepInfo.functionName} } from '${stepInfo.importPath}';`
+ );
+
+ const config = node.data.config || {};
+ const emailAddress = (config.emailAddress as string) || "";
+ const firstName = (config.firstName as string) || "";
+ const lastName = (config.lastName as string) || "";
+ const password = (config.password as string) || "";
+
+ const lines = [
+ `${indent}const ${varName} = await ${stepInfo.functionName}({`,
+ ];
+
+ if (emailAddress) {
+ lines.push(`${indent} emailAddress: ${formatTemplateValue(emailAddress)},`);
+ }
+ if (firstName) {
+ lines.push(`${indent} firstName: ${formatTemplateValue(firstName)},`);
+ }
+ if (lastName) {
+ lines.push(`${indent} lastName: ${formatTemplateValue(lastName)},`);
+ }
+ if (password) {
+ lines.push(`${indent} password: ${formatTemplateValue(password)},`);
+ }
+
+ lines.push(`${indent}});`);
+
+ return lines;
+ }
+
+ function generateClerkUpdateUserActionCode(
+ node: WorkflowNode,
+ indent: string,
+ varName: string
+ ): string[] {
+ const stepInfo = getStepInfo("Update User");
+ imports.add(
+ `import { ${stepInfo.functionName} } from '${stepInfo.importPath}';`
+ );
+
+ const config = node.data.config || {};
+ const userId = (config.userId as string) || "";
+ const firstName = (config.firstName as string) || "";
+ const lastName = (config.lastName as string) || "";
+
+ const lines = [
+ `${indent}const ${varName} = await ${stepInfo.functionName}({`,
+ `${indent} userId: ${formatTemplateValue(userId)},`,
+ ];
+
+ if (firstName) {
+ lines.push(`${indent} firstName: ${formatTemplateValue(firstName)},`);
+ }
+ if (lastName) {
+ lines.push(`${indent} lastName: ${formatTemplateValue(lastName)},`);
+ }
+
+ lines.push(`${indent}});`);
+
+ return lines;
+ }
+
+ function generateClerkDeleteUserActionCode(
+ node: WorkflowNode,
+ indent: string,
+ varName: string
+ ): string[] {
+ const stepInfo = getStepInfo("Delete User");
+ imports.add(
+ `import { ${stepInfo.functionName} } from '${stepInfo.importPath}';`
+ );
+
+ const config = node.data.config || {};
+ const userId = (config.userId as string) || "";
+
+ const lines = [
+ `${indent}const ${varName} = await ${stepInfo.functionName}({`,
+ `${indent} userId: ${formatTemplateValue(userId)},`,
+ `${indent}});`,
+ ];
+
+ return lines;
+ }
+
// biome-ignore lint/complexity/noExcessiveCognitiveComplexity: Action type routing requires many conditionals
function generateActionNodeCode(
node: WorkflowNode,
@@ -740,6 +855,22 @@ export function generateWorkflowCode(
lines.push(
...wrapActionCall(generateFindIssuesActionCode(indent, varName))
);
+ } else if (actionType === "Get User") {
+ lines.push(
+ ...wrapActionCall(generateClerkGetUserActionCode(node, indent, varName))
+ );
+ } else if (actionType === "Create User") {
+ lines.push(
+ ...wrapActionCall(generateClerkCreateUserActionCode(node, indent, varName))
+ );
+ } else if (actionType === "Update User") {
+ lines.push(
+ ...wrapActionCall(generateClerkUpdateUserActionCode(node, indent, varName))
+ );
+ } else if (actionType === "Delete User") {
+ lines.push(
+ ...wrapActionCall(generateClerkDeleteUserActionCode(node, indent, varName))
+ );
} else if (outputIsUsed) {
lines.push(`${indent}const ${varName} = { status: 'success' };`);
} else {
diff --git a/lib/workflow-executor.workflow.ts b/lib/workflow-executor.workflow.ts
index 3febcd0d..828ee9b2 100644
--- a/lib/workflow-executor.workflow.ts
+++ b/lib/workflow-executor.workflow.ts
@@ -226,6 +226,36 @@ async function executeActionStep(input: {
return await sendMessageStep(stepInput as any);
}
+ // Clerk actions
+ if (actionType === "Get User") {
+ const { clerkGetUserStep } = await import(
+ "../plugins/clerk/steps/get-user/step"
+ );
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type
+ return await clerkGetUserStep(stepInput as any);
+ }
+ if (actionType === "Create User") {
+ const { clerkCreateUserStep } = await import(
+ "../plugins/clerk/steps/create-user/step"
+ );
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type
+ return await clerkCreateUserStep(stepInput as any);
+ }
+ if (actionType === "Update User") {
+ const { clerkUpdateUserStep } = await import(
+ "../plugins/clerk/steps/update-user/step"
+ );
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type
+ return await clerkUpdateUserStep(stepInput as any);
+ }
+ if (actionType === "Delete User") {
+ const { clerkDeleteUserStep } = await import(
+ "../plugins/clerk/steps/delete-user/step"
+ );
+ // biome-ignore lint/suspicious/noExplicitAny: Dynamic step input type
+ return await clerkDeleteUserStep(stepInput as any);
+ }
+
// Fallback for unknown action types
return {
success: false,
diff --git a/plugins/clerk/codegen/create-user.ts b/plugins/clerk/codegen/create-user.ts
new file mode 100644
index 00000000..038bc8a0
--- /dev/null
+++ b/plugins/clerk/codegen/create-user.ts
@@ -0,0 +1,47 @@
+/**
+ * Code generation template for Create User action
+ * Used when exporting workflows to standalone Next.js projects
+ */
+export const createUserCodegenTemplate = `export async function clerkCreateUserStep(input: {
+ emailAddress: string;
+ password?: string;
+ firstName?: string;
+ lastName?: string;
+ publicMetadata?: string;
+ privateMetadata?: string;
+}) {
+ "use step";
+
+ const secretKey = process.env.CLERK_SECRET_KEY;
+
+ if (!secretKey) {
+ throw new Error('CLERK_SECRET_KEY environment variable is required');
+ }
+
+ const body: Record = {
+ email_address: [input.emailAddress],
+ };
+
+ if (input.password) body.password = input.password;
+ if (input.firstName) body.first_name = input.firstName;
+ if (input.lastName) body.last_name = input.lastName;
+ if (input.publicMetadata) body.public_metadata = JSON.parse(input.publicMetadata);
+ if (input.privateMetadata) body.private_metadata = JSON.parse(input.privateMetadata);
+
+ const response = await fetch('https://api.clerk.com/v1/users', {
+ method: 'POST',
+ headers: {
+ Authorization: \`Bearer \${secretKey}\`,
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'workflow-builder.dev',
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ throw new Error(error.errors?.[0]?.message || \`Failed to create user: \${response.status}\`);
+ }
+
+ return await response.json();
+}`;
diff --git a/plugins/clerk/codegen/delete-user.ts b/plugins/clerk/codegen/delete-user.ts
new file mode 100644
index 00000000..68e70ba8
--- /dev/null
+++ b/plugins/clerk/codegen/delete-user.ts
@@ -0,0 +1,34 @@
+/**
+ * Code generation template for Delete User action
+ * Used when exporting workflows to standalone Next.js projects
+ */
+export const deleteUserCodegenTemplate = `export async function clerkDeleteUserStep(input: {
+ userId: string;
+}) {
+ "use step";
+
+ const secretKey = process.env.CLERK_SECRET_KEY;
+
+ if (!secretKey) {
+ throw new Error('CLERK_SECRET_KEY environment variable is required');
+ }
+
+ const response = await fetch(
+ \`https://api.clerk.com/v1/users/\${input.userId}\`,
+ {
+ method: 'DELETE',
+ headers: {
+ Authorization: \`Bearer \${secretKey}\`,
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'workflow-builder.dev',
+ },
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ throw new Error(error.errors?.[0]?.message || \`Failed to delete user: \${response.status}\`);
+ }
+
+ return { deleted: true, id: input.userId };
+}`;
diff --git a/plugins/clerk/codegen/get-user.ts b/plugins/clerk/codegen/get-user.ts
new file mode 100644
index 00000000..67e91c70
--- /dev/null
+++ b/plugins/clerk/codegen/get-user.ts
@@ -0,0 +1,33 @@
+/**
+ * Code generation template for Get User action
+ * Used when exporting workflows to standalone Next.js projects
+ */
+export const getUserCodegenTemplate = `export async function clerkGetUserStep(input: {
+ userId: string;
+}) {
+ "use step";
+
+ const secretKey = process.env.CLERK_SECRET_KEY;
+
+ if (!secretKey) {
+ throw new Error('CLERK_SECRET_KEY environment variable is required');
+ }
+
+ const response = await fetch(
+ \`https://api.clerk.com/v1/users/\${input.userId}\`,
+ {
+ headers: {
+ Authorization: \`Bearer \${secretKey}\`,
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'workflow-builder.dev',
+ },
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ throw new Error(error.errors?.[0]?.message || \`Failed to get user: \${response.status}\`);
+ }
+
+ return await response.json();
+}`;
diff --git a/plugins/clerk/codegen/update-user.ts b/plugins/clerk/codegen/update-user.ts
new file mode 100644
index 00000000..692f7712
--- /dev/null
+++ b/plugins/clerk/codegen/update-user.ts
@@ -0,0 +1,46 @@
+/**
+ * Code generation template for Update User action
+ * Used when exporting workflows to standalone Next.js projects
+ */
+export const updateUserCodegenTemplate = `export async function clerkUpdateUserStep(input: {
+ userId: string;
+ firstName?: string;
+ lastName?: string;
+ publicMetadata?: string;
+ privateMetadata?: string;
+}) {
+ "use step";
+
+ const secretKey = process.env.CLERK_SECRET_KEY;
+
+ if (!secretKey) {
+ throw new Error('CLERK_SECRET_KEY environment variable is required');
+ }
+
+ const body: Record = {};
+
+ if (input.firstName) body.first_name = input.firstName;
+ if (input.lastName) body.last_name = input.lastName;
+ if (input.publicMetadata) body.public_metadata = JSON.parse(input.publicMetadata);
+ if (input.privateMetadata) body.private_metadata = JSON.parse(input.privateMetadata);
+
+ const response = await fetch(
+ \`https://api.clerk.com/v1/users/\${input.userId}\`,
+ {
+ method: 'PATCH',
+ headers: {
+ Authorization: \`Bearer \${secretKey}\`,
+ 'Content-Type': 'application/json',
+ 'User-Agent': 'workflow-builder.dev',
+ },
+ body: JSON.stringify(body),
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ throw new Error(error.errors?.[0]?.message || \`Failed to update user: \${response.status}\`);
+ }
+
+ return await response.json();
+}`;
diff --git a/plugins/clerk/icon.tsx b/plugins/clerk/icon.tsx
new file mode 100644
index 00000000..3710a7e2
--- /dev/null
+++ b/plugins/clerk/icon.tsx
@@ -0,0 +1,21 @@
+export function ClerkIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/plugins/clerk/index.tsx b/plugins/clerk/index.tsx
new file mode 100644
index 00000000..37af34df
--- /dev/null
+++ b/plugins/clerk/index.tsx
@@ -0,0 +1,115 @@
+import { User, UserPlus, UserCog, UserX } from "lucide-react";
+import type { IntegrationPlugin } from "../registry";
+import { registerIntegration } from "../registry";
+import { createUserCodegenTemplate } from "./codegen/create-user";
+import { deleteUserCodegenTemplate } from "./codegen/delete-user";
+import { getUserCodegenTemplate } from "./codegen/get-user";
+import { updateUserCodegenTemplate } from "./codegen/update-user";
+import { ClerkIcon } from "./icon";
+import { ClerkSettings } from "./settings";
+import { CreateUserConfigFields } from "./steps/create-user/config";
+import { DeleteUserConfigFields } from "./steps/delete-user/config";
+import { GetUserConfigFields } from "./steps/get-user/config";
+import { UpdateUserConfigFields } from "./steps/update-user/config";
+
+// Export step functions for workflow execution
+export { clerkGetUserStep } from "./steps/get-user/step";
+export { clerkCreateUserStep } from "./steps/create-user/step";
+export { clerkUpdateUserStep } from "./steps/update-user/step";
+export { clerkDeleteUserStep } from "./steps/delete-user/step";
+
+const clerkPlugin: IntegrationPlugin = {
+ type: "clerk",
+ label: "Clerk",
+ description: "User authentication and management",
+
+ icon: {
+ type: "svg",
+ value: "ClerkIcon",
+ svgComponent: ClerkIcon,
+ },
+
+ settingsComponent: ClerkSettings,
+
+ formFields: [
+ {
+ id: "clerkSecretKey",
+ label: "Secret Key",
+ type: "password",
+ placeholder: "sk_live_...",
+ configKey: "clerkSecretKey",
+ helpText: "Get your secret key from ",
+ helpLink: {
+ text: "Clerk Dashboard",
+ url: "https://dashboard.clerk.com",
+ },
+ },
+ ],
+
+ credentialMapping: (config) => {
+ const creds: Record = {};
+ if (config.clerkSecretKey) {
+ creds.CLERK_SECRET_KEY = String(config.clerkSecretKey);
+ }
+ return creds;
+ },
+
+ testConfig: {
+ getTestFunction: async () => {
+ const { testClerk } = await import("./test");
+ return testClerk;
+ },
+ },
+
+ actions: [
+ {
+ id: "Get User",
+ label: "Get User",
+ description: "Fetch a user by ID from Clerk",
+ category: "Clerk",
+ icon: User,
+ stepFunction: "clerkGetUserStep",
+ stepImportPath: "get-user",
+ configFields: GetUserConfigFields,
+ codegenTemplate: getUserCodegenTemplate,
+ },
+ {
+ id: "Create User",
+ label: "Create User",
+ description: "Create a new user in Clerk",
+ category: "Clerk",
+ icon: UserPlus,
+ stepFunction: "clerkCreateUserStep",
+ stepImportPath: "create-user",
+ configFields: CreateUserConfigFields,
+ codegenTemplate: createUserCodegenTemplate,
+ },
+ {
+ id: "Update User",
+ label: "Update User",
+ description: "Update an existing user in Clerk",
+ category: "Clerk",
+ icon: UserCog,
+ stepFunction: "clerkUpdateUserStep",
+ stepImportPath: "update-user",
+ configFields: UpdateUserConfigFields,
+ codegenTemplate: updateUserCodegenTemplate,
+ },
+ {
+ id: "Delete User",
+ label: "Delete User",
+ description: "Delete a user from Clerk",
+ category: "Clerk",
+ icon: UserX,
+ stepFunction: "clerkDeleteUserStep",
+ stepImportPath: "delete-user",
+ configFields: DeleteUserConfigFields,
+ codegenTemplate: deleteUserCodegenTemplate,
+ },
+ ],
+};
+
+// Auto-register on import
+registerIntegration(clerkPlugin);
+
+export default clerkPlugin;
diff --git a/plugins/clerk/settings.tsx b/plugins/clerk/settings.tsx
new file mode 100644
index 00000000..8bef3cf5
--- /dev/null
+++ b/plugins/clerk/settings.tsx
@@ -0,0 +1,47 @@
+import { Input } from "@/components/ui/input";
+import { Label } from "@/components/ui/label";
+
+export function ClerkSettings({
+ apiKey,
+ hasKey,
+ onApiKeyChange,
+}: {
+ apiKey: string;
+ hasKey?: boolean;
+ onApiKeyChange: (key: string) => void;
+ showCard?: boolean;
+ config?: Record;
+ onConfigChange?: (key: string, value: string) => void;
+}) {
+ return (
+
+
+
+
onApiKeyChange(e.target.value)}
+ placeholder={
+ hasKey ? "Secret key is configured" : "sk_live_..."
+ }
+ type="password"
+ value={apiKey}
+ />
+
+ Get your secret key from{" "}
+
+ Clerk Dashboard
+
+ {" "}under API Keys.
+
+
+
+ );
+}
diff --git a/plugins/clerk/steps/create-user/config.tsx b/plugins/clerk/steps/create-user/config.tsx
new file mode 100644
index 00000000..370cae8a
--- /dev/null
+++ b/plugins/clerk/steps/create-user/config.tsx
@@ -0,0 +1,110 @@
+import { Label } from "@/components/ui/label";
+import { TemplateBadgeInput } from "@/components/ui/template-badge-input";
+import { TemplateBadgeTextarea } from "@/components/ui/template-badge-textarea";
+
+/**
+ * Create User Config Fields Component
+ * UI for configuring the create user action
+ */
+export function CreateUserConfigFields({
+ config,
+ onUpdateConfig,
+ disabled,
+}: {
+ config: Record;
+ onUpdateConfig: (key: string, value: unknown) => void;
+ disabled?: boolean;
+}) {
+ return (
+ <>
+
+
+ onUpdateConfig("emailAddress", value)}
+ placeholder="user@example.com or {{NodeName.email}}"
+ value={(config?.emailAddress as string) || ""}
+ />
+
+
+
+
+
onUpdateConfig("password", value)}
+ placeholder="Min 8 chars, or leave empty"
+ value={(config?.password as string) || ""}
+ />
+
+ Must be at least 8 characters. Leave empty to let user set their own.
+
+
+
+
+
+ onUpdateConfig("firstName", value)}
+ placeholder="John or {{NodeName.firstName}}"
+ value={(config?.firstName as string) || ""}
+ />
+
+
+
+
+ onUpdateConfig("lastName", value)}
+ placeholder="Doe or {{NodeName.lastName}}"
+ value={(config?.lastName as string) || ""}
+ />
+
+
+
+
+
onUpdateConfig("publicMetadata", value)}
+ placeholder='{"role": "user"}'
+ rows={3}
+ value={(config?.publicMetadata as string) || ""}
+ />
+
+ JSON object visible to the frontend.
+
+
+
+
+
+
onUpdateConfig("privateMetadata", value)}
+ placeholder='{"internalId": "abc123"}'
+ rows={3}
+ value={(config?.privateMetadata as string) || ""}
+ />
+
+ JSON object only accessible server-side.
+
+
+ >
+ );
+}
diff --git a/plugins/clerk/steps/create-user/step.ts b/plugins/clerk/steps/create-user/step.ts
new file mode 100644
index 00000000..95eb70c6
--- /dev/null
+++ b/plugins/clerk/steps/create-user/step.ts
@@ -0,0 +1,115 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { getErrorMessage } from "@/lib/utils";
+
+type ClerkUser = {
+ id: string;
+ first_name: string | null;
+ last_name: string | null;
+ email_addresses: Array<{
+ id: string;
+ email_address: string;
+ }>;
+ primary_email_address_id: string | null;
+ public_metadata: Record;
+ private_metadata: Record;
+ created_at: number;
+ updated_at: number;
+};
+
+type CreateUserResult =
+ | { success: true; user: ClerkUser }
+ | { success: false; error: string };
+
+/**
+ * Create User Step
+ * Creates a new user in Clerk
+ */
+export async function clerkCreateUserStep(input: {
+ integrationId?: string;
+ emailAddress: string;
+ password?: string;
+ firstName?: string;
+ lastName?: string;
+ publicMetadata?: string;
+ privateMetadata?: string;
+}): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ const secretKey = credentials.CLERK_SECRET_KEY;
+
+ if (!secretKey) {
+ return {
+ success: false,
+ error:
+ "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ try {
+ const body: Record = {
+ email_address: [input.emailAddress],
+ };
+
+ if (input.password) {
+ body.password = input.password;
+ }
+ if (input.firstName) {
+ body.first_name = input.firstName;
+ }
+ if (input.lastName) {
+ body.last_name = input.lastName;
+ }
+ if (input.publicMetadata) {
+ try {
+ body.public_metadata = JSON.parse(input.publicMetadata);
+ } catch {
+ return {
+ success: false,
+ error: "Invalid JSON in public metadata",
+ };
+ }
+ }
+ if (input.privateMetadata) {
+ try {
+ body.private_metadata = JSON.parse(input.privateMetadata);
+ } catch {
+ return {
+ success: false,
+ error: "Invalid JSON in private metadata",
+ };
+ }
+ }
+
+ const response = await fetch("https://api.clerk.com/v1/users", {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${secretKey}`,
+ "Content-Type": "application/json",
+ "User-Agent": "workflow-builder.dev",
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ return {
+ success: false,
+ error: error.errors?.[0]?.message || `Failed to create user: ${response.status}`,
+ };
+ }
+
+ const user = await response.json();
+ return { success: true, user };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to create user: ${getErrorMessage(error)}`,
+ };
+ }
+}
diff --git a/plugins/clerk/steps/delete-user/config.tsx b/plugins/clerk/steps/delete-user/config.tsx
new file mode 100644
index 00000000..d2ff2392
--- /dev/null
+++ b/plugins/clerk/steps/delete-user/config.tsx
@@ -0,0 +1,36 @@
+import { Label } from "@/components/ui/label";
+import { TemplateBadgeInput } from "@/components/ui/template-badge-input";
+
+/**
+ * Delete User Config Fields Component
+ * UI for configuring the delete user action
+ */
+export function DeleteUserConfigFields({
+ config,
+ onUpdateConfig,
+ disabled,
+}: {
+ config: Record;
+ onUpdateConfig: (key: string, value: unknown) => void;
+ disabled?: boolean;
+}) {
+ return (
+ <>
+
+
+
onUpdateConfig("userId", value)}
+ placeholder="user_... or {{NodeName.userId}}"
+ value={(config?.userId as string) || ""}
+ />
+
+ The Clerk user ID to delete. This action is irreversible.
+
+
+ >
+ );
+}
diff --git a/plugins/clerk/steps/delete-user/step.ts b/plugins/clerk/steps/delete-user/step.ts
new file mode 100644
index 00000000..97fb11cf
--- /dev/null
+++ b/plugins/clerk/steps/delete-user/step.ts
@@ -0,0 +1,69 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { getErrorMessage } from "@/lib/utils";
+
+type DeleteUserResult =
+ | { success: true; deleted: true; id: string }
+ | { success: false; error: string };
+
+/**
+ * Delete User Step
+ * Deletes a user from Clerk
+ */
+export async function clerkDeleteUserStep(input: {
+ integrationId?: string;
+ userId: string;
+}): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ const secretKey = credentials.CLERK_SECRET_KEY;
+
+ if (!secretKey) {
+ return {
+ success: false,
+ error:
+ "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!input.userId) {
+ return {
+ success: false,
+ error: "User ID is required. Example: {{@node-id:Node Label.user.id}}",
+ };
+ }
+
+ try {
+ const response = await fetch(
+ `https://api.clerk.com/v1/users/${input.userId}`,
+ {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${secretKey}`,
+ "Content-Type": "application/json",
+ "User-Agent": "workflow-builder.dev",
+ },
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ return {
+ success: false,
+ error: error.errors?.[0]?.message || `Failed to delete user: ${response.status}`,
+ };
+ }
+
+ return { success: true, deleted: true, id: input.userId };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to delete user: ${getErrorMessage(error)}`,
+ };
+ }
+}
diff --git a/plugins/clerk/steps/get-user/config.tsx b/plugins/clerk/steps/get-user/config.tsx
new file mode 100644
index 00000000..6e6baa3c
--- /dev/null
+++ b/plugins/clerk/steps/get-user/config.tsx
@@ -0,0 +1,36 @@
+import { Label } from "@/components/ui/label";
+import { TemplateBadgeInput } from "@/components/ui/template-badge-input";
+
+/**
+ * Get User Config Fields Component
+ * UI for configuring the get user action
+ */
+export function GetUserConfigFields({
+ config,
+ onUpdateConfig,
+ disabled,
+}: {
+ config: Record;
+ onUpdateConfig: (key: string, value: unknown) => void;
+ disabled?: boolean;
+}) {
+ return (
+ <>
+
+
+
onUpdateConfig("userId", value)}
+ placeholder="user_... or {{NodeName.userId}}"
+ value={(config?.userId as string) || ""}
+ />
+
+ The Clerk user ID to fetch.
+
+
+ >
+ );
+}
diff --git a/plugins/clerk/steps/get-user/step.ts b/plugins/clerk/steps/get-user/step.ts
new file mode 100644
index 00000000..add74384
--- /dev/null
+++ b/plugins/clerk/steps/get-user/step.ts
@@ -0,0 +1,84 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { getErrorMessage } from "@/lib/utils";
+
+type ClerkUser = {
+ id: string;
+ first_name: string | null;
+ last_name: string | null;
+ email_addresses: Array<{
+ id: string;
+ email_address: string;
+ }>;
+ primary_email_address_id: string | null;
+ public_metadata: Record;
+ private_metadata: Record;
+ created_at: number;
+ updated_at: number;
+};
+
+type GetUserResult =
+ | { success: true; user: ClerkUser }
+ | { success: false; error: string };
+
+/**
+ * Get User Step
+ * Fetches a user by ID from Clerk
+ */
+export async function clerkGetUserStep(input: {
+ integrationId?: string;
+ userId: string;
+}): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ const secretKey = credentials.CLERK_SECRET_KEY;
+
+ if (!secretKey) {
+ return {
+ success: false,
+ error:
+ "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!input.userId) {
+ return {
+ success: false,
+ error: "User ID is required.",
+ };
+ }
+
+ try {
+ const response = await fetch(
+ `https://api.clerk.com/v1/users/${input.userId}`,
+ {
+ headers: {
+ Authorization: `Bearer ${secretKey}`,
+ "Content-Type": "application/json",
+ "User-Agent": "workflow-builder.dev",
+ },
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ return {
+ success: false,
+ error: error.errors?.[0]?.message || `Failed to get user: ${response.status}`,
+ };
+ }
+
+ const user = await response.json();
+ return { success: true, user };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to get user: ${getErrorMessage(error)}`,
+ };
+ }
+}
diff --git a/plugins/clerk/steps/update-user/config.tsx b/plugins/clerk/steps/update-user/config.tsx
new file mode 100644
index 00000000..664b9806
--- /dev/null
+++ b/plugins/clerk/steps/update-user/config.tsx
@@ -0,0 +1,97 @@
+import { Label } from "@/components/ui/label";
+import { TemplateBadgeInput } from "@/components/ui/template-badge-input";
+import { TemplateBadgeTextarea } from "@/components/ui/template-badge-textarea";
+
+/**
+ * Update User Config Fields Component
+ * UI for configuring the update user action
+ */
+export function UpdateUserConfigFields({
+ config,
+ onUpdateConfig,
+ disabled,
+}: {
+ config: Record;
+ onUpdateConfig: (key: string, value: unknown) => void;
+ disabled?: boolean;
+}) {
+ return (
+ <>
+
+
+
onUpdateConfig("userId", value)}
+ placeholder="user_... or {{NodeName.userId}}"
+ value={(config?.userId as string) || ""}
+ />
+
+ The Clerk user ID to update.
+
+
+
+
+
+ onUpdateConfig("firstName", value)}
+ placeholder="John or {{NodeName.firstName}}"
+ value={(config?.firstName as string) || ""}
+ />
+
+
+
+
+ onUpdateConfig("lastName", value)}
+ placeholder="Doe or {{NodeName.lastName}}"
+ value={(config?.lastName as string) || ""}
+ />
+
+
+
+
+
onUpdateConfig("publicMetadata", value)}
+ placeholder='{"role": "admin"}'
+ rows={3}
+ value={(config?.publicMetadata as string) || ""}
+ />
+
+ JSON object to merge with existing public metadata.
+
+
+
+
+
+
onUpdateConfig("privateMetadata", value)}
+ placeholder='{"stripeId": "cus_..."}'
+ rows={3}
+ value={(config?.privateMetadata as string) || ""}
+ />
+
+ JSON object to merge with existing private metadata.
+
+
+ >
+ );
+}
diff --git a/plugins/clerk/steps/update-user/step.ts b/plugins/clerk/steps/update-user/step.ts
new file mode 100644
index 00000000..a1c33fee
--- /dev/null
+++ b/plugins/clerk/steps/update-user/step.ts
@@ -0,0 +1,119 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { getErrorMessage } from "@/lib/utils";
+
+type ClerkUser = {
+ id: string;
+ first_name: string | null;
+ last_name: string | null;
+ email_addresses: Array<{
+ id: string;
+ email_address: string;
+ }>;
+ primary_email_address_id: string | null;
+ public_metadata: Record;
+ private_metadata: Record;
+ created_at: number;
+ updated_at: number;
+};
+
+type UpdateUserResult =
+ | { success: true; user: ClerkUser }
+ | { success: false; error: string };
+
+/**
+ * Update User Step
+ * Updates an existing user in Clerk
+ */
+export async function clerkUpdateUserStep(input: {
+ integrationId?: string;
+ userId: string;
+ firstName?: string;
+ lastName?: string;
+ publicMetadata?: string;
+ privateMetadata?: string;
+}): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ const secretKey = credentials.CLERK_SECRET_KEY;
+
+ if (!secretKey) {
+ return {
+ success: false,
+ error:
+ "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!input.userId) {
+ return {
+ success: false,
+ error: "User ID is required. Example: {{@node-id:Node Label.user.id}}",
+ };
+ }
+
+ try {
+ const body: Record = {};
+
+ if (input.firstName !== undefined && input.firstName !== "") {
+ body.first_name = input.firstName;
+ }
+ if (input.lastName !== undefined && input.lastName !== "") {
+ body.last_name = input.lastName;
+ }
+ if (input.publicMetadata) {
+ try {
+ body.public_metadata = JSON.parse(input.publicMetadata);
+ } catch {
+ return {
+ success: false,
+ error: "Invalid JSON in public metadata",
+ };
+ }
+ }
+ if (input.privateMetadata) {
+ try {
+ body.private_metadata = JSON.parse(input.privateMetadata);
+ } catch {
+ return {
+ success: false,
+ error: "Invalid JSON in private metadata",
+ };
+ }
+ }
+
+ const response = await fetch(
+ `https://api.clerk.com/v1/users/${input.userId}`,
+ {
+ method: "PATCH",
+ headers: {
+ Authorization: `Bearer ${secretKey}`,
+ "Content-Type": "application/json",
+ "User-Agent": "workflow-builder.dev",
+ },
+ body: JSON.stringify(body),
+ }
+ );
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ return {
+ success: false,
+ error: error.errors?.[0]?.message || `Failed to update user: ${response.status}`,
+ };
+ }
+
+ const user = await response.json();
+ return { success: true, user };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to update user: ${getErrorMessage(error)}`,
+ };
+ }
+}
diff --git a/plugins/clerk/test.ts b/plugins/clerk/test.ts
new file mode 100644
index 00000000..1f235ea4
--- /dev/null
+++ b/plugins/clerk/test.ts
@@ -0,0 +1,43 @@
+export async function testClerk(credentials: Record) {
+ try {
+ const secretKey = credentials.CLERK_SECRET_KEY;
+
+ if (!secretKey) {
+ return {
+ success: false,
+ error: "Secret key is required",
+ };
+ }
+
+ // Validate format - Clerk secret keys start with sk_live_ or sk_test_
+ if (!secretKey.startsWith("sk_live_") && !secretKey.startsWith("sk_test_")) {
+ return {
+ success: false,
+ error: "Invalid secret key format. Clerk secret keys start with 'sk_live_' or 'sk_test_'",
+ };
+ }
+
+ // Test the connection by fetching users list (limit 1)
+ const response = await fetch("https://api.clerk.com/v1/users?limit=1", {
+ headers: {
+ Authorization: `Bearer ${secretKey}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ const error = await response.json().catch(() => ({}));
+ return {
+ success: false,
+ error: error.errors?.[0]?.message || `API error: ${response.status}`,
+ };
+ }
+
+ return { success: true };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+}
diff --git a/plugins/index.ts b/plugins/index.ts
index 50ffd6ba..09bb785d 100644
--- a/plugins/index.ts
+++ b/plugins/index.ts
@@ -13,10 +13,11 @@
* 1. Delete the plugin directory
* 2. Run: pnpm discover-plugins (or it runs automatically on build)
*
- * Discovered plugins: ai-gateway, firecrawl, linear, resend, slack, v0
+ * Discovered plugins: ai-gateway, clerk, firecrawl, linear, resend, slack, v0
*/
import "./ai-gateway";
+import "./clerk";
import "./firecrawl";
import "./linear";
import "./resend";
From 9de8d177fd803f3f4e4bd9c7c0d6bb94d8078567 Mon Sep 17 00:00:00 2001
From: Railly
Date: Fri, 28 Nov 2025 19:28:23 -0500
Subject: [PATCH 2/6] fix: add user-agent to test clerk function + fix
formatting
---
.../[integrationId]/test/route.ts | 11 +++--
.../settings/integration-form-dialog.tsx | 4 +-
lib/workflow-codegen-sdk.ts | 48 ++++++++++++++-----
lib/workflow-codegen.ts | 16 +++++--
plugins/clerk/test.ts | 9 +++-
5 files changed, 63 insertions(+), 25 deletions(-)
diff --git a/app/api/integrations/[integrationId]/test/route.ts b/app/api/integrations/[integrationId]/test/route.ts
index 1368a6f7..a4a1de70 100644
--- a/app/api/integrations/[integrationId]/test/route.ts
+++ b/app/api/integrations/[integrationId]/test/route.ts
@@ -69,9 +69,7 @@ export async function POST(
);
break;
case "clerk":
- result = await testClerkConnection(
- integration.config.clerkSecretKey
- );
+ result = await testClerkConnection(integration.config.clerkSecretKey);
break;
default:
return NextResponse.json(
@@ -299,10 +297,13 @@ async function testClerkConnection(
}
// Validate key format
- if (!secretKey.startsWith("sk_live_") && !secretKey.startsWith("sk_test_")) {
+ if (
+ !(secretKey.startsWith("sk_live_") || secretKey.startsWith("sk_test_"))
+ ) {
return {
status: "error",
- message: "Invalid secret key format. Must start with sk_live_ or sk_test_",
+ message:
+ "Invalid secret key format. Must start with sk_live_ or sk_test_",
};
}
diff --git a/components/settings/integration-form-dialog.tsx b/components/settings/integration-form-dialog.tsx
index b3de0e34..396a7a58 100644
--- a/components/settings/integration-form-dialog.tsx
+++ b/components/settings/integration-form-dialog.tsx
@@ -338,8 +338,8 @@ export function IntegrationFormDialog({
target="_blank"
>
dashboard.clerk.com
-
- {" "}under API Keys.
+ {" "}
+ under API Keys.
);
diff --git a/lib/workflow-codegen-sdk.ts b/lib/workflow-codegen-sdk.ts
index a2116df3..084b08ca 100644
--- a/lib/workflow-codegen-sdk.ts
+++ b/lib/workflow-codegen-sdk.ts
@@ -636,50 +636,74 @@ export function generateWorkflowSDKCode(
];
}
- function buildClerkCreateUserParams(config: Record): string[] {
+ function buildClerkCreateUserParams(
+ config: Record
+ ): string[] {
const params = [
`emailAddress: \`${convertTemplateToJS((config.emailAddress as string) || "")}\``,
"secretKey: process.env.CLERK_SECRET_KEY!",
];
if (config.password) {
- params.push(`password: \`${convertTemplateToJS(config.password as string)}\``);
+ params.push(
+ `password: \`${convertTemplateToJS(config.password as string)}\``
+ );
}
if (config.firstName) {
- params.push(`firstName: \`${convertTemplateToJS(config.firstName as string)}\``);
+ params.push(
+ `firstName: \`${convertTemplateToJS(config.firstName as string)}\``
+ );
}
if (config.lastName) {
- params.push(`lastName: \`${convertTemplateToJS(config.lastName as string)}\``);
+ params.push(
+ `lastName: \`${convertTemplateToJS(config.lastName as string)}\``
+ );
}
if (config.publicMetadata) {
- params.push(`publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\``);
+ params.push(
+ `publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\``
+ );
}
if (config.privateMetadata) {
- params.push(`privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\``);
+ params.push(
+ `privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\``
+ );
}
return params;
}
- function buildClerkUpdateUserParams(config: Record): string[] {
+ function buildClerkUpdateUserParams(
+ config: Record
+ ): string[] {
const params = [
`userId: \`${convertTemplateToJS((config.userId as string) || "")}\``,
"secretKey: process.env.CLERK_SECRET_KEY!",
];
if (config.firstName) {
- params.push(`firstName: \`${convertTemplateToJS(config.firstName as string)}\``);
+ params.push(
+ `firstName: \`${convertTemplateToJS(config.firstName as string)}\``
+ );
}
if (config.lastName) {
- params.push(`lastName: \`${convertTemplateToJS(config.lastName as string)}\``);
+ params.push(
+ `lastName: \`${convertTemplateToJS(config.lastName as string)}\``
+ );
}
if (config.publicMetadata) {
- params.push(`publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\``);
+ params.push(
+ `publicMetadata: \`${convertTemplateToJS(config.publicMetadata as string)}\``
+ );
}
if (config.privateMetadata) {
- params.push(`privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\``);
+ params.push(
+ `privateMetadata: \`${convertTemplateToJS(config.privateMetadata as string)}\``
+ );
}
return params;
}
- function buildClerkDeleteUserParams(config: Record): string[] {
+ function buildClerkDeleteUserParams(
+ config: Record
+ ): string[] {
return [
`userId: \`${convertTemplateToJS((config.userId as string) || "")}\``,
"secretKey: process.env.CLERK_SECRET_KEY!",
diff --git a/lib/workflow-codegen.ts b/lib/workflow-codegen.ts
index bda9946c..9dc5c855 100644
--- a/lib/workflow-codegen.ts
+++ b/lib/workflow-codegen.ts
@@ -636,7 +636,9 @@ export function generateWorkflowCode(
];
if (emailAddress) {
- lines.push(`${indent} emailAddress: ${formatTemplateValue(emailAddress)},`);
+ lines.push(
+ `${indent} emailAddress: ${formatTemplateValue(emailAddress)},`
+ );
}
if (firstName) {
lines.push(`${indent} firstName: ${formatTemplateValue(firstName)},`);
@@ -861,15 +863,21 @@ export function generateWorkflowCode(
);
} else if (actionType === "Create User") {
lines.push(
- ...wrapActionCall(generateClerkCreateUserActionCode(node, indent, varName))
+ ...wrapActionCall(
+ generateClerkCreateUserActionCode(node, indent, varName)
+ )
);
} else if (actionType === "Update User") {
lines.push(
- ...wrapActionCall(generateClerkUpdateUserActionCode(node, indent, varName))
+ ...wrapActionCall(
+ generateClerkUpdateUserActionCode(node, indent, varName)
+ )
);
} else if (actionType === "Delete User") {
lines.push(
- ...wrapActionCall(generateClerkDeleteUserActionCode(node, indent, varName))
+ ...wrapActionCall(
+ generateClerkDeleteUserActionCode(node, indent, varName)
+ )
);
} else if (outputIsUsed) {
lines.push(`${indent}const ${varName} = { status: 'success' };`);
diff --git a/plugins/clerk/test.ts b/plugins/clerk/test.ts
index 1f235ea4..23898a54 100644
--- a/plugins/clerk/test.ts
+++ b/plugins/clerk/test.ts
@@ -10,10 +10,14 @@ export async function testClerk(credentials: Record) {
}
// Validate format - Clerk secret keys start with sk_live_ or sk_test_
- if (!secretKey.startsWith("sk_live_") && !secretKey.startsWith("sk_test_")) {
+ if (
+ !secretKey.startsWith("sk_live_") &&
+ !secretKey.startsWith("sk_test_")
+ ) {
return {
success: false,
- error: "Invalid secret key format. Clerk secret keys start with 'sk_live_' or 'sk_test_'",
+ error:
+ "Invalid secret key format. Clerk secret keys start with 'sk_live_' or 'sk_test_'",
};
}
@@ -22,6 +26,7 @@ export async function testClerk(credentials: Record) {
headers: {
Authorization: `Bearer ${secretKey}`,
"Content-Type": "application/json",
+ "User-Agent": "workflow-builder.dev",
},
});
From 47613de16d8aa001c64a38962184cb61c1fb174e Mon Sep 17 00:00:00 2001
From: Railly
Date: Mon, 1 Dec 2025 19:09:32 -0500
Subject: [PATCH 3/6] fix: use ClerkCredentials type in delete-user step
---
plugins/clerk/steps/delete-user.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/plugins/clerk/steps/delete-user.ts b/plugins/clerk/steps/delete-user.ts
index c4e38ab9..0206121b 100644
--- a/plugins/clerk/steps/delete-user.ts
+++ b/plugins/clerk/steps/delete-user.ts
@@ -23,7 +23,7 @@ export type ClerkDeleteUserInput = StepInput &
*/
async function stepHandler(
input: ClerkDeleteUserCoreInput,
- credentials: { CLERK_SECRET_KEY?: string }
+ credentials: ClerkCredentials
): Promise {
const secretKey = credentials.CLERK_SECRET_KEY;
From 08bd23ca6f5f9789adc3ba6b7ecd487eb42c2a79 Mon Sep 17 00:00:00 2001
From: Railly
Date: Tue, 2 Dec 2025 13:17:17 -0500
Subject: [PATCH 4/6] fix: address PR review comments
- Revert AGENTS.md to upstream version
- Remove plugins/clerk/codegen/ folder (auto-generated)
- Remove template variable hint from AI prompt (auto-generated)
---
AGENTS.md | 5 +++
app/api/ai/generate/route.ts | 3 --
plugins/clerk/codegen/create-user.ts | 47 ----------------------------
plugins/clerk/codegen/delete-user.ts | 34 --------------------
plugins/clerk/codegen/get-user.ts | 33 -------------------
plugins/clerk/codegen/update-user.ts | 46 ---------------------------
6 files changed, 5 insertions(+), 163 deletions(-)
delete mode 100644 plugins/clerk/codegen/create-user.ts
delete mode 100644 plugins/clerk/codegen/delete-user.ts
delete mode 100644 plugins/clerk/codegen/get-user.ts
delete mode 100644 plugins/clerk/codegen/update-user.ts
diff --git a/AGENTS.md b/AGENTS.md
index 3779821a..bbe28478 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -78,3 +78,8 @@ If any of the above commands fail or show errors:
- `api.workflow.*` - Workflow CRUD and operations (create, update, delete, deploy, execute, etc.)
- **No Barrel Files**: Do not create barrel/index files that re-export from other files
+## Plugin Guidelines
+- **No SDK Dependencies**: Plugin step files must use `fetch` directly instead of SDK client libraries. Do not add npm package dependencies for API integrations.
+- **No dependencies field**: Do not use the `dependencies` field in plugin `index.ts` files. All API calls should use native `fetch`.
+- **Why**: Using `fetch` instead of SDKs reduces supply chain attack surface. SDKs have transitive dependencies that could be compromised.
+
diff --git a/app/api/ai/generate/route.ts b/app/api/ai/generate/route.ts
index e4d7d68e..d79f483b 100644
--- a/app/api/ai/generate/route.ts
+++ b/app/api/ai/generate/route.ts
@@ -220,9 +220,6 @@ Edge structure:
"type": "default"
}
-TEMPLATE VARIABLES: To reference data from another node, use {{@nodeId:Label.field}}
-Example: If node id="create-user" label="Create User" returns {user: {id: "..."}}, reference it as {{@create-user:Create User.user.id}}
-
WORKFLOW FLOW:
- Trigger connects to first action
- Actions connect in sequence or to multiple branches
diff --git a/plugins/clerk/codegen/create-user.ts b/plugins/clerk/codegen/create-user.ts
deleted file mode 100644
index 038bc8a0..00000000
--- a/plugins/clerk/codegen/create-user.ts
+++ /dev/null
@@ -1,47 +0,0 @@
-/**
- * Code generation template for Create User action
- * Used when exporting workflows to standalone Next.js projects
- */
-export const createUserCodegenTemplate = `export async function clerkCreateUserStep(input: {
- emailAddress: string;
- password?: string;
- firstName?: string;
- lastName?: string;
- publicMetadata?: string;
- privateMetadata?: string;
-}) {
- "use step";
-
- const secretKey = process.env.CLERK_SECRET_KEY;
-
- if (!secretKey) {
- throw new Error('CLERK_SECRET_KEY environment variable is required');
- }
-
- const body: Record = {
- email_address: [input.emailAddress],
- };
-
- if (input.password) body.password = input.password;
- if (input.firstName) body.first_name = input.firstName;
- if (input.lastName) body.last_name = input.lastName;
- if (input.publicMetadata) body.public_metadata = JSON.parse(input.publicMetadata);
- if (input.privateMetadata) body.private_metadata = JSON.parse(input.privateMetadata);
-
- const response = await fetch('https://api.clerk.com/v1/users', {
- method: 'POST',
- headers: {
- Authorization: \`Bearer \${secretKey}\`,
- 'Content-Type': 'application/json',
- 'User-Agent': 'workflow-builder.dev',
- },
- body: JSON.stringify(body),
- });
-
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.errors?.[0]?.message || \`Failed to create user: \${response.status}\`);
- }
-
- return await response.json();
-}`;
diff --git a/plugins/clerk/codegen/delete-user.ts b/plugins/clerk/codegen/delete-user.ts
deleted file mode 100644
index 68e70ba8..00000000
--- a/plugins/clerk/codegen/delete-user.ts
+++ /dev/null
@@ -1,34 +0,0 @@
-/**
- * Code generation template for Delete User action
- * Used when exporting workflows to standalone Next.js projects
- */
-export const deleteUserCodegenTemplate = `export async function clerkDeleteUserStep(input: {
- userId: string;
-}) {
- "use step";
-
- const secretKey = process.env.CLERK_SECRET_KEY;
-
- if (!secretKey) {
- throw new Error('CLERK_SECRET_KEY environment variable is required');
- }
-
- const response = await fetch(
- \`https://api.clerk.com/v1/users/\${input.userId}\`,
- {
- method: 'DELETE',
- headers: {
- Authorization: \`Bearer \${secretKey}\`,
- 'Content-Type': 'application/json',
- 'User-Agent': 'workflow-builder.dev',
- },
- }
- );
-
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.errors?.[0]?.message || \`Failed to delete user: \${response.status}\`);
- }
-
- return { deleted: true, id: input.userId };
-}`;
diff --git a/plugins/clerk/codegen/get-user.ts b/plugins/clerk/codegen/get-user.ts
deleted file mode 100644
index 67e91c70..00000000
--- a/plugins/clerk/codegen/get-user.ts
+++ /dev/null
@@ -1,33 +0,0 @@
-/**
- * Code generation template for Get User action
- * Used when exporting workflows to standalone Next.js projects
- */
-export const getUserCodegenTemplate = `export async function clerkGetUserStep(input: {
- userId: string;
-}) {
- "use step";
-
- const secretKey = process.env.CLERK_SECRET_KEY;
-
- if (!secretKey) {
- throw new Error('CLERK_SECRET_KEY environment variable is required');
- }
-
- const response = await fetch(
- \`https://api.clerk.com/v1/users/\${input.userId}\`,
- {
- headers: {
- Authorization: \`Bearer \${secretKey}\`,
- 'Content-Type': 'application/json',
- 'User-Agent': 'workflow-builder.dev',
- },
- }
- );
-
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.errors?.[0]?.message || \`Failed to get user: \${response.status}\`);
- }
-
- return await response.json();
-}`;
diff --git a/plugins/clerk/codegen/update-user.ts b/plugins/clerk/codegen/update-user.ts
deleted file mode 100644
index 692f7712..00000000
--- a/plugins/clerk/codegen/update-user.ts
+++ /dev/null
@@ -1,46 +0,0 @@
-/**
- * Code generation template for Update User action
- * Used when exporting workflows to standalone Next.js projects
- */
-export const updateUserCodegenTemplate = `export async function clerkUpdateUserStep(input: {
- userId: string;
- firstName?: string;
- lastName?: string;
- publicMetadata?: string;
- privateMetadata?: string;
-}) {
- "use step";
-
- const secretKey = process.env.CLERK_SECRET_KEY;
-
- if (!secretKey) {
- throw new Error('CLERK_SECRET_KEY environment variable is required');
- }
-
- const body: Record = {};
-
- if (input.firstName) body.first_name = input.firstName;
- if (input.lastName) body.last_name = input.lastName;
- if (input.publicMetadata) body.public_metadata = JSON.parse(input.publicMetadata);
- if (input.privateMetadata) body.private_metadata = JSON.parse(input.privateMetadata);
-
- const response = await fetch(
- \`https://api.clerk.com/v1/users/\${input.userId}\`,
- {
- method: 'PATCH',
- headers: {
- Authorization: \`Bearer \${secretKey}\`,
- 'Content-Type': 'application/json',
- 'User-Agent': 'workflow-builder.dev',
- },
- body: JSON.stringify(body),
- }
- );
-
- if (!response.ok) {
- const error = await response.json().catch(() => ({}));
- throw new Error(error.errors?.[0]?.message || \`Failed to update user: \${response.status}\`);
- }
-
- return await response.json();
-}`;
From 5e2ff19e7e55f0f889360bfe22d23d19174f0eb2 Mon Sep 17 00:00:00 2001
From: Railly
Date: Tue, 2 Dec 2025 13:39:05 -0500
Subject: [PATCH 5/6] fix: align Clerk plugin with project conventions
- Remove codegen imports (auto-generated)
- Add outputFields to all actions
- Add maxRetries = 0 to all step functions
---
plugins/clerk/index.ts | 24 ++++++++++++++++--------
plugins/clerk/steps/create-user.ts | 1 +
plugins/clerk/steps/delete-user.ts | 1 +
plugins/clerk/steps/get-user.ts | 1 +
plugins/clerk/steps/update-user.ts | 1 +
5 files changed, 20 insertions(+), 8 deletions(-)
diff --git a/plugins/clerk/index.ts b/plugins/clerk/index.ts
index 755612ad..490bc5eb 100644
--- a/plugins/clerk/index.ts
+++ b/plugins/clerk/index.ts
@@ -1,9 +1,5 @@
import type { IntegrationPlugin } from "../registry";
import { registerIntegration } from "../registry";
-import { createUserCodegenTemplate } from "./codegen/create-user";
-import { deleteUserCodegenTemplate } from "./codegen/delete-user";
-import { getUserCodegenTemplate } from "./codegen/get-user";
-import { updateUserCodegenTemplate } from "./codegen/update-user";
import { ClerkIcon } from "./icon";
const clerkPlugin: IntegrationPlugin = {
@@ -44,7 +40,11 @@ const clerkPlugin: IntegrationPlugin = {
category: "Clerk",
stepFunction: "clerkGetUserStep",
stepImportPath: "get-user",
- codegenTemplate: getUserCodegenTemplate,
+ outputFields: [
+ { field: "user.id", description: "User ID" },
+ { field: "user.first_name", description: "First name" },
+ { field: "user.last_name", description: "Last name" },
+ ],
configFields: [
{
key: "userId",
@@ -63,7 +63,11 @@ const clerkPlugin: IntegrationPlugin = {
category: "Clerk",
stepFunction: "clerkCreateUserStep",
stepImportPath: "create-user",
- codegenTemplate: createUserCodegenTemplate,
+ outputFields: [
+ { field: "user.id", description: "User ID" },
+ { field: "user.first_name", description: "First name" },
+ { field: "user.last_name", description: "Last name" },
+ ],
configFields: [
{
key: "emailAddress",
@@ -124,7 +128,11 @@ const clerkPlugin: IntegrationPlugin = {
category: "Clerk",
stepFunction: "clerkUpdateUserStep",
stepImportPath: "update-user",
- codegenTemplate: updateUserCodegenTemplate,
+ outputFields: [
+ { field: "user.id", description: "User ID" },
+ { field: "user.first_name", description: "First name" },
+ { field: "user.last_name", description: "Last name" },
+ ],
configFields: [
{
key: "userId",
@@ -176,7 +184,7 @@ const clerkPlugin: IntegrationPlugin = {
category: "Clerk",
stepFunction: "clerkDeleteUserStep",
stepImportPath: "delete-user",
- codegenTemplate: deleteUserCodegenTemplate,
+ outputFields: [{ field: "deleted", description: "Deletion success" }],
configFields: [
{
key: "userId",
diff --git a/plugins/clerk/steps/create-user.ts b/plugins/clerk/steps/create-user.ts
index b2ab5e36..91ae703d 100644
--- a/plugins/clerk/steps/create-user.ts
+++ b/plugins/clerk/steps/create-user.ts
@@ -135,5 +135,6 @@ export async function clerkCreateUserStep(
return withStepLogging(input, () => stepHandler(input, credentials));
}
+clerkCreateUserStep.maxRetries = 0;
export const _integrationType = "clerk";
diff --git a/plugins/clerk/steps/delete-user.ts b/plugins/clerk/steps/delete-user.ts
index 0206121b..f53a507d 100644
--- a/plugins/clerk/steps/delete-user.ts
+++ b/plugins/clerk/steps/delete-user.ts
@@ -88,5 +88,6 @@ export async function clerkDeleteUserStep(
return withStepLogging(input, () => stepHandler(input, credentials));
}
+clerkDeleteUserStep.maxRetries = 0;
export const _integrationType = "clerk";
diff --git a/plugins/clerk/steps/get-user.ts b/plugins/clerk/steps/get-user.ts
index 6a5d6437..9e0e33b7 100644
--- a/plugins/clerk/steps/get-user.ts
+++ b/plugins/clerk/steps/get-user.ts
@@ -102,5 +102,6 @@ export async function clerkGetUserStep(
return withStepLogging(input, () => stepHandler(input, credentials));
}
+clerkGetUserStep.maxRetries = 0;
export const _integrationType = "clerk";
diff --git a/plugins/clerk/steps/update-user.ts b/plugins/clerk/steps/update-user.ts
index 7414d693..5ab073a5 100644
--- a/plugins/clerk/steps/update-user.ts
+++ b/plugins/clerk/steps/update-user.ts
@@ -132,5 +132,6 @@ export async function clerkUpdateUserStep(
return withStepLogging(input, () => stepHandler(input, credentials));
}
+clerkUpdateUserStep.maxRetries = 0;
export const _integrationType = "clerk";
From bf29126257bb930e1c70cacb20ec2f32659dc715 Mon Sep 17 00:00:00 2001
From: Railly
Date: Fri, 5 Dec 2025 11:51:34 -0500
Subject: [PATCH 6/6] fix: address PR review feedback for Clerk plugin
- 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
---
plugins/clerk/steps/create-user.ts | 25 +++++++++----------------
plugins/clerk/steps/delete-user.ts | 2 +-
plugins/clerk/steps/get-user.ts | 18 ++----------------
plugins/clerk/steps/update-user.ts | 27 ++++++++++-----------------
plugins/clerk/types.ts | 14 ++++++++++++++
5 files changed, 36 insertions(+), 50 deletions(-)
create mode 100644 plugins/clerk/types.ts
diff --git a/plugins/clerk/steps/create-user.ts b/plugins/clerk/steps/create-user.ts
index 91ae703d..53b0368a 100644
--- a/plugins/clerk/steps/create-user.ts
+++ b/plugins/clerk/steps/create-user.ts
@@ -4,20 +4,7 @@ import { fetchCredentials } from "@/lib/credential-fetcher";
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
import { getErrorMessage } from "@/lib/utils";
import type { ClerkCredentials } from "../credentials";
-
-type ClerkUser = {
- id: string;
- first_name: string | null;
- last_name: string | null;
- email_addresses: Array<{
- id: string;
- email_address: string;
- }>;
- public_metadata: Record;
- private_metadata: Record;
- created_at: number;
- updated_at: number;
-};
+import type { ClerkUser } from "../types";
type CreateUserResult =
| { success: true; user: ClerkUser }
@@ -80,14 +67,20 @@ async function stepHandler(
try {
body.public_metadata = JSON.parse(input.publicMetadata);
} catch {
- body.public_metadata = input.publicMetadata;
+ return {
+ success: false,
+ error: "Invalid JSON format for publicMetadata",
+ };
}
}
if (input.privateMetadata) {
try {
body.private_metadata = JSON.parse(input.privateMetadata);
} catch {
- body.private_metadata = input.privateMetadata;
+ return {
+ success: false,
+ error: "Invalid JSON format for privateMetadata",
+ };
}
}
diff --git a/plugins/clerk/steps/delete-user.ts b/plugins/clerk/steps/delete-user.ts
index f53a507d..b3ff451b 100644
--- a/plugins/clerk/steps/delete-user.ts
+++ b/plugins/clerk/steps/delete-user.ts
@@ -44,7 +44,7 @@ async function stepHandler(
try {
const response = await fetch(
- `https://api.clerk.com/v1/users/${input.userId}`,
+ `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`,
{
method: "DELETE",
headers: {
diff --git a/plugins/clerk/steps/get-user.ts b/plugins/clerk/steps/get-user.ts
index 9e0e33b7..83864e3a 100644
--- a/plugins/clerk/steps/get-user.ts
+++ b/plugins/clerk/steps/get-user.ts
@@ -4,21 +4,7 @@ import { fetchCredentials } from "@/lib/credential-fetcher";
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
import { getErrorMessage } from "@/lib/utils";
import type { ClerkCredentials } from "../credentials";
-
-type ClerkUser = {
- id: string;
- first_name: string | null;
- last_name: string | null;
- email_addresses: Array<{
- id: string;
- email_address: string;
- }>;
- primary_email_address_id: string | null;
- public_metadata: Record;
- private_metadata: Record;
- created_at: number;
- updated_at: number;
-};
+import type { ClerkUser } from "../types";
type GetUserResult =
| { success: true; user: ClerkUser }
@@ -59,7 +45,7 @@ async function stepHandler(
try {
const response = await fetch(
- `https://api.clerk.com/v1/users/${input.userId}`,
+ `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`,
{
headers: {
Authorization: `Bearer ${secretKey}`,
diff --git a/plugins/clerk/steps/update-user.ts b/plugins/clerk/steps/update-user.ts
index 5ab073a5..6083270a 100644
--- a/plugins/clerk/steps/update-user.ts
+++ b/plugins/clerk/steps/update-user.ts
@@ -4,20 +4,7 @@ import { fetchCredentials } from "@/lib/credential-fetcher";
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
import { getErrorMessage } from "@/lib/utils";
import type { ClerkCredentials } from "../credentials";
-
-type ClerkUser = {
- id: string;
- first_name: string | null;
- last_name: string | null;
- email_addresses: Array<{
- id: string;
- email_address: string;
- }>;
- public_metadata: Record;
- private_metadata: Record;
- created_at: number;
- updated_at: number;
-};
+import type { ClerkUser } from "../types";
type UpdateUserResult =
| { success: true; user: ClerkUser }
@@ -74,19 +61,25 @@ async function stepHandler(
try {
body.public_metadata = JSON.parse(input.publicMetadata);
} catch {
- body.public_metadata = input.publicMetadata;
+ return {
+ success: false,
+ error: "Invalid JSON format for publicMetadata",
+ };
}
}
if (input.privateMetadata) {
try {
body.private_metadata = JSON.parse(input.privateMetadata);
} catch {
- body.private_metadata = input.privateMetadata;
+ return {
+ success: false,
+ error: "Invalid JSON format for privateMetadata",
+ };
}
}
const response = await fetch(
- `https://api.clerk.com/v1/users/${input.userId}`,
+ `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`,
{
method: "PATCH",
headers: {
diff --git a/plugins/clerk/types.ts b/plugins/clerk/types.ts
new file mode 100644
index 00000000..269bb669
--- /dev/null
+++ b/plugins/clerk/types.ts
@@ -0,0 +1,14 @@
+export type ClerkUser = {
+ id: string;
+ first_name: string | null;
+ last_name: string | null;
+ email_addresses: Array<{
+ id: string;
+ email_address: string;
+ }>;
+ primary_email_address_id: string | null;
+ public_metadata: Record;
+ private_metadata: Record;
+ created_at: number;
+ updated_at: number;
+};