Skip to content
Merged
4 changes: 4 additions & 0 deletions app/api/ai/generate/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
54 changes: 54 additions & 0 deletions app/api/integrations/[integrationId]/test/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,9 @@ 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" },
Expand Down Expand Up @@ -281,3 +284,54 @@ async function testFirecrawlConnection(
};
}
}

async function testClerkConnection(
secretKey?: string
): Promise<TestConnectionResult> {
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",
};
}
}
27 changes: 27 additions & 0 deletions components/settings/integration-form-dialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type IntegrationFormData = {

const INTEGRATION_TYPES: IntegrationType[] = [
"ai-gateway",
"clerk",
"database",
"firecrawl",
"linear",
Expand All @@ -57,6 +58,7 @@ const INTEGRATION_LABELS: Record<IntegrationType, string> = {
"ai-gateway": "AI Gateway",
firecrawl: "Firecrawl",
v0: "v0",
clerk: "Clerk",
};

export function IntegrationFormDialog({
Expand Down Expand Up @@ -316,6 +318,31 @@ export function IntegrationFormDialog({
</p>
</div>
);
case "clerk":
return (
<div className="space-y-2">
<Label htmlFor="clerkSecretKey">Secret Key</Label>
<Input
id="clerkSecretKey"
onChange={(e) => updateConfig("clerkSecretKey", e.target.value)}
placeholder="sk_live_... or sk_test_..."
type="password"
value={formData.config.clerkSecretKey || ""}
/>
<p className="text-muted-foreground text-xs">
Get your secret key from{" "}
<a
className="underline hover:text-foreground"
href="https://dashboard.clerk.com"
rel="noopener noreferrer"
target="_blank"
>
dashboard.clerk.com
</a>{" "}
under API Keys.
</p>
</div>
);
default:
return null;
}
Expand Down
1 change: 1 addition & 0 deletions components/settings/integrations-manager.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ const INTEGRATION_TYPE_LABELS: Record<IntegrationType, string> = {
"ai-gateway": "AI Gateway",
firecrawl: "Firecrawl",
v0: "v0",
clerk: "Clerk",
};

type IntegrationsManagerProps = {
Expand Down
15 changes: 15 additions & 0 deletions components/workflow/nodes/action-node.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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";
};
Expand Down Expand Up @@ -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);
};
Expand Down Expand Up @@ -147,6 +157,11 @@ const getProviderLogo = (actionType: string) => {
case "Create Chat":
case "Send Message":
return <IntegrationIcon className="size-12" integration="v0" />;
case "Get User":
case "Create User":
case "Update User":
case "Delete User":
return <IntegrationIcon className="size-12" integration="clerk" />;
default:
return <Zap className="size-12 text-amber-300" strokeWidth={1.5} />;
}
Expand Down
13 changes: 13 additions & 0 deletions components/workflow/utils/code-generators.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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<string, unknown>) {
"use step";
Expand Down
4 changes: 3 additions & 1 deletion lib/api-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -318,7 +318,8 @@ export type IntegrationType =
| "database"
| "ai-gateway"
| "firecrawl"
| "v0";
| "v0"
| "clerk";

export type IntegrationConfig = {
apiKey?: string;
Expand All @@ -327,6 +328,7 @@ export type IntegrationConfig = {
url?: string;
openaiApiKey?: string;
firecrawlApiKey?: string;
clerkSecretKey?: string;
};

export type Integration = {
Expand Down
12 changes: 12 additions & 0 deletions lib/credential-fetcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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
*/
Expand Down Expand Up @@ -119,6 +128,9 @@ function mapIntegrationConfig(
if (integrationType === "v0") {
return mapV0Config(config);
}
if (integrationType === "clerk") {
return mapClerkConfig(config);
}
return {};
}

Expand Down
5 changes: 4 additions & 1 deletion lib/db/integrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,8 @@ export type IntegrationType =
| "database"
| "ai-gateway"
| "firecrawl"
| "v0";
| "v0"
| "clerk";

export type IntegrationConfig = {
// Resend
Expand All @@ -117,6 +118,8 @@ export type IntegrationConfig = {
// Firecrawl
firecrawlApiKey?: string;
// v0 (uses apiKey)
// Clerk
clerkSecretKey?: string;
};

export type DecryptedIntegration = {
Expand Down
1 change: 1 addition & 0 deletions lib/db/schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any>(),
Expand Down
96 changes: 96 additions & 0 deletions lib/workflow-codegen-sdk.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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];
Expand Down Expand Up @@ -619,6 +628,88 @@ export function generateWorkflowSDKCode(
"apiKey: process.env.V0_API_KEY!",
];
}

function buildClerkGetUserParams(config: Record<string, unknown>): string[] {
return [
`userId: \`${convertTemplateToJS((config.userId as string) || "")}\``,
"secretKey: process.env.CLERK_SECRET_KEY!",
];
}

function buildClerkCreateUserParams(
config: Record<string, unknown>
): 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, unknown>
): 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, unknown>
): string[] {
return [
`userId: \`${convertTemplateToJS((config.userId as string) || "")}\``,
"secretKey: process.env.CLERK_SECRET_KEY!",
];
}

function buildStepInputParams(
actionType: string,
config: Record<string, unknown>
Expand All @@ -636,6 +727,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];
Expand Down
Loading