├── .node-version ├── knip.json ├── plugins ├── fal │ ├── credentials.ts │ └── test.ts ├── v0 │ ├── credentials.ts │ ├── icon.tsx │ ├── test.ts │ └── steps │ │ ├── send-message.ts │ │ └── create-chat.ts ├── clerk │ ├── credentials.ts │ ├── icon.tsx │ ├── types.ts │ ├── test.ts │ ├── components │ │ └── user-card.tsx │ └── steps │ │ ├── get-user.ts │ │ └── delete-user.ts ├── github │ ├── credentials.ts │ ├── icon.tsx │ └── test.ts ├── slack │ ├── credentials.ts │ ├── test.ts │ ├── icon.tsx │ ├── index.ts │ └── steps │ │ └── send-slack-message.ts ├── webflow │ ├── credentials.ts │ ├── icon.tsx │ └── test.ts ├── blob │ ├── credentials.ts │ ├── icon.tsx │ └── test.ts ├── stripe │ ├── credentials.ts │ ├── icon.tsx │ └── test.ts ├── perplexity │ ├── credentials.ts │ ├── icon.tsx │ └── test.ts ├── ai-gateway │ ├── credentials.ts │ ├── icon.tsx │ ├── test.ts │ └── steps │ │ └── generate-image.ts ├── firecrawl │ ├── credentials.ts │ ├── test.ts │ ├── icon.tsx │ ├── index.ts │ └── steps │ │ ├── search.ts │ │ └── scrape.ts ├── superagent │ ├── credentials.ts │ ├── icon.tsx │ ├── test.ts │ └── steps │ │ └── redact.ts ├── linear │ ├── credentials.ts │ ├── icon.tsx │ └── test.ts ├── resend │ ├── credentials.ts │ ├── icon.tsx │ └── test.ts ├── _template │ ├── credentials.ts.txt │ ├── icon.tsx.txt │ └── test.ts.txt ├── legacy-mappings.ts └── index.ts ├── app ├── favicon.ico ├── api │ ├── auth │ │ └── [...all] │ │ │ └── route.ts │ ├── workflows │ │ ├── route.ts │ │ ├── [workflowId] │ │ │ └── code │ │ │ │ └── route.ts │ │ └── executions │ │ │ └── [executionId] │ │ │ ├── status │ │ │ └── route.ts │ │ │ └── logs │ │ │ └── route.ts │ ├── api-keys │ │ └── [keyId] │ │ │ └── route.ts │ └── ai-gateway │ │ └── status │ │ └── route.ts ├── workflows │ ├── page.tsx │ └── [workflowId] │ │ └── layout.tsx └── layout.tsx ├── drizzle ├── 0004_real_wither.sql ├── 0002_icy_otto_octavius.sql ├── 0003_clammy_tusk.sql ├── meta │ └── _journal.json └── 0001_dark_human_cannonball.sql ├── screenshot.png ├── postcss.config.mjs ├── lib ├── next-boilerplate │ ├── app │ │ ├── favicon.ico │ │ ├── globals.css │ │ └── layout.tsx │ ├── postcss.config.mjs │ ├── public │ │ ├── vercel.svg │ │ ├── window.svg │ │ ├── file.svg │ │ ├── globe.svg │ │ └── next.svg │ ├── next.config.ts │ ├── eslint.config.mjs │ ├── package.json │ ├── .gitignore │ ├── tsconfig.json │ ├── README.md │ └── lib │ │ └── credential-helper.ts ├── constants.ts ├── codegen-templates │ ├── condition.ts │ ├── database-query.ts │ └── http-request.ts ├── utils │ ├── id.ts │ ├── format-number.ts │ └── time.ts ├── auth-client.ts ├── fonts.ts ├── integrations-store.ts ├── ai-gateway │ ├── state.ts │ └── config.ts ├── steps │ ├── condition.ts │ └── trigger.ts ├── db │ └── index.ts ├── auth-providers.ts └── workflow-logging.ts ├── public ├── vercel.svg ├── window.svg ├── file.svg ├── globe.svg └── next.svg ├── .cursor └── hooks.json ├── vercel-template.json ├── next.config.ts ├── components ├── auth │ └── provider.tsx ├── theme-provider.tsx ├── ui │ ├── spinner.tsx │ ├── label.tsx │ ├── separator.tsx │ ├── code-editor.tsx │ ├── collapsible.tsx │ ├── input.tsx │ ├── sonner.tsx │ ├── avatar.tsx │ ├── checkbox.tsx │ ├── integration-icon.tsx │ ├── alert.tsx │ ├── workflow-icon.tsx │ ├── tooltip.tsx │ ├── tabs.tsx │ ├── resizable.tsx │ ├── animated-border.tsx │ ├── card.tsx │ ├── button.tsx │ └── button-group.tsx ├── global-modals.tsx ├── ai-elements │ ├── panel.tsx │ ├── connection.tsx │ ├── canvas.tsx │ ├── shimmer.tsx │ ├── controls.tsx │ └── node.tsx ├── workflow │ ├── persistent-canvas.tsx │ ├── config │ │ └── condition-config.tsx │ └── nodes │ │ ├── add-node.tsx │ │ └── trigger-node.tsx ├── github-stars-provider.tsx ├── deploy-button.tsx ├── icons │ └── github-icon.tsx ├── github-stars-button.tsx ├── github-stars-loader.tsx ├── overlays │ ├── overlay-sync.tsx │ ├── make-public-overlay.tsx │ ├── overlay.tsx │ ├── export-workflow-overlay.tsx │ ├── settings-overlay.tsx │ ├── confirm-overlay.tsx │ └── overlay-footer.tsx └── settings │ └── account-settings.tsx ├── drizzle.config.ts ├── biome.jsonc ├── components.json ├── LICENSE ├── hooks ├── use-mobile.ts └── use-touch.ts ├── playwright.config.ts ├── .gitignore ├── tsconfig.json ├── .vscode └── settings.json ├── .github └── workflows │ └── pr-checks.yml └── package.json /.node-version: -------------------------------------------------------------------------------- 1 | 22 2 | 3 | -------------------------------------------------------------------------------- /knip.json: -------------------------------------------------------------------------------- 1 | { 2 | "ignore": ["lib/next-boilerplate/**"], 3 | "ignoreDependencies": [] 4 | } 5 | -------------------------------------------------------------------------------- /plugins/fal/credentials.ts: -------------------------------------------------------------------------------- 1 | export type FalCredentials = { 2 | FAL_API_KEY?: string; 3 | }; 4 | -------------------------------------------------------------------------------- /plugins/v0/credentials.ts: -------------------------------------------------------------------------------- 1 | export type V0Credentials = { 2 | V0_API_KEY?: string; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel-labs/workflow-builder-template/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /drizzle/0004_real_wither.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "integrations" ADD COLUMN "is_managed" boolean DEFAULT false; -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel-labs/workflow-builder-template/HEAD/screenshot.png -------------------------------------------------------------------------------- /plugins/clerk/credentials.ts: -------------------------------------------------------------------------------- 1 | export type ClerkCredentials = { 2 | CLERK_SECRET_KEY?: string; 3 | }; 4 | -------------------------------------------------------------------------------- /plugins/github/credentials.ts: -------------------------------------------------------------------------------- 1 | export type GitHubCredentials = { 2 | GITHUB_TOKEN?: string; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /plugins/slack/credentials.ts: -------------------------------------------------------------------------------- 1 | export type SlackCredentials = { 2 | SLACK_API_KEY?: string; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /plugins/webflow/credentials.ts: -------------------------------------------------------------------------------- 1 | export type WebflowCredentials = { 2 | WEBFLOW_API_KEY?: string; 3 | }; 4 | -------------------------------------------------------------------------------- /plugins/blob/credentials.ts: -------------------------------------------------------------------------------- 1 | export type BlobCredentials = { 2 | BLOB_READ_WRITE_TOKEN?: string; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /plugins/stripe/credentials.ts: -------------------------------------------------------------------------------- 1 | export type StripeCredentials = { 2 | STRIPE_SECRET_KEY?: string; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /drizzle/0002_icy_otto_octavius.sql: -------------------------------------------------------------------------------- 1 | ALTER TABLE "workflows" ADD COLUMN "visibility" text DEFAULT 'private' NOT NULL; -------------------------------------------------------------------------------- /plugins/perplexity/credentials.ts: -------------------------------------------------------------------------------- 1 | export type PerplexityCredentials = { 2 | PERPLEXITY_API_KEY?: string; 3 | }; 4 | -------------------------------------------------------------------------------- /plugins/ai-gateway/credentials.ts: -------------------------------------------------------------------------------- 1 | export type AiGatewayCredentials = { 2 | AI_GATEWAY_API_KEY?: string; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /plugins/firecrawl/credentials.ts: -------------------------------------------------------------------------------- 1 | export type FirecrawlCredentials = { 2 | FIRECRAWL_API_KEY?: string; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /plugins/superagent/credentials.ts: -------------------------------------------------------------------------------- 1 | export type SuperagentCredentials = { 2 | SUPERAGENT_API_KEY?: string; 3 | }; 4 | 5 | -------------------------------------------------------------------------------- /plugins/linear/credentials.ts: -------------------------------------------------------------------------------- 1 | export type LinearCredentials = { 2 | LINEAR_API_KEY?: string; 3 | LINEAR_TEAM_ID?: string; 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /lib/next-boilerplate/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vercel-labs/workflow-builder-template/HEAD/lib/next-boilerplate/app/favicon.ico -------------------------------------------------------------------------------- /plugins/resend/credentials.ts: -------------------------------------------------------------------------------- 1 | export type ResendCredentials = { 2 | RESEND_API_KEY?: string; 3 | RESEND_FROM_EMAIL?: string; 4 | }; 5 | 6 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/next-boilerplate/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: { 3 | "@tailwindcss/postcss": {}, 4 | }, 5 | }; 6 | 7 | export default config; 8 | -------------------------------------------------------------------------------- /lib/next-boilerplate/public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.cursor/hooks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 1, 3 | "hooks": { 4 | "afterFileEdit": [ 5 | { 6 | "command": "npx ultracite fix" 7 | } 8 | ] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app/api/auth/[...all]/route.ts: -------------------------------------------------------------------------------- 1 | import { toNextJsHandler } from "better-auth/next-js"; 2 | import { auth } from "@/lib/auth"; 3 | 4 | export const { GET, POST } = toNextJsHandler(auth); 5 | -------------------------------------------------------------------------------- /lib/next-boilerplate/next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | 3 | const nextConfig: NextConfig = { 4 | /* config options here */ 5 | }; 6 | 7 | export default nextConfig; 8 | -------------------------------------------------------------------------------- /vercel-template.json: -------------------------------------------------------------------------------- 1 | { 2 | "products": [ 3 | { 4 | "type": "integration", 5 | "protocol": "storage", 6 | "productSlug": "neon", 7 | "integrationSlug": "neon" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next"; 2 | import { withWorkflow } from "workflow/next"; 3 | 4 | const nextConfig: NextConfig = { 5 | /* config options here */ 6 | }; 7 | 8 | export default withWorkflow(nextConfig); 9 | -------------------------------------------------------------------------------- /lib/constants.ts: -------------------------------------------------------------------------------- 1 | // Vercel deployment configuration 2 | export const VERCEL_DEPLOY_URL = "https://vercel.new/workflow-builder"; 3 | 4 | // Vercel button URL for markdown 5 | export const VERCEL_DEPLOY_BUTTON_URL = `[![Deploy with Vercel](https://vercel.com/button)](${VERCEL_DEPLOY_URL})`; 6 | -------------------------------------------------------------------------------- /plugins/_template/credentials.ts.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * CREDENTIALS TYPE 3 | * 4 | * Defines the credential fields for your integration. 5 | */ 6 | 7 | export type [IntegrationName]Credentials = { 8 | [INTEGRATION_NAME]_API_KEY?: string; 9 | // Add other credential fields as needed 10 | }; 11 | -------------------------------------------------------------------------------- /components/auth/provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { ReactNode } from "react"; 4 | 5 | export function AuthProvider({ children }: { children: ReactNode }) { 6 | // No automatic session creation - let users browse anonymously 7 | // Anonymous sessions will be created on-demand when needed 8 | return <>{children}; 9 | } 10 | -------------------------------------------------------------------------------- /drizzle.config.ts: -------------------------------------------------------------------------------- 1 | import { config } from "dotenv"; 2 | import type { Config } from "drizzle-kit"; 3 | 4 | config(); 5 | 6 | export default { 7 | schema: "./lib/db/schema.ts", 8 | out: "./drizzle", 9 | dialect: "postgresql", 10 | dbCredentials: { 11 | url: process.env.DATABASE_URL || "postgres://localhost:5432/workflow", 12 | }, 13 | } satisfies Config; 14 | -------------------------------------------------------------------------------- /components/theme-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { ThemeProvider as NextThemesProvider } from "next-themes"; 4 | import type * as React from "react"; 5 | 6 | export function ThemeProvider({ 7 | children, 8 | ...props 9 | }: React.ComponentProps) { 10 | return {children}; 11 | } 12 | -------------------------------------------------------------------------------- /lib/codegen-templates/condition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Code template for Condition action step 3 | * This is a string template used for code generation - keep as string export 4 | */ 5 | export default `export async function conditionStep(input: { 6 | condition: boolean; 7 | }) { 8 | "use step"; 9 | 10 | // Evaluate condition 11 | return { condition: input.condition }; 12 | }`; 13 | -------------------------------------------------------------------------------- /plugins/ai-gateway/icon.tsx: -------------------------------------------------------------------------------- 1 | export function AiGatewayIcon({ className }: { className?: string }) { 2 | return ( 3 | 9 | 10 | 11 | ); 12 | } 13 | 14 | -------------------------------------------------------------------------------- /public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /biome.jsonc: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@biomejs/biome/configuration_schema.json", 3 | "extends": ["ultracite/core", "ultracite/react", "ultracite/next"], 4 | "files": { 5 | "includes": [ 6 | "**/*", 7 | "!components/ui", 8 | "!components/ai-elements", 9 | "!lib/utils.ts", 10 | "!hooks/use-mobile.ts", 11 | "!plugins" 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /lib/utils/id.ts: -------------------------------------------------------------------------------- 1 | import { customAlphabet } from "nanoid"; 2 | 3 | // Create a nanoid generator with lowercase URL-safe characters 4 | const nanoid = customAlphabet("0123456789abcdefghijklmnopqrstuvwxyz", 21); 5 | 6 | /** 7 | * Generate a unique lowercase ID suitable for database records and Vercel project names 8 | */ 9 | export function generateId(): string { 10 | return nanoid(); 11 | } 12 | -------------------------------------------------------------------------------- /lib/next-boilerplate/public/window.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/next-boilerplate/public/file.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/spinner.tsx: -------------------------------------------------------------------------------- 1 | import { Loader2Icon } from "lucide-react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Spinner({ className, ...props }: React.ComponentProps<"svg">) { 6 | return ( 7 | 13 | ) 14 | } 15 | 16 | export { Spinner } 17 | -------------------------------------------------------------------------------- /plugins/blob/icon.tsx: -------------------------------------------------------------------------------- 1 | export function BlobIcon({ className }: { className?: string }) { 2 | return ( 3 | 10 | Vercel 11 | 12 | 13 | ); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /lib/auth-client.ts: -------------------------------------------------------------------------------- 1 | import { anonymousClient } from "better-auth/client/plugins"; 2 | import { createAuthClient } from "better-auth/react"; 3 | 4 | export const authClient = createAuthClient({ 5 | baseURL: 6 | typeof window !== "undefined" 7 | ? window.location.origin 8 | : "http://localhost:3000", 9 | plugins: [anonymousClient()], 10 | }); 11 | 12 | export const { signIn, signOut, signUp, useSession } = authClient; 13 | -------------------------------------------------------------------------------- /lib/fonts.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Geist_Mono as createMono, 3 | Geist as createSans, 4 | } from "next/font/google"; 5 | 6 | export const sans = createSans({ 7 | variable: "--font-geist-sans", 8 | subsets: ["latin"], 9 | weight: "variable", 10 | display: "swap", 11 | }); 12 | 13 | export const mono = createMono({ 14 | variable: "--font-geist-mono", 15 | subsets: ["latin"], 16 | weight: "variable", 17 | display: "swap", 18 | }); 19 | -------------------------------------------------------------------------------- /components/global-modals.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { OverlayContainer } from "@/components/overlays/overlay-container"; 4 | import { OverlaySync } from "@/components/overlays/overlay-sync"; 5 | 6 | /** 7 | * Global modals and overlays that need to be rendered once at app level 8 | */ 9 | export function GlobalModals() { 10 | return ( 11 | <> 12 | 13 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /components/ai-elements/panel.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils"; 2 | import { Panel as PanelPrimitive } from "@xyflow/react"; 3 | import type { ComponentProps } from "react"; 4 | 5 | type PanelProps = ComponentProps; 6 | 7 | export const Panel = ({ className, ...props }: PanelProps) => ( 8 | 15 | ); 16 | -------------------------------------------------------------------------------- /drizzle/0003_clammy_tusk.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "api_keys" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "user_id" text NOT NULL, 4 | "name" text, 5 | "key_hash" text NOT NULL, 6 | "key_prefix" text NOT NULL, 7 | "created_at" timestamp DEFAULT now() NOT NULL, 8 | "last_used_at" timestamp 9 | ); 10 | --> statement-breakpoint 11 | ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action; -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "iconLibrary": "lucide", 14 | "aliases": { 15 | "components": "@/components", 16 | "utils": "@/lib/utils", 17 | "ui": "@/components/ui", 18 | "lib": "@/lib", 19 | "hooks": "@/hooks" 20 | }, 21 | "registries": {} 22 | } 23 | -------------------------------------------------------------------------------- /plugins/webflow/icon.tsx: -------------------------------------------------------------------------------- 1 | export function WebflowIcon({ className }: { className?: string }) { 2 | return ( 3 | 10 | Webflow 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /components/workflow/persistent-canvas.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { usePathname } from "next/navigation"; 4 | import { WorkflowCanvas } from "./workflow-canvas"; 5 | 6 | export function PersistentCanvas() { 7 | const pathname = usePathname(); 8 | 9 | // Show canvas on homepage and workflow pages 10 | const showCanvas = pathname === "/" || pathname.startsWith("/workflows/"); 11 | 12 | if (!showCanvas) { 13 | return null; 14 | } 15 | 16 | return ( 17 |
18 | 19 |
20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /lib/next-boilerplate/eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig, globalIgnores } from "eslint/config"; 2 | import nextVitals from "eslint-config-next/core-web-vitals"; 3 | import nextTs from "eslint-config-next/typescript"; 4 | 5 | const eslintConfig = defineConfig([ 6 | ...nextVitals, 7 | ...nextTs, 8 | // Override default ignores of eslint-config-next. 9 | globalIgnores([ 10 | // Default ignores of eslint-config-next: 11 | ".next/**", 12 | "out/**", 13 | "build/**", 14 | "next-env.d.ts", 15 | ]), 16 | ]); 17 | 18 | export default eslintConfig; 19 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2025 Vercel, Inc. 2 | 3 | Licensed under the Apache License, Version 2.0 (the "License"); 4 | you may not use this file except in compliance with the License. 5 | You may obtain a copy of the License at 6 | 7 | http://www.apache.org/licenses/LICENSE-2.0 8 | 9 | Unless required by applicable law or agreed to in writing, software 10 | distributed under the License is distributed on an "AS IS" BASIS, 11 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | See the License for the specific language governing permissions and 13 | limitations under the License. 14 | -------------------------------------------------------------------------------- /plugins/v0/icon.tsx: -------------------------------------------------------------------------------- 1 | export function V0Icon({ className }: { className?: string }) { 2 | return ( 3 | 9 | 10 | 11 | 12 | ); 13 | } 14 | 15 | -------------------------------------------------------------------------------- /lib/next-boilerplate/app/globals.css: -------------------------------------------------------------------------------- 1 | @import "tailwindcss"; 2 | 3 | :root { 4 | --background: #ffffff; 5 | --foreground: #171717; 6 | } 7 | 8 | @theme inline { 9 | --color-background: var(--background); 10 | --color-foreground: var(--foreground); 11 | --font-sans: var(--font-geist-sans); 12 | --font-mono: var(--font-geist-mono); 13 | } 14 | 15 | @media (prefers-color-scheme: dark) { 16 | :root { 17 | --background: #0a0a0a; 18 | --foreground: #ededed; 19 | } 20 | } 21 | 22 | body { 23 | background: var(--background); 24 | color: var(--foreground); 25 | font-family: Arial, Helvetica, sans-serif; 26 | } 27 | -------------------------------------------------------------------------------- /components/github-stars-provider.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { createContext, type ReactNode, useContext } from "react"; 4 | 5 | const GitHubStarsContext = createContext(null); 6 | 7 | type GitHubStarsProviderProps = { 8 | children: ReactNode; 9 | stars: number | null; 10 | }; 11 | 12 | export function GitHubStarsProvider({ 13 | children, 14 | stars, 15 | }: GitHubStarsProviderProps) { 16 | return ( 17 | 18 | {children} 19 | 20 | ); 21 | } 22 | 23 | export function useGitHubStars() { 24 | return useContext(GitHubStarsContext); 25 | } 26 | -------------------------------------------------------------------------------- /hooks/use-mobile.ts: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | const MOBILE_BREAKPOINT = 768 4 | 5 | export function useIsMobile() { 6 | const [isMobile, setIsMobile] = React.useState(undefined) 7 | 8 | React.useEffect(() => { 9 | const mql = window.matchMedia(`(max-width: ${MOBILE_BREAKPOINT - 1}px)`) 10 | const onChange = () => { 11 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 12 | } 13 | mql.addEventListener("change", onChange) 14 | setIsMobile(window.innerWidth < MOBILE_BREAKPOINT) 15 | return () => mql.removeEventListener("change", onChange) 16 | }, []) 17 | 18 | return !!isMobile 19 | } 20 | -------------------------------------------------------------------------------- /lib/next-boilerplate/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "next-boilerplate", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "eslint" 10 | }, 11 | "dependencies": { 12 | "next": "16.0.10", 13 | "react": "19.2.0", 14 | "react-dom": "19.2.0" 15 | }, 16 | "devDependencies": { 17 | "@tailwindcss/postcss": "^4", 18 | "@types/node": "^20", 19 | "@types/react": "^19", 20 | "@types/react-dom": "^19", 21 | "eslint": "^9", 22 | "eslint-config-next": "16.0.3", 23 | "tailwindcss": "^4", 24 | "typescript": "^5" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /lib/next-boilerplate/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | .pnpm-debug.log* 32 | 33 | # env files (can opt-in for committing if needed) 34 | .env* 35 | 36 | # vercel 37 | .vercel 38 | 39 | # typescript 40 | *.tsbuildinfo 41 | next-env.d.ts 42 | -------------------------------------------------------------------------------- /plugins/resend/icon.tsx: -------------------------------------------------------------------------------- 1 | export function ResendIcon({ className }: { className?: string }) { 2 | return ( 3 | 9 | 10 | 11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /plugins/perplexity/icon.tsx: -------------------------------------------------------------------------------- 1 | export function PerplexityIcon({ className }: { className?: string }) { 2 | return ( 3 | 13 | Perplexity 14 | 15 | 16 | 17 | 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /components/ai-elements/connection.tsx: -------------------------------------------------------------------------------- 1 | import type { ConnectionLineComponent } from "@xyflow/react"; 2 | 3 | const HALF = 0.5; 4 | 5 | export const Connection: ConnectionLineComponent = ({ 6 | fromX, 7 | fromY, 8 | toX, 9 | toY, 10 | }) => ( 11 | 12 | 19 | 27 | 28 | ); 29 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Label as LabelPrimitive } from "radix-ui" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /plugins/v0/test.ts: -------------------------------------------------------------------------------- 1 | export async function testV0(credentials: Record) { 2 | try { 3 | const apiKey = credentials.V0_API_KEY; 4 | 5 | if (!apiKey) { 6 | return { 7 | success: false, 8 | error: "API key is required", 9 | }; 10 | } 11 | 12 | // Test the API key by making a request to get user info 13 | const { createClient } = await import("v0-sdk"); 14 | const client = createClient({ apiKey }); 15 | await client.user.get(); 16 | 17 | return { 18 | success: true, 19 | }; 20 | } catch (error) { 21 | return { 22 | success: false, 23 | error: error instanceof Error ? error.message : String(error), 24 | }; 25 | } 26 | } 27 | 28 | -------------------------------------------------------------------------------- /hooks/use-touch.ts: -------------------------------------------------------------------------------- 1 | import { useEffect, useState } from "react"; 2 | 3 | /** 4 | * Detects if the device has touch capability. 5 | * Useful for determining if auto-focus should be disabled (to avoid opening the keyboard). 6 | * Returns undefined during SSR/hydration, then true/false after mount. 7 | */ 8 | export function useIsTouch() { 9 | const [isTouch, setIsTouch] = useState(undefined); 10 | 11 | useEffect(() => { 12 | const hasTouch = 13 | "ontouchstart" in window || 14 | navigator.maxTouchPoints > 0 || 15 | // @ts-expect-error - msMaxTouchPoints is IE-specific 16 | navigator.msMaxTouchPoints > 0; 17 | 18 | setIsTouch(hasTouch); 19 | }, []); 20 | 21 | return isTouch; 22 | } 23 | -------------------------------------------------------------------------------- /playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from "@playwright/test"; 2 | 3 | export default defineConfig({ 4 | testDir: "./e2e", 5 | fullyParallel: true, 6 | forbidOnly: !!process.env.CI, 7 | retries: process.env.CI ? 2 : 0, 8 | workers: process.env.CI ? 1 : undefined, 9 | reporter: "html", 10 | timeout: 60_000, 11 | use: { 12 | baseURL: "http://localhost:3000", 13 | trace: "on-first-retry", 14 | screenshot: "only-on-failure", 15 | navigationTimeout: 60_000, 16 | }, 17 | projects: [ 18 | { 19 | name: "chromium", 20 | use: { ...devices["Desktop Chrome"] }, 21 | }, 22 | ], 23 | webServer: { 24 | command: "pnpm dev", 25 | url: "http://localhost:3000", 26 | reuseExistingServer: !process.env.CI, 27 | timeout: 120_000, 28 | }, 29 | }); 30 | -------------------------------------------------------------------------------- /plugins/superagent/icon.tsx: -------------------------------------------------------------------------------- 1 | export function SuperagentIcon({ className }: { className?: string }) { 2 | return ( 3 | 12 | Superagent 13 | 17 | 21 | 25 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /components/ai-elements/canvas.tsx: -------------------------------------------------------------------------------- 1 | import { Background, ReactFlow, type ReactFlowProps } from "@xyflow/react"; 2 | import type { ReactNode } from "react"; 3 | import "@xyflow/react/dist/style.css"; 4 | 5 | type CanvasProps = ReactFlowProps & { 6 | children?: ReactNode; 7 | }; 8 | 9 | export const Canvas = ({ children, ...props }: CanvasProps) => { 10 | return ( 11 | 20 | 26 | {children} 27 | 28 | ); 29 | }; 30 | -------------------------------------------------------------------------------- /components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Separator as SeparatorPrimitive } from "radix-ui" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /lib/next-boilerplate/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react-jsx", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | } 20 | ], 21 | "paths": { 22 | "@/*": ["./*"] 23 | } 24 | }, 25 | "include": [ 26 | "next-env.d.ts", 27 | "**/*.ts", 28 | "**/*.tsx", 29 | ".next/types/**/*.ts", 30 | ".next/dev/types/**/*.ts", 31 | "**/*.mts" 32 | ], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /components/ui/code-editor.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import MonacoEditor, { type EditorProps, type OnMount } from "@monaco-editor/react"; 4 | import { useTheme } from "next-themes"; 5 | import { vercelDarkTheme } from "@/lib/monaco-theme"; 6 | 7 | export function CodeEditor(props: EditorProps) { 8 | const { resolvedTheme } = useTheme(); 9 | 10 | const handleEditorMount: OnMount = (editor, monaco) => { 11 | monaco.editor.defineTheme("vercel-dark", vercelDarkTheme); 12 | monaco.editor.setTheme(resolvedTheme === "dark" ? "vercel-dark" : "light"); 13 | 14 | if (props.onMount) { 15 | props.onMount(editor, monaco); 16 | } 17 | }; 18 | 19 | return ( 20 | 25 | ); 26 | } 27 | 28 | -------------------------------------------------------------------------------- /plugins/stripe/icon.tsx: -------------------------------------------------------------------------------- 1 | export function StripeIcon({ className }: { className?: string }) { 2 | return ( 3 | 10 | Stripe 11 | 12 | 13 | ); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /lib/next-boilerplate/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Geist, Geist_Mono } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | const geistSans = Geist({ 6 | variable: "--font-geist-sans", 7 | subsets: ["latin"], 8 | }); 9 | 10 | const geistMono = Geist_Mono({ 11 | variable: "--font-geist-mono", 12 | subsets: ["latin"], 13 | }); 14 | 15 | export const metadata: Metadata = { 16 | title: "Create Next App", 17 | description: "Generated by create next app", 18 | }; 19 | 20 | export default function RootLayout({ 21 | children, 22 | }: Readonly<{ 23 | children: React.ReactNode; 24 | }>) { 25 | return ( 26 | 27 | 30 | {children} 31 | 32 | 33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /plugins/ai-gateway/test.ts: -------------------------------------------------------------------------------- 1 | import { createGateway, generateText } from "ai"; 2 | 3 | export async function testAiGateway(credentials: Record) { 4 | try { 5 | const apiKey = credentials.AI_GATEWAY_API_KEY; 6 | 7 | if (!apiKey) { 8 | return { 9 | success: false, 10 | error: "AI_GATEWAY_API_KEY is required", 11 | }; 12 | } 13 | 14 | // Try a simple text generation to verify the API key works 15 | const gateway = createGateway({ apiKey }); 16 | 17 | await generateText({ 18 | model: gateway("openai/gpt-4o-mini"), 19 | prompt: "Say 'test' if you can read this.", 20 | }); 21 | 22 | return { 23 | success: true, 24 | }; 25 | } catch (error) { 26 | return { 27 | success: false, 28 | error: error instanceof Error ? error.message : String(error), 29 | }; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /components/deploy-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import { VERCEL_DEPLOY_URL } from "@/lib/constants"; 5 | 6 | export function DeployButton() { 7 | return ( 8 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /lib/integrations-store.ts: -------------------------------------------------------------------------------- 1 | import { atom } from "jotai"; 2 | import type { Integration } from "@/lib/api-client"; 3 | 4 | // Store for all user integrations 5 | export const integrationsAtom = atom([]); 6 | 7 | // Track if integrations have been loaded (to avoid showing warnings before fetch) 8 | export const integrationsLoadedAtom = atom(false); 9 | 10 | // Selected integration for forms/dialogs 11 | export const selectedIntegrationAtom = atom(null); 12 | 13 | // Version counter that increments when integrations are added/deleted/modified 14 | // Components can use this to know when to re-fetch integrations 15 | export const integrationsVersionAtom = atom(0); 16 | 17 | // Derived atom to get all integration IDs for quick lookup 18 | export const integrationIdsAtom = atom((get) => { 19 | const integrations = get(integrationsAtom); 20 | return new Set(integrations.map((i) => i.id)); 21 | }); 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | /test-results/ 16 | /playwright-report/ 17 | /blob-report/ 18 | /playwright/.cache/ 19 | 20 | # next.js 21 | /.next/ 22 | /out/ 23 | /.swc/ 24 | 25 | # production 26 | /build 27 | 28 | # misc 29 | .DS_Store 30 | *.pem 31 | 32 | # debug 33 | npm-debug.log* 34 | yarn-debug.log* 35 | yarn-error.log* 36 | .pnpm-debug.log* 37 | 38 | # env files (can opt-in for committing if needed) 39 | .env* 40 | 41 | # vercel 42 | .vercel 43 | 44 | # typescript 45 | *.tsbuildinfo 46 | next-env.d.ts 47 | .env*.local 48 | 49 | tmp/ 50 | 51 | # generated files 52 | lib/types/integration.ts 53 | lib/codegen-registry.ts 54 | lib/step-registry.ts 55 | lib/output-display-configs.ts -------------------------------------------------------------------------------- /components/ui/collapsible.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as CollapsiblePrimitive from "@radix-ui/react-collapsible" 4 | 5 | function Collapsible({ 6 | ...props 7 | }: React.ComponentProps) { 8 | return 9 | } 10 | 11 | function CollapsibleTrigger({ 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 19 | ) 20 | } 21 | 22 | function CollapsibleContent({ 23 | ...props 24 | }: React.ComponentProps) { 25 | return ( 26 | 30 | ) 31 | } 32 | 33 | export { Collapsible, CollapsibleTrigger, CollapsibleContent } 34 | -------------------------------------------------------------------------------- /drizzle/meta/_journal.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "7", 3 | "dialect": "postgresql", 4 | "entries": [ 5 | { 6 | "idx": 0, 7 | "version": "7", 8 | "when": 1763524363750, 9 | "tag": "0000_easy_supernaut", 10 | "breakpoints": true 11 | }, 12 | { 13 | "idx": 1, 14 | "version": "7", 15 | "when": 1763686045429, 16 | "tag": "0001_dark_human_cannonball", 17 | "breakpoints": true 18 | }, 19 | { 20 | "idx": 2, 21 | "version": "7", 22 | "when": 1764691121867, 23 | "tag": "0002_icy_otto_octavius", 24 | "breakpoints": true 25 | }, 26 | { 27 | "idx": 3, 28 | "version": "7", 29 | "when": 1764694021611, 30 | "tag": "0003_clammy_tusk", 31 | "breakpoints": true 32 | }, 33 | { 34 | "idx": 4, 35 | "version": "7", 36 | "when": 1765834764376, 37 | "tag": "0004_real_wither", 38 | "breakpoints": true 39 | } 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /components/icons/github-icon.tsx: -------------------------------------------------------------------------------- 1 | export function GitHubIcon({ className }: { className?: string }) { 2 | return ( 3 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /lib/utils/format-number.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Formats a number into an abbreviated form (e.g., 1.1k, 2.5M) 3 | * 4 | * @param num - The number to format 5 | * @returns The formatted string 6 | * 7 | * @example 8 | * formatAbbreviatedNumber(1109) // "1.1k" 9 | * formatAbbreviatedNumber(1500) // "1.5k" 10 | * formatAbbreviatedNumber(1000000) // "1M" 11 | * formatAbbreviatedNumber(500) // "500" 12 | */ 13 | export function formatAbbreviatedNumber(num: number): string { 14 | if (num >= 1_000_000) { 15 | const formatted = (num / 1_000_000).toFixed(1); 16 | // Remove .0 if present 17 | return formatted.endsWith(".0") 18 | ? `${Math.floor(num / 1_000_000)}M` 19 | : `${formatted}M`; 20 | } 21 | 22 | if (num >= 1000) { 23 | const formatted = (num / 1000).toFixed(1); 24 | // Remove .0 if present 25 | return formatted.endsWith(".0") 26 | ? `${Math.floor(num / 1000)}k` 27 | : `${formatted}k`; 28 | } 29 | 30 | return num.toString(); 31 | } 32 | -------------------------------------------------------------------------------- /lib/ai-gateway/state.ts: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { atom } from "jotai"; 4 | import type { VercelTeam } from "@/lib/api-client"; 5 | 6 | /** 7 | * AI Gateway status (fetched from API) 8 | */ 9 | export type AiGatewayStatus = { 10 | /** Whether the user keys feature is enabled */ 11 | enabled: boolean; 12 | /** Whether the user is signed in */ 13 | signedIn: boolean; 14 | /** Whether the user signed in with Vercel OAuth */ 15 | isVercelUser: boolean; 16 | /** Whether the user has a managed AI Gateway integration */ 17 | hasManagedKey: boolean; 18 | /** The ID of the managed integration (if exists) */ 19 | managedIntegrationId?: string; 20 | } | null; 21 | 22 | export const aiGatewayStatusAtom = atom(null); 23 | 24 | /** 25 | * Vercel teams for the current user 26 | */ 27 | export const aiGatewayTeamsAtom = atom([]); 28 | export const aiGatewayTeamsLoadingAtom = atom(false); 29 | export const aiGatewayTeamsFetchedAtom = atom(false); 30 | -------------------------------------------------------------------------------- /plugins/superagent/test.ts: -------------------------------------------------------------------------------- 1 | export async function testSuperagent(credentials: Record) { 2 | try { 3 | const apiKey = credentials.SUPERAGENT_API_KEY; 4 | 5 | if (!apiKey) { 6 | return { 7 | success: false, 8 | error: "SUPERAGENT_API_KEY is required", 9 | }; 10 | } 11 | 12 | const response = await fetch("https://app.superagent.sh/api/guard", { 13 | method: "POST", 14 | headers: { 15 | "Content-Type": "application/json", 16 | Authorization: `Bearer ${apiKey}`, 17 | }, 18 | body: JSON.stringify({ 19 | text: "Hello, this is a test message.", 20 | }), 21 | }); 22 | 23 | if (response.ok) { 24 | return { success: true }; 25 | } 26 | const error = await response.text(); 27 | return { success: false, error }; 28 | } catch (error) { 29 | return { 30 | success: false, 31 | error: error instanceof Error ? error.message : String(error), 32 | }; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /plugins/legacy-mappings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Legacy Action Mappings 3 | * 4 | * This file maps old action type names to new namespaced action IDs. 5 | * Used for backward compatibility with existing workflows. 6 | * 7 | * Format: "Old Label" -> "plugin-type/action-slug" 8 | * 9 | * TODO: Remove this file once all workflows have been migrated to the new format. 10 | */ 11 | export const LEGACY_ACTION_MAPPINGS: Record = { 12 | // Firecrawl 13 | Scrape: "firecrawl/scrape", 14 | Search: "firecrawl/search", 15 | 16 | // AI Gateway 17 | "Generate Text": "ai-gateway/generate-text", 18 | "Generate Image": "ai-gateway/generate-image", 19 | 20 | // Resend 21 | "Send Email": "resend/send-email", 22 | 23 | // Linear 24 | "Create Ticket": "linear/create-ticket", 25 | "Find Issues": "linear/find-issues", 26 | 27 | // Slack 28 | "Send Slack Message": "slack/send-message", 29 | 30 | // v0 31 | "Create Chat": "v0/create-chat", 32 | "Send Message": "v0/send-message", 33 | }; 34 | 35 | -------------------------------------------------------------------------------- /plugins/_template/icon.tsx.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * ICON COMPONENT TEMPLATE 3 | * 4 | * Define your integration's icon here as an SVG component. 5 | * The icon should accept an optional className prop for styling. 6 | * 7 | * Tips: 8 | * - Use fill="currentColor" to inherit the text color 9 | * - Standard viewBox is "0 0 24 24" but adjust as needed for your SVG 10 | * - Include aria-label and title for accessibility 11 | * - Find the SVG for the [IntegrationName] logo at https://simpleicons.org/?q=[integration-name] 12 | */ 13 | 14 | export function [IntegrationName]Icon({ className }: { className?: string }) { 15 | return ( 16 | 23 | [Integration Name] 24 | {/* Replace with your SVG path(s) */} 25 | 26 | 27 | ); 28 | } 29 | -------------------------------------------------------------------------------- /public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/firecrawl/test.ts: -------------------------------------------------------------------------------- 1 | export async function testFirecrawl(credentials: Record) { 2 | try { 3 | const apiKey = credentials.FIRECRAWL_API_KEY; 4 | 5 | if (!apiKey) { 6 | return { 7 | success: false, 8 | error: "FIRECRAWL_API_KEY is required", 9 | }; 10 | } 11 | 12 | const response = await fetch("https://api.firecrawl.dev/v1/scrape", { 13 | method: "POST", 14 | headers: { 15 | "Content-Type": "application/json", 16 | Authorization: `Bearer ${apiKey}`, 17 | }, 18 | body: JSON.stringify({ 19 | url: "https://example.com", 20 | formats: ["markdown"], 21 | }), 22 | }); 23 | 24 | if (response.ok) { 25 | return { success: true }; 26 | } 27 | const error = await response.text(); 28 | return { success: false, error }; 29 | } catch (error) { 30 | return { 31 | success: false, 32 | error: error instanceof Error ? error.message : String(error), 33 | }; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2017", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "noEmit": true, 9 | "esModuleInterop": true, 10 | "module": "esnext", 11 | "moduleResolution": "bundler", 12 | "resolveJsonModule": true, 13 | "isolatedModules": true, 14 | "jsx": "react-jsx", 15 | "incremental": true, 16 | "plugins": [ 17 | { 18 | "name": "next" 19 | }, 20 | { 21 | "name": "workflow" 22 | } 23 | ], 24 | "paths": { 25 | "@/*": ["./*"] 26 | }, 27 | "strictNullChecks": true 28 | }, 29 | "include": [ 30 | "next-env.d.ts", 31 | "**/*.ts", 32 | "**/*.tsx", 33 | ".next/types/**/*.ts", 34 | ".next/dev/types/**/*.ts", 35 | "**/*.mts" 36 | ], 37 | "exclude": [ 38 | "node_modules", 39 | "lib/next-boilerplate/**/*.ts", 40 | "lib/next-boilerplate/**/*.tsx", 41 | "tmp/**/*" 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /lib/next-boilerplate/public/globe.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /drizzle/0001_dark_human_cannonball.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE "integrations" ( 2 | "id" text PRIMARY KEY NOT NULL, 3 | "user_id" text NOT NULL, 4 | "name" text NOT NULL, 5 | "type" text NOT NULL, 6 | "config" jsonb NOT NULL, 7 | "created_at" timestamp DEFAULT now() NOT NULL, 8 | "updated_at" timestamp DEFAULT now() NOT NULL 9 | ); 10 | --> statement-breakpoint 11 | ALTER TABLE "workflows" DROP CONSTRAINT "workflows_vercel_project_id_unique";--> statement-breakpoint 12 | ALTER TABLE "integrations" ADD CONSTRAINT "integrations_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint 13 | ALTER TABLE "workflows" DROP COLUMN "vercel_project_id";--> statement-breakpoint 14 | ALTER TABLE "workflows" DROP COLUMN "vercel_project_name";--> statement-breakpoint 15 | ALTER TABLE "workflows" DROP COLUMN "deployment_status";--> statement-breakpoint 16 | ALTER TABLE "workflows" DROP COLUMN "deployment_url";--> statement-breakpoint 17 | ALTER TABLE "workflows" DROP COLUMN "last_deployed_at"; -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "[javascript]": { 4 | "editor.defaultFormatter": "biomejs.biome" 5 | }, 6 | "[typescript]": { 7 | "editor.defaultFormatter": "biomejs.biome" 8 | }, 9 | "[javascriptreact]": { 10 | "editor.defaultFormatter": "biomejs.biome" 11 | }, 12 | "[typescriptreact]": { 13 | "editor.defaultFormatter": "biomejs.biome" 14 | }, 15 | "[json]": { 16 | "editor.defaultFormatter": "biomejs.biome" 17 | }, 18 | "[jsonc]": { 19 | "editor.defaultFormatter": "biomejs.biome" 20 | }, 21 | "[css]": { 22 | "editor.defaultFormatter": "biomejs.biome" 23 | }, 24 | "[graphql]": { 25 | "editor.defaultFormatter": "biomejs.biome" 26 | }, 27 | "typescript.tsdk": "node_modules/typescript/lib", 28 | "editor.formatOnSave": true, 29 | "editor.formatOnPaste": true, 30 | "emmet.showExpandedAbbreviation": "never", 31 | "editor.codeActionsOnSave": { 32 | "source.fixAll.biome": "explicit", 33 | "source.organizeImports.biome": "explicit" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /lib/codegen-templates/database-query.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Code template for Database Query action step 3 | * This is a string template used for code generation - keep as string export 4 | * 5 | * Requires: pnpm add postgres 6 | * Environment: DATABASE_URL 7 | */ 8 | export default `export async function databaseQueryStep(input: { 9 | query: string; 10 | }) { 11 | "use step"; 12 | 13 | const databaseUrl = process.env.DATABASE_URL; 14 | if (!databaseUrl) { 15 | return { success: false, error: "DATABASE_URL environment variable is not set" }; 16 | } 17 | 18 | const postgres = await import("postgres"); 19 | const sql = postgres.default(databaseUrl, { max: 1 }); 20 | 21 | try { 22 | const result = await sql.unsafe(input.query); 23 | await sql.end(); 24 | return { success: true, rows: result, count: result.length }; 25 | } catch (error) { 26 | await sql.end(); 27 | const message = error instanceof Error ? error.message : String(error); 28 | return { success: false, error: \`Database query failed: \${message}\` }; 29 | } 30 | }`; 31 | -------------------------------------------------------------------------------- /plugins/github/icon.tsx: -------------------------------------------------------------------------------- 1 | export function GitHubIcon({ className }: { className?: string }) { 2 | return ( 3 | 10 | GitHub 11 | 12 | 13 | ); 14 | } 15 | 16 | -------------------------------------------------------------------------------- /lib/ai-gateway/config.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * AI Gateway Managed Keys Configuration 3 | * 4 | * This feature allows signed-in users to use their own Vercel AI Gateway 5 | * API keys (and credits) instead of manually entering an API key. 6 | * 7 | * The AI Gateway itself is available to everyone via AI_GATEWAY_API_KEY. 8 | * This feature flag only controls the ability to create API keys on behalf 9 | * of users through OAuth - which is an internal Vercel feature. 10 | * 11 | * Set AI_GATEWAY_MANAGED_KEYS_ENABLED=true to enable. 12 | */ 13 | 14 | export function isAiGatewayManagedKeysEnabled(): boolean { 15 | return process.env.AI_GATEWAY_MANAGED_KEYS_ENABLED === "true"; 16 | } 17 | 18 | /** 19 | * Check if managed keys feature is enabled on the client side 20 | * Uses NEXT_PUBLIC_ prefix for client-side access 21 | */ 22 | export function isAiGatewayManagedKeysEnabledClient(): boolean { 23 | if (typeof window === "undefined") { 24 | return process.env.AI_GATEWAY_MANAGED_KEYS_ENABLED === "true"; 25 | } 26 | return process.env.NEXT_PUBLIC_AI_GATEWAY_MANAGED_KEYS_ENABLED === "true"; 27 | } 28 | -------------------------------------------------------------------------------- /components/github-stars-button.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { GitHubIcon } from "@/components/icons/github-icon"; 4 | import { Button } from "@/components/ui/button"; 5 | import { formatAbbreviatedNumber } from "@/lib/utils/format-number"; 6 | import { useGitHubStars } from "./github-stars-provider"; 7 | 8 | const GITHUB_REPO_URL = 9 | "https://github.com/vercel-labs/workflow-builder-template"; 10 | 11 | export function GitHubStarsButton() { 12 | const stars = useGitHubStars(); 13 | 14 | return ( 15 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /lib/steps/condition.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Executable step function for Condition action 3 | */ 4 | import "server-only"; 5 | 6 | import { type StepInput, withStepLogging } from "./step-handler"; 7 | 8 | export type ConditionInput = StepInput & { 9 | condition: boolean; 10 | /** Original condition expression string for logging (e.g., "{{@nodeId:Label.field}} === 'good'") */ 11 | expression?: string; 12 | /** Resolved values of template variables for logging (e.g., { "Label.field": "actual_value" }) */ 13 | values?: Record; 14 | }; 15 | 16 | type ConditionResult = { 17 | condition: boolean; 18 | }; 19 | 20 | function evaluateCondition(input: ConditionInput): ConditionResult { 21 | return { condition: input.condition }; 22 | } 23 | 24 | // biome-ignore lint/suspicious/useAwait: workflow "use step" requires async 25 | export async function conditionStep( 26 | input: ConditionInput 27 | ): Promise { 28 | "use step"; 29 | return withStepLogging(input, () => 30 | Promise.resolve(evaluateCondition(input)) 31 | ); 32 | } 33 | conditionStep.maxRetries = 0; 34 | -------------------------------------------------------------------------------- /plugins/linear/icon.tsx: -------------------------------------------------------------------------------- 1 | export function LinearIcon({ className }: { className?: string }) { 2 | return ( 3 | 10 | Linear 11 | 15 | 16 | ); 17 | } 18 | 19 | -------------------------------------------------------------------------------- /components/workflow/config/condition-config.tsx: -------------------------------------------------------------------------------- 1 | import { Label } from "@/components/ui/label"; 2 | import { TemplateBadgeInput } from "@/components/ui/template-badge-input"; 3 | 4 | type ConditionConfigProps = { 5 | config: Record; 6 | onUpdateConfig: (key: string, value: string) => void; 7 | disabled: boolean; 8 | }; 9 | 10 | export function ConditionConfig({ 11 | config, 12 | onUpdateConfig, 13 | disabled, 14 | }: ConditionConfigProps) { 15 | return ( 16 |
17 | 18 | onUpdateConfig("condition", value)} 22 | placeholder="e.g., 5 > 3, status === 200, {{PreviousNode.value}} > 100" 23 | value={(config?.condition as string) || ""} 24 | /> 25 |

26 | Enter a JavaScript expression that evaluates to true or false. You can 27 | use @ to reference previous node outputs. 28 |

29 |
30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /components/github-stars-loader.tsx: -------------------------------------------------------------------------------- 1 | import type { ReactNode } from "react"; 2 | import { GitHubStarsProvider } from "@/components/github-stars-provider"; 3 | 4 | const GITHUB_REPO = "vercel-labs/workflow-builder-template"; 5 | 6 | async function getGitHubStars(): Promise { 7 | try { 8 | const response = await fetch( 9 | `https://api.github.com/repos/${GITHUB_REPO}`, 10 | { 11 | headers: { 12 | Accept: "application/vnd.github.v3+json", 13 | ...(process.env.GITHUB_TOKEN && { 14 | Authorization: `Bearer ${process.env.GITHUB_TOKEN}`, 15 | }), 16 | }, 17 | next: { revalidate: 3600 }, // Cache for 1 hour 18 | } 19 | ); 20 | 21 | if (!response.ok) { 22 | return null; 23 | } 24 | 25 | const data = await response.json(); 26 | return data.stargazers_count; 27 | } catch { 28 | return null; 29 | } 30 | } 31 | 32 | export async function GitHubStarsLoader({ children }: { children: ReactNode }) { 33 | const stars = await getGitHubStars(); 34 | return {children}; 35 | } 36 | -------------------------------------------------------------------------------- /plugins/perplexity/test.ts: -------------------------------------------------------------------------------- 1 | export async function testPerplexity(credentials: Record) { 2 | try { 3 | const apiKey = credentials.PERPLEXITY_API_KEY; 4 | 5 | if (!apiKey) { 6 | return { 7 | success: false, 8 | error: "PERPLEXITY_API_KEY is required", 9 | }; 10 | } 11 | 12 | // Make a lightweight API call to verify the key works 13 | const response = await fetch("https://api.perplexity.ai/chat/completions", { 14 | method: "POST", 15 | headers: { 16 | "Content-Type": "application/json", 17 | Authorization: `Bearer ${apiKey}`, 18 | }, 19 | body: JSON.stringify({ 20 | model: "sonar", 21 | messages: [{ role: "user", content: "ping" }], 22 | max_tokens: 1, 23 | }), 24 | }); 25 | 26 | if (response.ok) { 27 | return { success: true }; 28 | } 29 | 30 | const error = await response.text(); 31 | return { success: false, error: error || "Invalid API key" }; 32 | } catch (error) { 33 | return { 34 | success: false, 35 | error: error instanceof Error ? error.message : String(error), 36 | }; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { 4 | CircleCheckIcon, 5 | InfoIcon, 6 | Loader2Icon, 7 | OctagonXIcon, 8 | TriangleAlertIcon, 9 | } from "lucide-react" 10 | import { useTheme } from "next-themes" 11 | import { Toaster as Sonner, type ToasterProps } from "sonner" 12 | 13 | const Toaster = ({ ...props }: ToasterProps) => { 14 | const { theme = "system" } = useTheme() 15 | 16 | return ( 17 | , 22 | info: , 23 | warning: , 24 | error: , 25 | loading: , 26 | }} 27 | style={ 28 | { 29 | "--normal-bg": "var(--popover)", 30 | "--normal-text": "var(--popover-foreground)", 31 | "--normal-border": "var(--border)", 32 | "--border-radius": "var(--radius)", 33 | } as React.CSSProperties 34 | } 35 | {...props} 36 | /> 37 | ) 38 | } 39 | 40 | export { Toaster } 41 | -------------------------------------------------------------------------------- /app/workflows/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useRouter } from "next/navigation"; 4 | import { useEffect } from "react"; 5 | import { api } from "@/lib/api-client"; 6 | 7 | export default function WorkflowsPage() { 8 | const router = useRouter(); 9 | 10 | useEffect(() => { 11 | const redirectToWorkflow = async () => { 12 | try { 13 | const workflows = await api.workflow.getAll(); 14 | // Filter out the auto-save workflow 15 | const filtered = workflows.filter((w) => w.name !== "__current__"); 16 | 17 | if (filtered.length > 0) { 18 | // Sort by updatedAt descending to get most recent 19 | const mostRecent = filtered.sort( 20 | (a, b) => 21 | new Date(b.updatedAt).getTime() - new Date(a.updatedAt).getTime() 22 | )[0]; 23 | router.replace(`/workflows/${mostRecent.id}`); 24 | } else { 25 | // No workflows, redirect to homepage 26 | router.replace("/"); 27 | } 28 | } catch (error) { 29 | console.error("Failed to load workflows:", error); 30 | router.replace("/"); 31 | } 32 | }; 33 | 34 | redirectToWorkflow(); 35 | }, [router]); 36 | 37 | return null; 38 | } 39 | -------------------------------------------------------------------------------- /plugins/webflow/test.ts: -------------------------------------------------------------------------------- 1 | const WEBFLOW_API_URL = "https://api.webflow.com/v2"; 2 | 3 | export async function testWebflow(credentials: Record) { 4 | try { 5 | const apiKey = credentials.WEBFLOW_API_KEY; 6 | 7 | if (!apiKey) { 8 | return { 9 | success: false, 10 | error: "WEBFLOW_API_KEY is required", 11 | }; 12 | } 13 | 14 | // Use the list sites endpoint to validate the API key 15 | const response = await fetch(`${WEBFLOW_API_URL}/sites`, { 16 | method: "GET", 17 | headers: { 18 | Accept: "application/json", 19 | Authorization: `Bearer ${apiKey}`, 20 | }, 21 | }); 22 | 23 | if (!response.ok) { 24 | if (response.status === 401) { 25 | return { 26 | success: false, 27 | error: "Invalid API key. Please check your Webflow API token.", 28 | }; 29 | } 30 | return { 31 | success: false, 32 | error: `API validation failed: HTTP ${response.status}`, 33 | }; 34 | } 35 | 36 | return { success: true }; 37 | } catch (error) { 38 | return { 39 | success: false, 40 | error: error instanceof Error ? error.message : String(error), 41 | }; 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /app/api/workflows/route.ts: -------------------------------------------------------------------------------- 1 | import { desc, eq } from "drizzle-orm"; 2 | import { NextResponse } from "next/server"; 3 | import { auth } from "@/lib/auth"; 4 | import { db } from "@/lib/db"; 5 | import { workflows } from "@/lib/db/schema"; 6 | 7 | export async function GET(request: Request) { 8 | try { 9 | const session = await auth.api.getSession({ 10 | headers: request.headers, 11 | }); 12 | 13 | if (!session?.user) { 14 | return NextResponse.json([], { status: 200 }); 15 | } 16 | 17 | const userWorkflows = await db 18 | .select() 19 | .from(workflows) 20 | .where(eq(workflows.userId, session.user.id)) 21 | .orderBy(desc(workflows.updatedAt)); 22 | 23 | const mappedWorkflows = userWorkflows.map((workflow) => ({ 24 | ...workflow, 25 | createdAt: workflow.createdAt.toISOString(), 26 | updatedAt: workflow.updatedAt.toISOString(), 27 | })); 28 | 29 | return NextResponse.json(mappedWorkflows); 30 | } catch (error) { 31 | console.error("Failed to get workflows:", error); 32 | return NextResponse.json( 33 | { 34 | error: 35 | error instanceof Error ? error.message : "Failed to get workflows", 36 | }, 37 | { status: 500 } 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /components/overlays/overlay-sync.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useAtom } from "jotai"; 4 | import { useEffect, useRef } from "react"; 5 | import { overlayStackAtom } from "@/lib/atoms/overlay"; 6 | import { useOverlay } from "./overlay-provider"; 7 | 8 | /** 9 | * Syncs the overlay context state with Jotai atoms. 10 | * This enables using either the context API or Jotai atoms interchangeably. 11 | * 12 | * Place this inside both OverlayProvider and Jotai Provider. 13 | */ 14 | export function OverlaySync() { 15 | const { stack } = useOverlay(); 16 | const [atomStack, setAtomStack] = useAtom(overlayStackAtom); 17 | const isUpdatingFromAtom = useRef(false); 18 | const isUpdatingFromContext = useRef(false); 19 | 20 | // Sync context -> atom 21 | useEffect(() => { 22 | if (isUpdatingFromAtom.current) { 23 | isUpdatingFromAtom.current = false; 24 | return; 25 | } 26 | isUpdatingFromContext.current = true; 27 | setAtomStack(stack); 28 | }, [stack, setAtomStack]); 29 | 30 | // Note: Full two-way sync would require the provider to accept external state. 31 | // For now, the atoms are read-only views of the context state. 32 | // To use atoms for mutations, we'd need to refactor the provider. 33 | 34 | return null; 35 | } 36 | -------------------------------------------------------------------------------- /plugins/resend/test.ts: -------------------------------------------------------------------------------- 1 | const RESEND_API_URL = "https://api.resend.com"; 2 | 3 | export async function testResend(credentials: Record) { 4 | try { 5 | const apiKey = credentials.RESEND_API_KEY; 6 | 7 | if (!apiKey || !apiKey.startsWith("re_")) { 8 | return { 9 | success: false, 10 | error: "Invalid API key format. Resend API keys start with 're_'", 11 | }; 12 | } 13 | 14 | // Validate API key by fetching domains (lightweight read-only endpoint) 15 | const response = await fetch(`${RESEND_API_URL}/domains`, { 16 | method: "GET", 17 | headers: { 18 | Authorization: `Bearer ${apiKey}`, 19 | }, 20 | }); 21 | 22 | if (!response.ok) { 23 | if (response.status === 401) { 24 | return { 25 | success: false, 26 | error: "Invalid API key. Please check your Resend API key.", 27 | }; 28 | } 29 | return { 30 | success: false, 31 | error: `API validation failed: HTTP ${response.status}`, 32 | }; 33 | } 34 | 35 | return { success: true }; 36 | } catch (error) { 37 | return { 38 | success: false, 39 | error: error instanceof Error ? error.message : String(error), 40 | }; 41 | } 42 | } 43 | 44 | -------------------------------------------------------------------------------- /plugins/slack/test.ts: -------------------------------------------------------------------------------- 1 | const SLACK_API_URL = "https://slack.com/api"; 2 | 3 | type SlackAuthTestResponse = { 4 | ok: boolean; 5 | error?: string; 6 | }; 7 | 8 | export async function testSlack(credentials: Record) { 9 | try { 10 | const apiKey = credentials.SLACK_API_KEY; 11 | 12 | if (!apiKey) { 13 | return { 14 | success: false, 15 | error: "SLACK_API_KEY is required", 16 | }; 17 | } 18 | 19 | const response = await fetch(`${SLACK_API_URL}/auth.test`, { 20 | method: "POST", 21 | headers: { 22 | Authorization: `Bearer ${apiKey}`, 23 | }, 24 | }); 25 | 26 | if (!response.ok) { 27 | return { 28 | success: false, 29 | error: `API validation failed: HTTP ${response.status}`, 30 | }; 31 | } 32 | 33 | const result = (await response.json()) as SlackAuthTestResponse; 34 | 35 | if (!result.ok) { 36 | return { 37 | success: false, 38 | error: result.error || "Invalid Slack Bot Token", 39 | }; 40 | } 41 | 42 | return { success: true }; 43 | } catch (error) { 44 | return { 45 | success: false, 46 | error: error instanceof Error ? error.message : String(error), 47 | }; 48 | } 49 | } 50 | 51 | -------------------------------------------------------------------------------- /plugins/fal/test.ts: -------------------------------------------------------------------------------- 1 | export async function testFal(credentials: Record) { 2 | try { 3 | const apiKey = credentials.FAL_API_KEY; 4 | 5 | if (!apiKey) { 6 | return { 7 | success: false, 8 | error: "FAL_API_KEY is required", 9 | }; 10 | } 11 | 12 | // Test with a simple API call to check credentials 13 | const response = await fetch("https://queue.fal.run/fal-ai/flux/schnell", { 14 | method: "POST", 15 | headers: { 16 | "Content-Type": "application/json", 17 | Authorization: `Key ${apiKey}`, 18 | }, 19 | body: JSON.stringify({ 20 | prompt: "test", 21 | num_images: 1, 22 | image_size: "square", 23 | }), 24 | }); 25 | 26 | if (response.ok) { 27 | return { success: true }; 28 | } 29 | 30 | // Check for auth errors specifically 31 | if (response.status === 401 || response.status === 403) { 32 | return { 33 | success: false, 34 | error: "Invalid API key", 35 | }; 36 | } 37 | 38 | const error = await response.text(); 39 | return { success: false, error }; 40 | } catch (error) { 41 | return { 42 | success: false, 43 | error: error instanceof Error ? error.message : String(error), 44 | }; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import { Avatar as AvatarPrimitive } from "radix-ui" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ) 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ) 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback } 54 | -------------------------------------------------------------------------------- /app/api/api-keys/[keyId]/route.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { NextResponse } from "next/server"; 3 | import { auth } from "@/lib/auth"; 4 | import { db } from "@/lib/db"; 5 | import { apiKeys } from "@/lib/db/schema"; 6 | 7 | // DELETE - Delete an API key 8 | export async function DELETE( 9 | request: Request, 10 | context: { params: Promise<{ keyId: string }> } 11 | ) { 12 | try { 13 | const { keyId } = await context.params; 14 | const session = await auth.api.getSession({ 15 | headers: request.headers, 16 | }); 17 | 18 | if (!session?.user) { 19 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 20 | } 21 | 22 | // Delete the key (only if it belongs to the user) 23 | const result = await db 24 | .delete(apiKeys) 25 | .where(and(eq(apiKeys.id, keyId), eq(apiKeys.userId, session.user.id))) 26 | .returning({ id: apiKeys.id }); 27 | 28 | if (result.length === 0) { 29 | return NextResponse.json({ error: "API key not found" }, { status: 404 }); 30 | } 31 | 32 | return NextResponse.json({ success: true }); 33 | } catch (error) { 34 | console.error("Failed to delete API key:", error); 35 | return NextResponse.json( 36 | { error: "Failed to delete API key" }, 37 | { status: 500 } 38 | ); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /.github/workflows/pr-checks.yml: -------------------------------------------------------------------------------- 1 | name: PR Checks 2 | 3 | on: 4 | pull_request: 5 | branches: ['*'] 6 | 7 | jobs: 8 | checks: 9 | runs-on: ubuntu-latest 10 | 11 | steps: 12 | - name: Checkout code 13 | uses: actions/checkout@v4 14 | 15 | - name: Setup Node.js 16 | uses: actions/setup-node@v4 17 | with: 18 | node-version: '22' 19 | 20 | - name: Setup pnpm 21 | uses: pnpm/action-setup@v4 22 | with: 23 | version: 9 24 | 25 | - name: Get pnpm store directory 26 | shell: bash 27 | run: | 28 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 29 | 30 | - name: Setup pnpm cache 31 | uses: actions/cache@v4 32 | with: 33 | path: ${{ env.STORE_PATH }} 34 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 35 | restore-keys: | 36 | ${{ runner.os }}-pnpm-store- 37 | 38 | - name: Install dependencies 39 | run: pnpm install --frozen-lockfile 40 | 41 | - name: Generate plugin registry 42 | run: pnpm discover-plugins 43 | 44 | - name: Run check 45 | run: pnpm check 46 | 47 | - name: Run type check 48 | run: pnpm type-check 49 | 50 | - name: Run build 51 | run: pnpm build 52 | 53 | -------------------------------------------------------------------------------- /plugins/clerk/icon.tsx: -------------------------------------------------------------------------------- 1 | export function ClerkIcon({ className }: { className?: string }) { 2 | return ( 3 | 9 | 10 | 15 | 19 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /lib/utils/time.ts: -------------------------------------------------------------------------------- 1 | function formatTimeDifference(value: number, unit: string): string { 2 | return `${value} ${unit}${value === 1 ? "" : "s"} ago`; 3 | } 4 | 5 | export function getRelativeTime(date: string | Date): string { 6 | const now = new Date(); 7 | const past = new Date(date); 8 | const diffInSeconds = Math.floor((now.getTime() - past.getTime()) / 1000); 9 | 10 | if (diffInSeconds < 60) { 11 | return "just now"; 12 | } 13 | 14 | const diffInMinutes = Math.floor(diffInSeconds / 60); 15 | if (diffInMinutes < 60) { 16 | return formatTimeDifference(diffInMinutes, "min"); 17 | } 18 | 19 | const diffInHours = Math.floor(diffInMinutes / 60); 20 | if (diffInHours < 24) { 21 | return formatTimeDifference(diffInHours, "hour"); 22 | } 23 | 24 | const diffInDays = Math.floor(diffInHours / 24); 25 | if (diffInDays < 7) { 26 | return formatTimeDifference(diffInDays, "day"); 27 | } 28 | 29 | const diffInWeeks = Math.floor(diffInDays / 7); 30 | if (diffInWeeks < 4) { 31 | return formatTimeDifference(diffInWeeks, "week"); 32 | } 33 | 34 | const diffInMonths = Math.floor(diffInDays / 30); 35 | if (diffInMonths < 12) { 36 | return formatTimeDifference(diffInMonths, "month"); 37 | } 38 | 39 | const diffInYears = Math.floor(diffInDays / 365); 40 | return formatTimeDifference(diffInYears, "year"); 41 | } 42 | -------------------------------------------------------------------------------- /components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { CheckIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Checkbox({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export { Checkbox } 33 | -------------------------------------------------------------------------------- /lib/next-boilerplate/public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /plugins/clerk/types.ts: -------------------------------------------------------------------------------- 1 | export type ClerkApiUser = { 2 | id: string; 3 | first_name: string | null; 4 | last_name: string | null; 5 | email_addresses: Array<{ 6 | id: string; 7 | email_address: string; 8 | }>; 9 | primary_email_address_id: string | null; 10 | public_metadata: Record; 11 | private_metadata: Record; 12 | created_at: number; 13 | updated_at: number; 14 | }; 15 | 16 | /** 17 | * Flat user data for workflow steps 18 | */ 19 | export type ClerkUserData = { 20 | id: string; 21 | firstName: string | null; 22 | lastName: string | null; 23 | primaryEmailAddress: string | null; 24 | createdAt: number; 25 | updatedAt: number; 26 | }; 27 | 28 | /** 29 | * Standard step output format 30 | */ 31 | export type ClerkUserResult = 32 | | { success: true; data: ClerkUserData } 33 | | { success: false; error: { message: string } }; 34 | 35 | export function toClerkUserData(apiUser: ClerkApiUser): ClerkUserData { 36 | const primaryEmail = apiUser.email_addresses.find( 37 | (e) => e.id === apiUser.primary_email_address_id 38 | ); 39 | return { 40 | id: apiUser.id, 41 | firstName: apiUser.first_name, 42 | lastName: apiUser.last_name, 43 | primaryEmailAddress: primaryEmail?.email_address ?? null, 44 | createdAt: apiUser.created_at, 45 | updatedAt: apiUser.updated_at, 46 | }; 47 | } 48 | -------------------------------------------------------------------------------- /lib/codegen-templates/http-request.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Code template for HTTP Request action step 3 | * This is a string template used for code generation - keep as string export 4 | */ 5 | export default `export async function httpRequestStep(input: { 6 | endpoint: string; 7 | httpMethod: string; 8 | httpHeaders?: string; 9 | httpBody?: string; 10 | }) { 11 | "use step"; 12 | 13 | let headers = {}; 14 | if (input.httpHeaders) { 15 | try { 16 | headers = JSON.parse(input.httpHeaders); 17 | } catch { 18 | // If parsing fails, use empty headers 19 | } 20 | } 21 | 22 | let body: string | undefined; 23 | if (input.httpMethod !== "GET" && input.httpBody) { 24 | try { 25 | const parsedBody = JSON.parse(input.httpBody); 26 | if (Object.keys(parsedBody).length > 0) { 27 | body = JSON.stringify(parsedBody); 28 | } 29 | } catch { 30 | if (input.httpBody.trim() && input.httpBody.trim() !== "{}") { 31 | body = input.httpBody; 32 | } 33 | } 34 | } 35 | 36 | const response = await fetch(input.endpoint, { 37 | method: input.httpMethod, 38 | headers, 39 | body, 40 | }); 41 | 42 | const contentType = response.headers.get("content-type"); 43 | if (contentType?.includes("application/json")) { 44 | return await response.json(); 45 | } 46 | return await response.text(); 47 | }`; 48 | -------------------------------------------------------------------------------- /plugins/stripe/test.ts: -------------------------------------------------------------------------------- 1 | const STRIPE_API_URL = "https://api.stripe.com/v1"; 2 | 3 | export async function testStripe(credentials: Record) { 4 | try { 5 | const apiKey = credentials.STRIPE_SECRET_KEY; 6 | 7 | if (!apiKey) { 8 | return { 9 | success: false, 10 | error: "STRIPE_SECRET_KEY is required", 11 | }; 12 | } 13 | 14 | if (!apiKey.startsWith("sk_")) { 15 | return { 16 | success: false, 17 | error: 18 | "Invalid API key format. Stripe secret keys start with 'sk_live_' or 'sk_test_'", 19 | }; 20 | } 21 | 22 | // Validate API key by fetching balance (lightweight read-only endpoint) 23 | const response = await fetch(`${STRIPE_API_URL}/balance`, { 24 | method: "GET", 25 | headers: { 26 | Authorization: `Bearer ${apiKey}`, 27 | }, 28 | }); 29 | 30 | if (!response.ok) { 31 | if (response.status === 401) { 32 | return { 33 | success: false, 34 | error: "Invalid API key. Please check your Stripe secret key.", 35 | }; 36 | } 37 | return { 38 | success: false, 39 | error: `API validation failed: HTTP ${response.status}`, 40 | }; 41 | } 42 | 43 | return { success: true }; 44 | } catch (error) { 45 | return { 46 | success: false, 47 | error: error instanceof Error ? error.message : String(error), 48 | }; 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /plugins/blob/test.ts: -------------------------------------------------------------------------------- 1 | const BLOB_API_URL = "https://blob.vercel-storage.com"; 2 | 3 | export async function testBlob(credentials: Record) { 4 | try { 5 | const token = credentials.BLOB_READ_WRITE_TOKEN; 6 | 7 | if (!token) { 8 | return { 9 | success: false, 10 | error: "BLOB_READ_WRITE_TOKEN is required", 11 | }; 12 | } 13 | 14 | if (!token.startsWith("vercel_blob_rw_")) { 15 | return { 16 | success: false, 17 | error: 18 | "Invalid token format. Vercel Blob tokens start with 'vercel_blob_rw_'", 19 | }; 20 | } 21 | 22 | // Test the token by listing blobs (lightweight read operation) 23 | const response = await fetch(BLOB_API_URL, { 24 | method: "GET", 25 | headers: { 26 | Authorization: `Bearer ${token}`, 27 | }, 28 | }); 29 | 30 | if (!response.ok) { 31 | if (response.status === 401 || response.status === 403) { 32 | return { 33 | success: false, 34 | error: "Invalid token. Please check your Vercel Blob token.", 35 | }; 36 | } 37 | return { 38 | success: false, 39 | error: `API validation failed: HTTP ${response.status}`, 40 | }; 41 | } 42 | 43 | return { success: true }; 44 | } catch (error) { 45 | return { 46 | success: false, 47 | error: error instanceof Error ? error.message : String(error), 48 | }; 49 | } 50 | } 51 | 52 | -------------------------------------------------------------------------------- /plugins/clerk/test.ts: -------------------------------------------------------------------------------- 1 | export async function testClerk(credentials: Record) { 2 | try { 3 | const secretKey = credentials.CLERK_SECRET_KEY; 4 | 5 | if (!secretKey) { 6 | return { 7 | success: false, 8 | error: "Secret key is required", 9 | }; 10 | } 11 | 12 | // Validate format - Clerk secret keys start with sk_live_ or sk_test_ 13 | if ( 14 | !secretKey.startsWith("sk_live_") && 15 | !secretKey.startsWith("sk_test_") 16 | ) { 17 | return { 18 | success: false, 19 | error: 20 | "Invalid secret key format. Clerk secret keys start with 'sk_live_' or 'sk_test_'", 21 | }; 22 | } 23 | 24 | // Test the connection by fetching users list (limit 1) 25 | const response = await fetch("https://api.clerk.com/v1/users?limit=1", { 26 | headers: { 27 | Authorization: `Bearer ${secretKey}`, 28 | "Content-Type": "application/json", 29 | "User-Agent": "workflow-builder.dev", 30 | }, 31 | }); 32 | 33 | if (!response.ok) { 34 | const error = await response.json().catch(() => ({})); 35 | return { 36 | success: false, 37 | error: error.errors?.[0]?.message || `API error: ${response.status}`, 38 | }; 39 | } 40 | 41 | return { success: true }; 42 | } catch (error) { 43 | return { 44 | success: false, 45 | error: error instanceof Error ? error.message : String(error), 46 | }; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /components/settings/account-settings.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent } from "@/components/ui/card"; 2 | import { Input } from "@/components/ui/input"; 3 | import { Label } from "@/components/ui/label"; 4 | 5 | type AccountSettingsProps = { 6 | accountName: string; 7 | accountEmail: string; 8 | onNameChange: (name: string) => void; 9 | onEmailChange: (email: string) => void; 10 | }; 11 | 12 | export function AccountSettings({ 13 | accountName, 14 | accountEmail, 15 | onNameChange, 16 | onEmailChange, 17 | }: AccountSettingsProps) { 18 | return ( 19 | 20 | 21 |
22 | 25 | onNameChange(e.target.value)} 28 | placeholder="Your name" 29 | value={accountName} 30 | /> 31 |
32 | 33 |
34 | 37 | onEmailChange(e.target.value)} 40 | placeholder="your.email@example.com" 41 | type="email" 42 | value={accountEmail} 43 | /> 44 |
45 |
46 |
47 | ); 48 | } 49 | -------------------------------------------------------------------------------- /lib/db/index.ts: -------------------------------------------------------------------------------- 1 | import type { PostgresJsDatabase } from "drizzle-orm/postgres-js"; 2 | import { drizzle } from "drizzle-orm/postgres-js"; 3 | import postgres from "postgres"; 4 | import { 5 | accounts, 6 | apiKeys, 7 | integrations, 8 | sessions, 9 | users, 10 | verifications, 11 | workflowExecutionLogs, 12 | workflowExecutions, 13 | workflowExecutionsRelations, 14 | workflows, 15 | } from "./schema"; 16 | 17 | // Construct schema object for drizzle 18 | const schema = { 19 | users, 20 | sessions, 21 | accounts, 22 | verifications, 23 | workflows, 24 | workflowExecutions, 25 | workflowExecutionLogs, 26 | workflowExecutionsRelations, 27 | apiKeys, 28 | integrations, 29 | }; 30 | 31 | const connectionString = 32 | process.env.DATABASE_URL || "postgres://localhost:5432/workflow"; 33 | 34 | // For migrations 35 | export const migrationClient = postgres(connectionString, { max: 1 }); 36 | 37 | // Use global singleton to prevent connection exhaustion during HMR 38 | const globalForDb = globalThis as unknown as { 39 | queryClient: ReturnType | undefined; 40 | db: PostgresJsDatabase | undefined; 41 | }; 42 | 43 | // For queries - reuse connection in development 44 | const queryClient = 45 | globalForDb.queryClient ?? postgres(connectionString, { max: 10 }); 46 | export const db = globalForDb.db ?? drizzle(queryClient, { schema }); 47 | 48 | if (process.env.NODE_ENV !== "production") { 49 | globalForDb.queryClient = queryClient; 50 | globalForDb.db = db; 51 | } 52 | -------------------------------------------------------------------------------- /plugins/github/test.ts: -------------------------------------------------------------------------------- 1 | const GITHUB_API_URL = "https://api.github.com"; 2 | 3 | type GitHubUser = { 4 | login: string; 5 | id: number; 6 | name?: string; 7 | }; 8 | 9 | export async function testGitHub(credentials: Record) { 10 | try { 11 | const token = credentials.GITHUB_TOKEN; 12 | 13 | if (!token) { 14 | return { 15 | success: false, 16 | error: "GITHUB_TOKEN is required", 17 | }; 18 | } 19 | 20 | const response = await fetch(`${GITHUB_API_URL}/user`, { 21 | method: "GET", 22 | headers: { 23 | Accept: "application/vnd.github+json", 24 | Authorization: `Bearer ${token}`, 25 | "X-GitHub-Api-Version": "2022-11-28", 26 | }, 27 | }); 28 | 29 | if (!response.ok) { 30 | if (response.status === 401) { 31 | return { 32 | success: false, 33 | error: "Invalid token. Please check your GitHub personal access token.", 34 | }; 35 | } 36 | return { 37 | success: false, 38 | error: `API validation failed: HTTP ${response.status}`, 39 | }; 40 | } 41 | 42 | const user = (await response.json()) as GitHubUser; 43 | 44 | if (!user.login) { 45 | return { 46 | success: false, 47 | error: "Failed to verify GitHub connection", 48 | }; 49 | } 50 | 51 | return { success: true }; 52 | } catch (error) { 53 | return { 54 | success: false, 55 | error: error instanceof Error ? error.message : String(error), 56 | }; 57 | } 58 | } 59 | 60 | -------------------------------------------------------------------------------- /lib/next-boilerplate/README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. 37 | -------------------------------------------------------------------------------- /lib/steps/trigger.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Trigger step - handles trigger execution with proper logging 3 | * Also handles workflow completion when called with _workflowComplete 4 | */ 5 | import "server-only"; 6 | 7 | import { 8 | logWorkflowComplete, 9 | type StepInput, 10 | withStepLogging, 11 | } from "./step-handler"; 12 | 13 | type TriggerResult = { 14 | success: true; 15 | data: Record; 16 | }; 17 | 18 | export type TriggerInput = StepInput & { 19 | triggerData: Record; 20 | /** If set, this call is just to log workflow completion (no trigger execution) */ 21 | _workflowComplete?: { 22 | executionId: string; 23 | status: "success" | "error"; 24 | output?: unknown; 25 | error?: string; 26 | startTime: number; 27 | }; 28 | }; 29 | 30 | /** 31 | * Trigger logic - just passes through the trigger data 32 | */ 33 | function executeTrigger(input: TriggerInput): TriggerResult { 34 | return { 35 | success: true, 36 | data: input.triggerData, 37 | }; 38 | } 39 | 40 | /** 41 | * Trigger Step 42 | * Executes a trigger and logs it properly 43 | * Also handles workflow completion when called with _workflowComplete 44 | */ 45 | export async function triggerStep(input: TriggerInput): Promise { 46 | "use step"; 47 | 48 | // If this is a completion-only call, just log workflow completion 49 | if (input._workflowComplete) { 50 | await logWorkflowComplete(input._workflowComplete); 51 | return { success: true, data: {} }; 52 | } 53 | 54 | // Normal trigger execution with logging 55 | return withStepLogging(input, () => Promise.resolve(executeTrigger(input))); 56 | } 57 | triggerStep.maxRetries = 0; 58 | -------------------------------------------------------------------------------- /plugins/firecrawl/icon.tsx: -------------------------------------------------------------------------------- 1 | export function FirecrawlIcon({ className }: { className?: string }) { 2 | return ( 3 | 9 | Firecrawl 10 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /app/api/workflows/[workflowId]/code/route.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { NextResponse } from "next/server"; 3 | import { auth } from "@/lib/auth"; 4 | import { db } from "@/lib/db"; 5 | import { workflows } from "@/lib/db/schema"; 6 | import { generateWorkflowSDKCode } from "@/lib/workflow-codegen-sdk"; 7 | 8 | export async function GET( 9 | request: Request, 10 | context: { params: Promise<{ workflowId: string }> } 11 | ) { 12 | try { 13 | const { workflowId } = await context.params; 14 | const session = await auth.api.getSession({ 15 | headers: request.headers, 16 | }); 17 | 18 | if (!session?.user) { 19 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 20 | } 21 | 22 | const workflow = await db.query.workflows.findFirst({ 23 | where: and( 24 | eq(workflows.id, workflowId), 25 | eq(workflows.userId, session.user.id) 26 | ), 27 | }); 28 | 29 | if (!workflow) { 30 | return NextResponse.json( 31 | { error: "Workflow not found" }, 32 | { status: 404 } 33 | ); 34 | } 35 | 36 | // Generate code 37 | const code = generateWorkflowSDKCode( 38 | workflow.name, 39 | workflow.nodes, 40 | workflow.edges 41 | ); 42 | 43 | return NextResponse.json({ 44 | code, 45 | workflowName: workflow.name, 46 | }); 47 | } catch (error) { 48 | console.error("Failed to get workflow code:", error); 49 | return NextResponse.json( 50 | { 51 | error: 52 | error instanceof Error 53 | ? error.message 54 | : "Failed to get workflow code", 55 | }, 56 | { status: 500 } 57 | ); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /plugins/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Plugins Index (Auto-Generated) 3 | * 4 | * This file is automatically generated by scripts/discover-plugins.ts 5 | * DO NOT EDIT MANUALLY - your changes will be overwritten! 6 | * 7 | * To add a new integration: 8 | * 1. Create a new directory in plugins/ (e.g., plugins/my-integration/) 9 | * 2. Add your plugin files (index.tsx, steps/, codegen/, etc.) 10 | * 3. Run: pnpm discover-plugins (or it runs automatically on build) 11 | * 12 | * To remove an integration: 13 | * 1. Delete the plugin directory 14 | * 2. Run: pnpm discover-plugins (or it runs automatically on build) 15 | */ 16 | 17 | import "./ai-gateway"; 18 | import "./blob"; 19 | import "./clerk"; 20 | import "./fal"; 21 | import "./firecrawl"; 22 | import "./github"; 23 | import "./linear"; 24 | import "./perplexity"; 25 | import "./resend"; 26 | import "./slack"; 27 | import "./stripe"; 28 | import "./superagent"; 29 | import "./v0"; 30 | import "./webflow"; 31 | 32 | export type { 33 | ActionConfigField, 34 | ActionConfigFieldBase, 35 | ActionConfigFieldGroup, 36 | ActionWithFullId, 37 | IntegrationPlugin, 38 | PluginAction, 39 | } from "./registry"; 40 | 41 | // Export the registry utilities 42 | export { 43 | computeActionId, 44 | findActionById, 45 | flattenConfigFields, 46 | generateAIActionPrompts, 47 | getActionsByCategory, 48 | getAllActions, 49 | getAllDependencies, 50 | getAllEnvVars, 51 | getAllIntegrations, 52 | getCredentialMapping, 53 | getDependenciesForActions, 54 | getIntegration, 55 | getIntegrationLabels, 56 | getIntegrationTypes, 57 | getPluginEnvVars, 58 | getSortedIntegrationTypes, 59 | isFieldGroup, 60 | parseActionId, 61 | registerIntegration, 62 | } from "./registry"; 63 | -------------------------------------------------------------------------------- /components/ui/integration-icon.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Database, HelpCircle } from "lucide-react"; 4 | import type { IntegrationType } from "@/lib/types/integration"; 5 | import { cn } from "@/lib/utils"; 6 | import { getIntegration } from "@/plugins"; 7 | 8 | interface IntegrationIconProps { 9 | integration: string; 10 | className?: string; 11 | } 12 | 13 | // Inline SVG for Vercel icon (special case - no plugin) 14 | function VercelIcon({ className }: { className?: string }) { 15 | return ( 16 | 24 | 25 | 26 | ); 27 | } 28 | 29 | // Special icons for integrations without plugins (database, vercel) 30 | const SPECIAL_ICONS: Record< 31 | string, 32 | React.ComponentType<{ className?: string }> 33 | > = { 34 | database: Database, 35 | vercel: VercelIcon, 36 | }; 37 | 38 | export function IntegrationIcon({ 39 | integration, 40 | className = "h-3 w-3", 41 | }: IntegrationIconProps) { 42 | // Check for special icons first (integrations without plugins) 43 | const SpecialIcon = SPECIAL_ICONS[integration]; 44 | if (SpecialIcon) { 45 | return ; 46 | } 47 | 48 | // Look up plugin from registry 49 | const plugin = getIntegration(integration as IntegrationType); 50 | 51 | if (plugin?.icon) { 52 | const PluginIcon = plugin.icon; 53 | return ; 54 | } 55 | 56 | // Fallback for unknown integrations 57 | return ; 58 | } 59 | -------------------------------------------------------------------------------- /plugins/clerk/components/user-card.tsx: -------------------------------------------------------------------------------- 1 | import type { ResultComponentProps } from "@/plugins/registry"; 2 | 3 | // The logging layer unwraps standardized outputs, so we receive just the data 4 | type ClerkUserData = { 5 | id: string; 6 | firstName: string | null; 7 | lastName: string | null; 8 | primaryEmailAddress: string | null; 9 | createdAt: number; 10 | }; 11 | 12 | export function UserCard({ output }: ResultComponentProps) { 13 | const data = output as ClerkUserData; 14 | 15 | // Validate we have the expected data shape 16 | if (!data || typeof data !== "object" || !("id" in data)) { 17 | return null; 18 | } 19 | 20 | const initials = [data.firstName?.[0], data.lastName?.[0]] 21 | .filter(Boolean) 22 | .join("") 23 | .toUpperCase(); 24 | 25 | const fullName = [data.firstName, data.lastName].filter(Boolean).join(" "); 26 | const createdDate = data.createdAt 27 | ? new Date(data.createdAt).toLocaleDateString() 28 | : "Unknown"; 29 | 30 | return ( 31 |
32 |
33 | {initials || "?"} 34 |
35 |
36 |
37 | {fullName || "Unknown User"} 38 |
39 | {data.primaryEmailAddress && ( 40 |
41 | {data.primaryEmailAddress} 42 |
43 | )} 44 |
45 | Created {createdDate} 46 |
47 |
48 |
49 | ); 50 | } 51 | 52 | 53 | 54 | -------------------------------------------------------------------------------- /plugins/slack/icon.tsx: -------------------------------------------------------------------------------- 1 | export function SlackIcon({ className }: { className?: string }) { 2 | return ( 3 | 9 | Slack 10 | 11 | 15 | 19 | 23 | 27 | 28 | 29 | ); 30 | } 31 | 32 | -------------------------------------------------------------------------------- /components/ai-elements/shimmer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { motion } from "motion/react"; 5 | import { 6 | type CSSProperties, 7 | type ElementType, 8 | type JSX, 9 | memo, 10 | useMemo, 11 | } from "react"; 12 | 13 | export type TextShimmerProps = { 14 | children: string; 15 | as?: ElementType; 16 | className?: string; 17 | duration?: number; 18 | spread?: number; 19 | }; 20 | 21 | const ShimmerComponent = ({ 22 | children, 23 | as: Component = "p", 24 | className, 25 | duration = 2, 26 | spread = 2, 27 | }: TextShimmerProps) => { 28 | const MotionComponent = motion.create( 29 | Component as keyof JSX.IntrinsicElements 30 | ); 31 | 32 | const dynamicSpread = useMemo( 33 | () => (children?.length ?? 0) * spread, 34 | [children, spread] 35 | ); 36 | 37 | return ( 38 | 59 | {children} 60 | 61 | ); 62 | }; 63 | 64 | export const Shimmer = memo(ShimmerComponent); 65 | -------------------------------------------------------------------------------- /components/overlays/make-public-overlay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Globe } from "lucide-react"; 4 | import { Overlay } from "./overlay"; 5 | import { useOverlay } from "./overlay-provider"; 6 | import type { OverlayComponentProps } from "./types"; 7 | 8 | type MakePublicOverlayProps = OverlayComponentProps<{ 9 | onConfirm: () => void; 10 | }>; 11 | 12 | export function MakePublicOverlay({ 13 | overlayId, 14 | onConfirm, 15 | }: MakePublicOverlayProps) { 16 | const { closeAll } = useOverlay(); 17 | 18 | const handleConfirm = () => { 19 | closeAll(); 20 | onConfirm(); 21 | }; 22 | 23 | return ( 24 | 32 |
33 | 34 |

35 | Making this workflow public means anyone with the link can: 36 |

37 |
38 | 39 |
    40 |
  • View the workflow structure and steps
  • 41 |
  • See action types and configurations
  • 42 |
  • Duplicate the workflow to their own account
  • 43 |
44 | 45 |

46 | The following will remain private: 47 |

48 |
    49 |
  • Your integration credentials (API keys, tokens)
  • 50 |
  • Execution logs and run history
  • 51 |
52 |
53 | ); 54 | } 55 | -------------------------------------------------------------------------------- /app/api/ai-gateway/status/route.ts: -------------------------------------------------------------------------------- 1 | import { and, eq } from "drizzle-orm"; 2 | import { isAiGatewayManagedKeysEnabled } from "@/lib/ai-gateway/config"; 3 | import { auth } from "@/lib/auth"; 4 | import { db } from "@/lib/db"; 5 | import { accounts, integrations } from "@/lib/db/schema"; 6 | 7 | /** 8 | * GET /api/ai-gateway/status 9 | * Returns user's AI Gateway status including whether they can use managed keys 10 | */ 11 | export async function GET(request: Request) { 12 | const enabled = isAiGatewayManagedKeysEnabled(); 13 | 14 | // If feature is not enabled, return minimal response 15 | if (!enabled) { 16 | return Response.json({ 17 | enabled: false, 18 | signedIn: false, 19 | isVercelUser: false, 20 | hasManagedKey: false, 21 | }); 22 | } 23 | 24 | const session = await auth.api.getSession({ 25 | headers: request.headers, 26 | }); 27 | 28 | if (!session?.user?.id) { 29 | return Response.json({ 30 | enabled: true, 31 | signedIn: false, 32 | isVercelUser: false, 33 | hasManagedKey: false, 34 | }); 35 | } 36 | 37 | // Check if user signed in with Vercel 38 | const account = await db.query.accounts.findFirst({ 39 | where: eq(accounts.userId, session.user.id), 40 | }); 41 | 42 | const isVercelUser = account?.providerId === "vercel"; 43 | 44 | // Check if user has a managed AI Gateway integration 45 | const managedIntegration = await db.query.integrations.findFirst({ 46 | where: and( 47 | eq(integrations.userId, session.user.id), 48 | eq(integrations.type, "ai-gateway"), 49 | eq(integrations.isManaged, true) 50 | ), 51 | }); 52 | 53 | return Response.json({ 54 | enabled: true, 55 | signedIn: true, 56 | isVercelUser, 57 | hasManagedKey: !!managedIntegration, 58 | managedIntegrationId: managedIntegration?.id, 59 | }); 60 | } 61 | -------------------------------------------------------------------------------- /components/overlays/overlay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | import { OverlayFooter } from "./overlay-footer"; 5 | import { SmartOverlayHeader } from "./overlay-header"; 6 | import type { OverlayProps } from "./types"; 7 | 8 | type OverlayComponentProps = OverlayProps & { 9 | /** The overlay's unique ID (passed automatically by the container) */ 10 | overlayId: string; 11 | }; 12 | 13 | /** 14 | * Base Overlay component for creating new overlays. 15 | * Provides consistent structure with header, content area, and footer. 16 | * 17 | * @example 18 | * ```tsx 19 | * function SettingsOverlay({ overlayId }: { overlayId: string }) { 20 | * const { pop } = useOverlay(); 21 | * 22 | * return ( 23 | * 32 | * 33 | * 34 | * ); 35 | * } 36 | * ``` 37 | */ 38 | export function Overlay({ 39 | overlayId, 40 | title, 41 | description, 42 | actions, 43 | children, 44 | className, 45 | }: OverlayComponentProps) { 46 | return ( 47 |
48 | {/* Header with smart back button detection */} 49 | {(title || description) && ( 50 | 55 | )} 56 | 57 | {/* Content area */} 58 | {children &&
{children}
} 59 | 60 | {/* Footer with actions */} 61 | 62 |
63 | ); 64 | } 65 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-card text-card-foreground", 12 | destructive: 13 | "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | function Alert({ 23 | className, 24 | variant, 25 | ...props 26 | }: React.ComponentProps<"div"> & VariantProps) { 27 | return ( 28 |
34 | ) 35 | } 36 | 37 | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { 38 | return ( 39 |
47 | ) 48 | } 49 | 50 | function AlertDescription({ 51 | className, 52 | ...props 53 | }: React.ComponentProps<"div">) { 54 | return ( 55 |
63 | ) 64 | } 65 | 66 | export { Alert, AlertTitle, AlertDescription } 67 | -------------------------------------------------------------------------------- /plugins/_template/test.ts.txt: -------------------------------------------------------------------------------- 1 | /** 2 | * TEST FUNCTION TEMPLATE 3 | * 4 | * This function validates credentials when users click "Test Connection". 5 | * It should verify credentials are valid without performing destructive actions. 6 | * 7 | * The function is lazy-loaded via getTestFunction() in index.ts to avoid 8 | * bundling server-side code in the client. 9 | * 10 | * Approaches for testing: 11 | * 1. Format validation (e.g., check API key prefix) 12 | * 2. Lightweight API call (e.g., fetch user info, list resources) 13 | * 3. Dedicated test endpoint if the API provides one 14 | */ 15 | 16 | export async function test[IntegrationName](credentials: Record) { 17 | try { 18 | const apiKey = credentials.[INTEGRATION_NAME]_API_KEY; 19 | 20 | // Validate API key is provided 21 | if (!apiKey) { 22 | return { 23 | success: false, 24 | error: "[INTEGRATION_NAME]_API_KEY is required", 25 | }; 26 | } 27 | 28 | // Option 1: Format validation (if API keys have a known format) 29 | // if (!apiKey.startsWith("sk_")) { 30 | // return { 31 | // success: false, 32 | // error: "Invalid API key format. API keys should start with 'sk_'", 33 | // }; 34 | // } 35 | 36 | // Option 2: Make a lightweight API call to verify the key works 37 | const response = await fetch("https://api.[integration-name].com/v1/test", { 38 | method: "GET", 39 | headers: { 40 | "Content-Type": "application/json", 41 | Authorization: `Bearer ${apiKey}`, 42 | }, 43 | }); 44 | 45 | if (response.ok) { 46 | return { success: true }; 47 | } 48 | 49 | const error = await response.text(); 50 | return { success: false, error: error || "Invalid API key" }; 51 | } catch (error) { 52 | return { 53 | success: false, 54 | error: error instanceof Error ? error.message : String(error), 55 | }; 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /plugins/linear/test.ts: -------------------------------------------------------------------------------- 1 | const LINEAR_API_URL = "https://api.linear.app/graphql"; 2 | 3 | type LinearGraphQLResponse = { 4 | data?: T; 5 | errors?: Array<{ message: string }>; 6 | }; 7 | 8 | type ViewerQueryResponse = { 9 | viewer: { 10 | id: string; 11 | name: string; 12 | }; 13 | }; 14 | 15 | export async function testLinear(credentials: Record) { 16 | try { 17 | const apiKey = credentials.LINEAR_API_KEY; 18 | 19 | if (!apiKey) { 20 | return { 21 | success: false, 22 | error: "LINEAR_API_KEY is required", 23 | }; 24 | } 25 | 26 | // Validate API key by fetching viewer (lightweight query) 27 | const response = await fetch(LINEAR_API_URL, { 28 | method: "POST", 29 | headers: { 30 | "Content-Type": "application/json", 31 | Authorization: apiKey, 32 | }, 33 | body: JSON.stringify({ 34 | query: `query { viewer { id name } }`, 35 | }), 36 | }); 37 | 38 | if (!response.ok) { 39 | if (response.status === 401) { 40 | return { 41 | success: false, 42 | error: "Invalid API key. Please check your Linear API key.", 43 | }; 44 | } 45 | return { 46 | success: false, 47 | error: `API validation failed: HTTP ${response.status}`, 48 | }; 49 | } 50 | 51 | const result = (await response.json()) as LinearGraphQLResponse; 52 | 53 | if (result.errors?.length) { 54 | return { 55 | success: false, 56 | error: result.errors[0].message, 57 | }; 58 | } 59 | 60 | if (!result.data?.viewer) { 61 | return { 62 | success: false, 63 | error: "Failed to verify Linear connection", 64 | }; 65 | } 66 | 67 | return { success: true }; 68 | } catch (error) { 69 | return { 70 | success: false, 71 | error: error instanceof Error ? error.message : String(error), 72 | }; 73 | } 74 | } 75 | 76 | -------------------------------------------------------------------------------- /components/ui/workflow-icon.tsx: -------------------------------------------------------------------------------- 1 | export function WorkflowIcon({ className }: { className?: string }) { 2 | return ( 3 | 11 | 15 | 19 | 23 | 24 | ); 25 | } 26 | 27 | -------------------------------------------------------------------------------- /components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as TooltipPrimitive from "@radix-ui/react-tooltip" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function TooltipProvider({ 9 | delayDuration = 0, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ) 19 | } 20 | 21 | function Tooltip({ 22 | ...props 23 | }: React.ComponentProps) { 24 | return ( 25 | 26 | 27 | 28 | ) 29 | } 30 | 31 | function TooltipTrigger({ 32 | ...props 33 | }: React.ComponentProps) { 34 | return 35 | } 36 | 37 | function TooltipContent({ 38 | className, 39 | sideOffset = 0, 40 | children, 41 | ...props 42 | }: React.ComponentProps) { 43 | return ( 44 | 45 | 54 | {children} 55 | 56 | 57 | 58 | ) 59 | } 60 | 61 | export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider } 62 | -------------------------------------------------------------------------------- /plugins/slack/index.ts: -------------------------------------------------------------------------------- 1 | import type { IntegrationPlugin } from "../registry"; 2 | import { registerIntegration } from "../registry"; 3 | import { SlackIcon } from "./icon"; 4 | 5 | const slackPlugin: IntegrationPlugin = { 6 | type: "slack", 7 | label: "Slack", 8 | description: "Send messages to Slack channels", 9 | 10 | icon: SlackIcon, 11 | 12 | formFields: [ 13 | { 14 | id: "apiKey", 15 | label: "Bot Token", 16 | type: "password", 17 | placeholder: "xoxb-...", 18 | configKey: "apiKey", 19 | envVar: "SLACK_API_KEY", 20 | helpText: "Create a Slack app and get your Bot Token from ", 21 | helpLink: { 22 | text: "api.slack.com/apps", 23 | url: "https://api.slack.com/apps", 24 | }, 25 | }, 26 | ], 27 | 28 | testConfig: { 29 | getTestFunction: async () => { 30 | const { testSlack } = await import("./test"); 31 | return testSlack; 32 | }, 33 | }, 34 | 35 | actions: [ 36 | { 37 | slug: "send-message", 38 | label: "Send Slack Message", 39 | description: "Send a message to a Slack channel", 40 | category: "Slack", 41 | stepFunction: "sendSlackMessageStep", 42 | stepImportPath: "send-slack-message", 43 | outputFields: [ 44 | { field: "ts", description: "Message timestamp" }, 45 | { field: "channel", description: "Channel ID" }, 46 | ], 47 | configFields: [ 48 | { 49 | key: "slackChannel", 50 | label: "Channel", 51 | type: "text", 52 | placeholder: "#general or {{NodeName.channel}}", 53 | example: "#general", 54 | required: true, 55 | }, 56 | { 57 | key: "slackMessage", 58 | label: "Message", 59 | type: "template-textarea", 60 | placeholder: 61 | "Your message. Use {{NodeName.field}} to insert data from previous nodes.", 62 | rows: 4, 63 | example: "Hello from my workflow!", 64 | required: true, 65 | }, 66 | ], 67 | }, 68 | ], 69 | }; 70 | 71 | // Auto-register on import 72 | registerIntegration(slackPlugin); 73 | 74 | export default slackPlugin; 75 | -------------------------------------------------------------------------------- /plugins/v0/steps/send-message.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createClient, type ChatsSendMessageResponse } from "v0-sdk"; 4 | import { fetchCredentials } from "@/lib/credential-fetcher"; 5 | import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; 6 | import { getErrorMessage } from "@/lib/utils"; 7 | import type { V0Credentials } from "../credentials"; 8 | 9 | type SendMessageResult = 10 | | { success: true; chatId: string; demoUrl?: string } 11 | | { success: false; error: string }; 12 | 13 | export type SendMessageCoreInput = { 14 | chatId: string; 15 | message: string; 16 | }; 17 | 18 | export type SendMessageInput = StepInput & 19 | SendMessageCoreInput & { 20 | integrationId?: string; 21 | }; 22 | 23 | /** 24 | * Core logic - portable between app and export 25 | */ 26 | async function stepHandler( 27 | input: SendMessageCoreInput, 28 | credentials: V0Credentials 29 | ): Promise { 30 | const apiKey = credentials.V0_API_KEY; 31 | 32 | if (!apiKey) { 33 | return { 34 | success: false, 35 | error: 36 | "V0_API_KEY is not configured. Please add it in Project Integrations.", 37 | }; 38 | } 39 | 40 | try { 41 | const client = createClient({ apiKey }); 42 | 43 | const result = (await client.chats.sendMessage({ 44 | chatId: input.chatId, 45 | message: input.message, 46 | })) as ChatsSendMessageResponse; 47 | 48 | return { 49 | success: true, 50 | chatId: result.id, 51 | demoUrl: result.latestVersion?.demoUrl, 52 | }; 53 | } catch (error) { 54 | return { 55 | success: false, 56 | error: `Failed to send message: ${getErrorMessage(error)}`, 57 | }; 58 | } 59 | } 60 | 61 | /** 62 | * App entry point - fetches credentials and wraps with logging 63 | */ 64 | export async function sendMessageStep( 65 | input: SendMessageInput 66 | ): Promise { 67 | "use step"; 68 | 69 | const credentials = input.integrationId 70 | ? await fetchCredentials(input.integrationId) 71 | : {}; 72 | 73 | return withStepLogging(input, () => stepHandler(input, credentials)); 74 | } 75 | sendMessageStep.maxRetries = 0; 76 | 77 | export const _integrationType = "v0"; 78 | -------------------------------------------------------------------------------- /plugins/v0/steps/create-chat.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { createClient, type ChatsCreateResponse } from "v0-sdk"; 4 | import { fetchCredentials } from "@/lib/credential-fetcher"; 5 | import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; 6 | import { getErrorMessage } from "@/lib/utils"; 7 | import type { V0Credentials } from "../credentials"; 8 | 9 | type CreateChatResult = 10 | | { success: true; chatId: string; url: string; demoUrl?: string } 11 | | { success: false; error: string }; 12 | 13 | export type CreateChatCoreInput = { 14 | message: string; 15 | system?: string; 16 | }; 17 | 18 | export type CreateChatInput = StepInput & 19 | CreateChatCoreInput & { 20 | integrationId?: string; 21 | }; 22 | 23 | /** 24 | * Core logic - portable between app and export 25 | */ 26 | async function stepHandler( 27 | input: CreateChatCoreInput, 28 | credentials: V0Credentials 29 | ): Promise { 30 | const apiKey = credentials.V0_API_KEY; 31 | 32 | if (!apiKey) { 33 | return { 34 | success: false, 35 | error: 36 | "V0_API_KEY is not configured. Please add it in Project Integrations.", 37 | }; 38 | } 39 | 40 | try { 41 | const client = createClient({ apiKey }); 42 | 43 | const result = (await client.chats.create({ 44 | message: input.message, 45 | system: input.system, 46 | })) as ChatsCreateResponse; 47 | 48 | return { 49 | success: true, 50 | chatId: result.id, 51 | url: result.webUrl, 52 | demoUrl: result.latestVersion?.demoUrl, 53 | }; 54 | } catch (error) { 55 | return { 56 | success: false, 57 | error: `Failed to create chat: ${getErrorMessage(error)}`, 58 | }; 59 | } 60 | } 61 | 62 | /** 63 | * App entry point - fetches credentials and wraps with logging 64 | */ 65 | export async function createChatStep( 66 | input: CreateChatInput 67 | ): Promise { 68 | "use step"; 69 | 70 | const credentials = input.integrationId 71 | ? await fetchCredentials(input.integrationId) 72 | : {}; 73 | 74 | return withStepLogging(input, () => stepHandler(input, credentials)); 75 | } 76 | createChatStep.maxRetries = 0; 77 | 78 | export const _integrationType = "v0"; 79 | -------------------------------------------------------------------------------- /components/ui/tabs.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { Tabs as TabsPrimitive } from "radix-ui"; 5 | 6 | import { cn } from "@/lib/utils"; 7 | 8 | function Tabs({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 18 | ); 19 | } 20 | 21 | function TabsList({ 22 | className, 23 | ...props 24 | }: React.ComponentProps) { 25 | return ( 26 | 34 | ); 35 | } 36 | 37 | function TabsTrigger({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ); 51 | } 52 | 53 | function TabsContent({ 54 | className, 55 | ...props 56 | }: React.ComponentProps) { 57 | return ( 58 | 63 | ); 64 | } 65 | 66 | export { Tabs, TabsList, TabsTrigger, TabsContent }; 67 | -------------------------------------------------------------------------------- /lib/next-boilerplate/lib/credential-helper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Credential Helper for Exported Workflows 3 | * 4 | * This module provides a fetchCredentials function that reads credentials 5 | * from environment variables. It mirrors the app's fetchCredentials API 6 | * but uses env vars instead of database lookups. 7 | * 8 | * Integration types map to their env vars via a registry that gets generated 9 | * at export time based on the plugins used in the workflow. 10 | */ 11 | 12 | /** 13 | * Credential mapping for each integration type 14 | * Maps integration type -> { envVarName: envVarKey } 15 | * 16 | * This is generated at export time by the workflow exporter 17 | * and injected into the exported project. 18 | */ 19 | export const INTEGRATION_ENV_VARS: Record> = { 20 | resend: { 21 | RESEND_API_KEY: "RESEND_API_KEY", 22 | RESEND_FROM_EMAIL: "RESEND_FROM_EMAIL", 23 | }, 24 | // Additional integrations will be added at export time 25 | }; 26 | 27 | /** 28 | * Fetch credentials for an integration by its type 29 | * 30 | * In exported workflows, integrationId is the integration type (e.g., "resend") 31 | * This function reads the corresponding environment variables and returns them 32 | * in the same format as the app's fetchCredentials function. 33 | * 34 | * Note: This function is async to match the app's fetchCredentials signature, 35 | * allowing step code to use the same API in both contexts. 36 | * 37 | * @param integrationType - The integration type (e.g., "resend", "slack") 38 | * @returns A record of credential values keyed by their env var names 39 | */ 40 | export function fetchCredentials( 41 | integrationType: string 42 | ): Promise> { 43 | const envVarMapping = INTEGRATION_ENV_VARS[integrationType]; 44 | 45 | if (!envVarMapping) { 46 | console.warn( 47 | `[Credential Helper] Unknown integration type: ${integrationType}` 48 | ); 49 | return Promise.resolve({}); 50 | } 51 | 52 | const credentials: Record = {}; 53 | 54 | for (const [credKey, envVarName] of Object.entries(envVarMapping)) { 55 | credentials[credKey] = process.env[envVarName]; 56 | } 57 | 58 | return Promise.resolve(credentials); 59 | } 60 | -------------------------------------------------------------------------------- /components/ui/resizable.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import * as React from "react"; 4 | import { GripVerticalIcon } from "lucide-react"; 5 | import * as ResizablePrimitive from "react-resizable-panels"; 6 | 7 | import { cn } from "@/lib/utils"; 8 | 9 | function ResizablePanelGroup({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | ); 23 | } 24 | 25 | function ResizablePanel({ 26 | ...props 27 | }: React.ComponentProps) { 28 | return ; 29 | } 30 | 31 | function ResizableHandle({ 32 | withHandle, 33 | className, 34 | ...props 35 | }: React.ComponentProps & { 36 | withHandle?: boolean; 37 | }) { 38 | return ( 39 | div]:rotate-90", 43 | className, 44 | )} 45 | {...props} 46 | > 47 | {withHandle && ( 48 |
49 | 50 |
51 | )} 52 |
53 | ); 54 | } 55 | 56 | export { ResizablePanelGroup, ResizablePanel, ResizableHandle }; 57 | -------------------------------------------------------------------------------- /app/api/workflows/executions/[executionId]/status/route.ts: -------------------------------------------------------------------------------- 1 | import { eq } from "drizzle-orm"; 2 | import { NextResponse } from "next/server"; 3 | import { auth } from "@/lib/auth"; 4 | import { db } from "@/lib/db"; 5 | import { workflowExecutionLogs, workflowExecutions } from "@/lib/db/schema"; 6 | 7 | type NodeStatus = { 8 | nodeId: string; 9 | status: "pending" | "running" | "success" | "error"; 10 | }; 11 | 12 | export async function GET( 13 | request: Request, 14 | context: { params: Promise<{ executionId: string }> } 15 | ) { 16 | try { 17 | const { executionId } = await context.params; 18 | const session = await auth.api.getSession({ 19 | headers: request.headers, 20 | }); 21 | 22 | if (!session?.user) { 23 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 24 | } 25 | 26 | // Get the execution and verify ownership 27 | const execution = await db.query.workflowExecutions.findFirst({ 28 | where: eq(workflowExecutions.id, executionId), 29 | with: { 30 | workflow: true, 31 | }, 32 | }); 33 | 34 | if (!execution) { 35 | return NextResponse.json( 36 | { error: "Execution not found" }, 37 | { status: 404 } 38 | ); 39 | } 40 | 41 | // Verify the workflow belongs to the user 42 | if (execution.workflow.userId !== session.user.id) { 43 | return NextResponse.json({ error: "Forbidden" }, { status: 403 }); 44 | } 45 | 46 | // Get logs for all nodes 47 | const logs = await db.query.workflowExecutionLogs.findMany({ 48 | where: eq(workflowExecutionLogs.executionId, executionId), 49 | }); 50 | 51 | // Map logs to node statuses 52 | const nodeStatuses: NodeStatus[] = logs.map((log) => ({ 53 | nodeId: log.nodeId, 54 | status: log.status, 55 | })); 56 | 57 | return NextResponse.json({ 58 | status: execution.status, 59 | nodeStatuses, 60 | }); 61 | } catch (error) { 62 | console.error("Failed to get execution status:", error); 63 | return NextResponse.json( 64 | { 65 | error: 66 | error instanceof Error 67 | ? error.message 68 | : "Failed to get execution status", 69 | }, 70 | { status: 500 } 71 | ); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /components/workflow/nodes/add-node.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { NodeProps } from "@xyflow/react"; 4 | import { Plus } from "lucide-react"; 5 | import { Button } from "@/components/ui/button"; 6 | 7 | type AddNodeData = { 8 | onClick?: () => void; 9 | }; 10 | 11 | export function AddNode({ data }: NodeProps & { data?: AddNodeData }) { 12 | return ( 13 |
14 |
15 |

16 | AI Workflow Builder Template 17 |

18 |

19 | Powered by{" "} 20 | 26 | Workflow 27 | 28 | ,{" "} 29 | 35 | AI SDK 36 | 37 | ,{" "} 38 | 44 | AI Gateway 45 | {" "} 46 | and{" "} 47 | 53 | AI Elements 54 | 55 |

56 |
57 | 61 |
62 | ); 63 | } 64 | -------------------------------------------------------------------------------- /components/ui/animated-border.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { cn } from "@/lib/utils"; 4 | 5 | interface AnimatedBorderProps { 6 | className?: string; 7 | } 8 | 9 | export const AnimatedBorder = ({ className }: AnimatedBorderProps) => { 10 | return ( 11 | <> 12 | 38 |
44 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 68 | 69 |
70 | {/* Static faint border for structure */} 71 |
77 | 78 | ); 79 | }; 80 | -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Card({ className, ...props }: React.ComponentProps<"div">) { 6 | return ( 7 |
15 | ) 16 | } 17 | 18 | function CardHeader({ className, ...props }: React.ComponentProps<"div">) { 19 | return ( 20 |
28 | ) 29 | } 30 | 31 | function CardTitle({ className, ...props }: React.ComponentProps<"div">) { 32 | return ( 33 |
38 | ) 39 | } 40 | 41 | function CardDescription({ className, ...props }: React.ComponentProps<"div">) { 42 | return ( 43 |
48 | ) 49 | } 50 | 51 | function CardAction({ className, ...props }: React.ComponentProps<"div">) { 52 | return ( 53 |
61 | ) 62 | } 63 | 64 | function CardContent({ className, ...props }: React.ComponentProps<"div">) { 65 | return ( 66 |
71 | ) 72 | } 73 | 74 | function CardFooter({ className, ...props }: React.ComponentProps<"div">) { 75 | return ( 76 |
81 | ) 82 | } 83 | 84 | export { 85 | Card, 86 | CardHeader, 87 | CardFooter, 88 | CardTitle, 89 | CardAction, 90 | CardDescription, 91 | CardContent, 92 | } 93 | -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot as SlotPrimitive } from "radix-ui" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive", 9 | { 10 | variants: { 11 | variant: { 12 | default: "bg-primary text-primary-foreground hover:bg-primary/90", 13 | destructive: 14 | "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", 15 | outline: 16 | "border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:bg-input/30 dark:border-input dark:hover:bg-input/50", 17 | secondary: 18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80", 19 | ghost: 20 | "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2 has-[>svg]:px-3", 25 | sm: "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5", 26 | lg: "h-10 rounded-md px-6 has-[>svg]:px-4", 27 | icon: "size-9", 28 | "icon-sm": "size-8", 29 | "icon-lg": "size-10", 30 | }, 31 | }, 32 | defaultVariants: { 33 | variant: "default", 34 | size: "default", 35 | }, 36 | } 37 | ) 38 | 39 | function Button({ 40 | className, 41 | variant, 42 | size, 43 | asChild = false, 44 | ...props 45 | }: React.ComponentProps<"button"> & 46 | VariantProps & { 47 | asChild?: boolean 48 | }) { 49 | const Comp = asChild ? SlotPrimitive.Slot : "button" 50 | 51 | return ( 52 | 57 | ) 58 | } 59 | 60 | export { Button, buttonVariants } 61 | -------------------------------------------------------------------------------- /app/api/workflows/executions/[executionId]/logs/route.ts: -------------------------------------------------------------------------------- 1 | import { desc, eq } from "drizzle-orm"; 2 | import { NextResponse } from "next/server"; 3 | import { auth } from "@/lib/auth"; 4 | import { db } from "@/lib/db"; 5 | import { workflowExecutionLogs, workflowExecutions } from "@/lib/db/schema"; 6 | import { redactSensitiveData } from "@/lib/utils/redact"; 7 | 8 | export async function GET( 9 | request: Request, 10 | context: { params: Promise<{ executionId: string }> } 11 | ) { 12 | try { 13 | const { executionId } = await context.params; 14 | const session = await auth.api.getSession({ 15 | headers: request.headers, 16 | }); 17 | 18 | if (!session?.user) { 19 | return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); 20 | } 21 | 22 | // Get the execution and verify ownership 23 | const execution = await db.query.workflowExecutions.findFirst({ 24 | where: eq(workflowExecutions.id, executionId), 25 | with: { 26 | workflow: true, 27 | }, 28 | }); 29 | 30 | if (!execution) { 31 | return NextResponse.json( 32 | { error: "Execution not found" }, 33 | { status: 404 } 34 | ); 35 | } 36 | 37 | // Verify the workflow belongs to the user 38 | if (execution.workflow.userId !== session.user.id) { 39 | return NextResponse.json({ error: "Forbidden" }, { status: 403 }); 40 | } 41 | 42 | // Get logs 43 | const logs = await db.query.workflowExecutionLogs.findMany({ 44 | where: eq(workflowExecutionLogs.executionId, executionId), 45 | orderBy: [desc(workflowExecutionLogs.timestamp)], 46 | }); 47 | 48 | // Apply an additional layer of redaction to ensure no sensitive data is exposed 49 | // Even though data should already be redacted when stored, this provides defense in depth 50 | const redactedLogs = logs.map((log) => ({ 51 | ...log, 52 | input: redactSensitiveData(log.input), 53 | output: redactSensitiveData(log.output), 54 | })); 55 | 56 | return NextResponse.json({ 57 | execution, 58 | logs: redactedLogs, 59 | }); 60 | } catch (error) { 61 | console.error("Failed to get execution logs:", error); 62 | return NextResponse.json( 63 | { 64 | error: 65 | error instanceof Error 66 | ? error.message 67 | : "Failed to get execution logs", 68 | }, 69 | { status: 500 } 70 | ); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /app/workflows/[workflowId]/layout.tsx: -------------------------------------------------------------------------------- 1 | import { eq } from "drizzle-orm"; 2 | import type { Metadata } from "next"; 3 | import type { ReactNode } from "react"; 4 | import { db } from "@/lib/db"; 5 | import { workflows } from "@/lib/db/schema"; 6 | 7 | type WorkflowLayoutProps = { 8 | children: ReactNode; 9 | params: Promise<{ workflowId: string }>; 10 | }; 11 | 12 | export async function generateMetadata({ 13 | params, 14 | }: WorkflowLayoutProps): Promise { 15 | const { workflowId } = await params; 16 | 17 | // Try to fetch the workflow to get its name 18 | let title = "Workflow"; 19 | let isPublic = false; 20 | 21 | try { 22 | const workflow = await db.query.workflows.findFirst({ 23 | where: eq(workflows.id, workflowId), 24 | columns: { 25 | name: true, 26 | visibility: true, 27 | }, 28 | }); 29 | 30 | if (workflow) { 31 | isPublic = workflow.visibility === "public"; 32 | // Only expose workflow name in metadata if it's public 33 | // This prevents private workflow name enumeration 34 | if (isPublic) { 35 | title = workflow.name; 36 | } 37 | } 38 | } catch { 39 | // Ignore errors, use defaults 40 | } 41 | 42 | const baseUrl = 43 | process.env.NEXT_PUBLIC_APP_URL || "https://workflow-builder.dev"; 44 | const workflowUrl = `${baseUrl}/workflows/${workflowId}`; 45 | const ogImageUrl = isPublic 46 | ? `${baseUrl}/api/og/workflow/${workflowId}` 47 | : `${baseUrl}/og-default.png`; 48 | 49 | return { 50 | title: `${title} | AI Workflow Builder`, 51 | description: `View and explore the "${title}" workflow built with AI Workflow Builder.`, 52 | openGraph: { 53 | title: `${title} | AI Workflow Builder`, 54 | description: `View and explore the "${title}" workflow built with AI Workflow Builder.`, 55 | type: "website", 56 | url: workflowUrl, 57 | siteName: "AI Workflow Builder", 58 | images: [ 59 | { 60 | url: ogImageUrl, 61 | width: 1200, 62 | height: 630, 63 | alt: `${title} workflow visualization`, 64 | }, 65 | ], 66 | }, 67 | twitter: { 68 | card: "summary_large_image", 69 | title: `${title} | AI Workflow Builder`, 70 | description: `View and explore the "${title}" workflow built with AI Workflow Builder.`, 71 | images: [ogImageUrl], 72 | }, 73 | }; 74 | } 75 | 76 | export default function WorkflowLayout({ children }: WorkflowLayoutProps) { 77 | return children; 78 | } 79 | -------------------------------------------------------------------------------- /components/ai-elements/controls.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useReactFlow } from "@xyflow/react"; 4 | import { ZoomIn, ZoomOut, Maximize2, MapPin, MapPinXInside } from "lucide-react"; 5 | import { useAtom } from "jotai"; 6 | import { Button } from "@/components/ui/button"; 7 | import { ButtonGroup } from "@/components/ui/button-group"; 8 | import { showMinimapAtom } from "@/lib/workflow-store"; 9 | 10 | export const Controls = () => { 11 | const { zoomIn, zoomOut, fitView } = useReactFlow(); 12 | const [showMinimap, setShowMinimap] = useAtom(showMinimapAtom); 13 | 14 | const handleZoomIn = () => { 15 | zoomIn(); 16 | }; 17 | 18 | const handleZoomOut = () => { 19 | zoomOut(); 20 | }; 21 | 22 | const handleFitView = () => { 23 | fitView({ padding: 0.2, duration: 300 }); 24 | }; 25 | 26 | const handleToggleMinimap = () => { 27 | setShowMinimap(!showMinimap); 28 | }; 29 | 30 | return ( 31 | 32 | 41 | 50 | 59 | 72 | 73 | ); 74 | }; 75 | -------------------------------------------------------------------------------- /lib/auth-providers.ts: -------------------------------------------------------------------------------- 1 | type AuthProvider = "email" | "github" | "google" | "vercel"; 2 | 3 | type EnabledProviders = { 4 | email: boolean; 5 | github: boolean; 6 | google: boolean; 7 | vercel: boolean; 8 | }; 9 | 10 | interface WindowWithEnv extends Window { 11 | ENV?: { 12 | NEXT_PUBLIC_AUTH_PROVIDERS?: string; 13 | NEXT_PUBLIC_GITHUB_CLIENT_ID?: string; 14 | NEXT_PUBLIC_GOOGLE_CLIENT_ID?: string; 15 | NEXT_PUBLIC_VERCEL_CLIENT_ID?: string; 16 | }; 17 | } 18 | 19 | /** 20 | * Get the list of enabled authentication providers from environment variables 21 | * Defaults to email only if not specified 22 | */ 23 | export function getEnabledAuthProviders(): EnabledProviders { 24 | const providersEnv = 25 | process.env.NEXT_PUBLIC_AUTH_PROVIDERS || 26 | (typeof window !== "undefined" 27 | ? (window as WindowWithEnv).ENV?.NEXT_PUBLIC_AUTH_PROVIDERS 28 | : undefined) || 29 | "email"; 30 | 31 | const enabledProviders = providersEnv 32 | .split(",") 33 | .map((p: string) => p.trim().toLowerCase()); 34 | 35 | return { 36 | email: enabledProviders.includes("email"), 37 | github: 38 | enabledProviders.includes("github") && 39 | !!( 40 | process.env.NEXT_PUBLIC_GITHUB_CLIENT_ID || 41 | (typeof window !== "undefined" && 42 | (window as WindowWithEnv).ENV?.NEXT_PUBLIC_GITHUB_CLIENT_ID) 43 | ), 44 | google: 45 | enabledProviders.includes("google") && 46 | !!( 47 | process.env.NEXT_PUBLIC_GOOGLE_CLIENT_ID || 48 | (typeof window !== "undefined" && 49 | (window as WindowWithEnv).ENV?.NEXT_PUBLIC_GOOGLE_CLIENT_ID) 50 | ), 51 | vercel: 52 | enabledProviders.includes("vercel") && 53 | !!( 54 | process.env.NEXT_PUBLIC_VERCEL_CLIENT_ID || 55 | (typeof window !== "undefined" && 56 | (window as WindowWithEnv).ENV?.NEXT_PUBLIC_VERCEL_CLIENT_ID) 57 | ), 58 | }; 59 | } 60 | 61 | /** 62 | * Get array of enabled provider names 63 | */ 64 | export function getEnabledProvidersList(): AuthProvider[] { 65 | const providers = getEnabledAuthProviders(); 66 | return Object.entries(providers) 67 | .filter(([, enabled]) => enabled) 68 | .map(([name]) => name as AuthProvider); 69 | } 70 | 71 | /** 72 | * Get the single enabled provider, or null if there are multiple 73 | */ 74 | export function getSingleProvider(): AuthProvider | null { 75 | const providersList = getEnabledProvidersList(); 76 | return providersList.length === 1 ? providersList[0] : null; 77 | } 78 | -------------------------------------------------------------------------------- /components/overlays/export-workflow-overlay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Download, FlaskConical } from "lucide-react"; 4 | import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"; 5 | import { Overlay } from "./overlay"; 6 | import { useOverlay } from "./overlay-provider"; 7 | import type { OverlayComponentProps } from "./types"; 8 | 9 | type ExportWorkflowOverlayProps = OverlayComponentProps<{ 10 | onExport: () => void; 11 | isDownloading?: boolean; 12 | }>; 13 | 14 | export function ExportWorkflowOverlay({ 15 | overlayId, 16 | onExport, 17 | isDownloading, 18 | }: ExportWorkflowOverlayProps) { 19 | const { closeAll } = useOverlay(); 20 | 21 | const handleExport = () => { 22 | closeAll(); 23 | onExport(); 24 | }; 25 | 26 | return ( 27 | 39 |
40 | 41 |

42 | Export your workflow as a standalone Next.js project that you can run 43 | independently. 44 |

45 |
46 | 47 |

48 | This will generate a complete Next.js project containing your workflow 49 | code. Once exported, you can run your workflow outside of the Workflow 50 | Builder, deploy it to Vercel, or integrate it into your existing 51 | applications. 52 |

53 | 54 | 55 | 56 | Experimental Feature 57 | 58 | This feature is experimental and may have limitations. If you 59 | encounter any issues, please{" "} 60 | 66 | report them on GitHub 67 | 68 | . 69 | 70 | 71 |
72 | ); 73 | } 74 | -------------------------------------------------------------------------------- /components/ui/button-group.tsx: -------------------------------------------------------------------------------- 1 | import { Slot as SlotPrimitive } from "radix-ui" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | import { Separator } from "@/components/ui/separator" 6 | 7 | const buttonGroupVariants = cva( 8 | "flex w-fit items-stretch [&>*]:focus-visible:z-10 [&>*]:focus-visible:relative [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1 has-[select[aria-hidden=true]:last-child]:[&>[data-slot=select-trigger]:last-of-type]:rounded-r-md has-[>[data-slot=button-group]]:gap-2", 9 | { 10 | variants: { 11 | orientation: { 12 | horizontal: 13 | "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none", 14 | vertical: 15 | "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none", 16 | }, 17 | }, 18 | defaultVariants: { 19 | orientation: "horizontal", 20 | }, 21 | } 22 | ) 23 | 24 | function ButtonGroup({ 25 | className, 26 | orientation, 27 | ...props 28 | }: React.ComponentProps<"div"> & VariantProps) { 29 | return ( 30 |
37 | ) 38 | } 39 | 40 | function ButtonGroupText({ 41 | className, 42 | asChild = false, 43 | ...props 44 | }: React.ComponentProps<"div"> & { 45 | asChild?: boolean 46 | }) { 47 | const Comp = asChild ? SlotPrimitive.Slot : "div" 48 | 49 | return ( 50 | 57 | ) 58 | } 59 | 60 | function ButtonGroupSeparator({ 61 | className, 62 | orientation = "vertical", 63 | ...props 64 | }: React.ComponentProps) { 65 | return ( 66 | 75 | ) 76 | } 77 | 78 | export { 79 | ButtonGroup, 80 | ButtonGroupSeparator, 81 | ButtonGroupText, 82 | buttonGroupVariants, 83 | } 84 | -------------------------------------------------------------------------------- /components/ai-elements/node.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardAction, 4 | CardContent, 5 | CardDescription, 6 | CardFooter, 7 | CardHeader, 8 | CardTitle, 9 | } from "@/components/ui/card"; 10 | import { cn } from "@/lib/utils"; 11 | import { Handle, Position } from "@xyflow/react"; 12 | import type { ComponentProps } from "react"; 13 | import { AnimatedBorder } from "@/components/ui/animated-border"; 14 | 15 | export type NodeProps = ComponentProps & { 16 | handles: { 17 | target: boolean; 18 | source: boolean; 19 | }; 20 | status?: "idle" | "running" | "success" | "error"; 21 | }; 22 | 23 | export const Node = ({ handles, className, status, ...props }: NodeProps) => ( 24 | 33 | {status === "running" && } 34 | {handles.target && } 35 | {handles.source && } 36 | {props.children} 37 | 38 | ); 39 | 40 | export type NodeHeaderProps = ComponentProps; 41 | 42 | export const NodeHeader = ({ className, ...props }: NodeHeaderProps) => ( 43 | 47 | ); 48 | 49 | export type NodeTitleProps = ComponentProps; 50 | 51 | export const NodeTitle = (props: NodeTitleProps) => ; 52 | 53 | export type NodeDescriptionProps = ComponentProps; 54 | 55 | export const NodeDescription = (props: NodeDescriptionProps) => ( 56 | 57 | ); 58 | 59 | export type NodeActionProps = ComponentProps; 60 | 61 | export const NodeAction = (props: NodeActionProps) => ; 62 | 63 | export type NodeContentProps = ComponentProps; 64 | 65 | export const NodeContent = ({ className, ...props }: NodeContentProps) => ( 66 | 67 | ); 68 | 69 | export type NodeFooterProps = ComponentProps; 70 | 71 | export const NodeFooter = ({ className, ...props }: NodeFooterProps) => ( 72 | 76 | ); 77 | -------------------------------------------------------------------------------- /components/workflow/nodes/trigger-node.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import type { NodeProps } from "@xyflow/react"; 4 | import { Check, Clock, Play, Webhook, XCircle } from "lucide-react"; 5 | import { memo } from "react"; 6 | import { 7 | Node, 8 | NodeDescription, 9 | NodeTitle, 10 | } from "@/components/ai-elements/node"; 11 | import { cn } from "@/lib/utils"; 12 | import type { WorkflowNodeData } from "@/lib/workflow-store"; 13 | 14 | type TriggerNodeProps = NodeProps & { 15 | data?: WorkflowNodeData; 16 | }; 17 | 18 | export const TriggerNode = memo(({ data, selected }: TriggerNodeProps) => { 19 | if (!data) { 20 | return null; 21 | } 22 | 23 | const triggerType = (data.config?.triggerType as string) || "Manual"; 24 | const displayTitle = data.label || triggerType; 25 | const displayDescription = data.description || "Trigger"; 26 | const status = data.status; 27 | 28 | // Select icon based on trigger type 29 | let TriggerIcon = Play; 30 | if (triggerType === "Schedule") { 31 | TriggerIcon = Clock; 32 | } else if (triggerType === "Webhook") { 33 | TriggerIcon = Webhook; 34 | } 35 | 36 | return ( 37 | 45 | {/* Status indicator badge in top right */} 46 | {status && status !== "idle" && status !== "running" && ( 47 |
54 | {status === "success" && ( 55 | 56 | )} 57 | {status === "error" && ( 58 | 59 | )} 60 |
61 | )} 62 | 63 |
64 | 65 |
66 | {displayTitle} 67 | {displayDescription && ( 68 | 69 | {displayDescription} 70 | 71 | )} 72 |
73 |
74 |
75 | ); 76 | }); 77 | 78 | TriggerNode.displayName = "TriggerNode"; 79 | -------------------------------------------------------------------------------- /plugins/firecrawl/index.ts: -------------------------------------------------------------------------------- 1 | import type { IntegrationPlugin } from "../registry"; 2 | import { registerIntegration } from "../registry"; 3 | import { FirecrawlIcon } from "./icon"; 4 | 5 | const firecrawlPlugin: IntegrationPlugin = { 6 | type: "firecrawl", 7 | label: "Firecrawl", 8 | description: "Scrape, search, and crawl the web", 9 | 10 | icon: FirecrawlIcon, 11 | 12 | formFields: [ 13 | { 14 | id: "firecrawlApiKey", 15 | label: "API Key", 16 | type: "password", 17 | placeholder: "fc-...", 18 | configKey: "firecrawlApiKey", 19 | envVar: "FIRECRAWL_API_KEY", 20 | helpText: "Get your API key from ", 21 | helpLink: { 22 | text: "firecrawl.dev", 23 | url: "https://firecrawl.dev/app/api-keys", 24 | }, 25 | }, 26 | ], 27 | 28 | testConfig: { 29 | getTestFunction: async () => { 30 | const { testFirecrawl } = await import("./test"); 31 | return testFirecrawl; 32 | }, 33 | }, 34 | 35 | actions: [ 36 | { 37 | slug: "scrape", 38 | label: "Scrape URL", 39 | description: "Scrape content from a URL", 40 | category: "Firecrawl", 41 | stepFunction: "firecrawlScrapeStep", 42 | stepImportPath: "scrape", 43 | outputFields: [ 44 | { field: "markdown", description: "Scraped content as markdown" }, 45 | { field: "metadata", description: "Page metadata object" }, 46 | ], 47 | configFields: [ 48 | { 49 | key: "url", 50 | label: "URL", 51 | type: "template-input", 52 | placeholder: "https://example.com or {{NodeName.url}}", 53 | example: "https://example.com", 54 | required: true, 55 | }, 56 | ], 57 | }, 58 | { 59 | slug: "search", 60 | label: "Search Web", 61 | description: "Search the web with Firecrawl", 62 | category: "Firecrawl", 63 | stepFunction: "firecrawlSearchStep", 64 | stepImportPath: "search", 65 | outputFields: [{ field: "data", description: "Array of search results" }], 66 | configFields: [ 67 | { 68 | key: "query", 69 | label: "Search Query", 70 | type: "template-input", 71 | placeholder: "Search query or {{NodeName.query}}", 72 | example: "latest AI news", 73 | required: true, 74 | }, 75 | { 76 | key: "limit", 77 | label: "Result Limit", 78 | type: "number", 79 | placeholder: "10", 80 | min: 1, 81 | example: "10", 82 | }, 83 | ], 84 | }, 85 | ], 86 | }; 87 | 88 | // Auto-register on import 89 | registerIntegration(firecrawlPlugin); 90 | 91 | export default firecrawlPlugin; 92 | -------------------------------------------------------------------------------- /plugins/clerk/steps/get-user.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { fetchCredentials } from "@/lib/credential-fetcher"; 4 | import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; 5 | import { getErrorMessage } from "@/lib/utils"; 6 | import type { ClerkCredentials } from "../credentials"; 7 | import { type ClerkUserResult, toClerkUserData } from "../types"; 8 | 9 | export type ClerkGetUserCoreInput = { 10 | userId: string; 11 | }; 12 | 13 | export type ClerkGetUserInput = StepInput & 14 | ClerkGetUserCoreInput & { 15 | integrationId?: string; 16 | }; 17 | 18 | /** 19 | * Core logic - portable between app and export 20 | */ 21 | async function stepHandler( 22 | input: ClerkGetUserCoreInput, 23 | credentials: ClerkCredentials 24 | ): Promise { 25 | const secretKey = credentials.CLERK_SECRET_KEY; 26 | 27 | if (!secretKey) { 28 | return { 29 | success: false, 30 | error: { 31 | message: 32 | "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", 33 | }, 34 | }; 35 | } 36 | 37 | if (!input.userId) { 38 | return { 39 | success: false, 40 | error: { message: "User ID is required." }, 41 | }; 42 | } 43 | 44 | try { 45 | const response = await fetch( 46 | `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`, 47 | { 48 | headers: { 49 | Authorization: `Bearer ${secretKey}`, 50 | "Content-Type": "application/json", 51 | "User-Agent": "workflow-builder.dev", 52 | }, 53 | } 54 | ); 55 | 56 | if (!response.ok) { 57 | const errorBody = await response.json().catch(() => ({})); 58 | return { 59 | success: false, 60 | error: { 61 | message: 62 | errorBody.errors?.[0]?.message || 63 | `Failed to get user: ${response.status}`, 64 | }, 65 | }; 66 | } 67 | 68 | const apiUser = await response.json(); 69 | return { success: true, data: toClerkUserData(apiUser) }; 70 | } catch (err) { 71 | return { 72 | success: false, 73 | error: { message: `Failed to get user: ${getErrorMessage(err)}` }, 74 | }; 75 | } 76 | } 77 | 78 | /** 79 | * App entry point - fetches credentials and wraps with logging 80 | */ 81 | export async function clerkGetUserStep( 82 | input: ClerkGetUserInput 83 | ): Promise { 84 | "use step"; 85 | 86 | const credentials = input.integrationId 87 | ? await fetchCredentials(input.integrationId) 88 | : {}; 89 | 90 | return withStepLogging(input, () => stepHandler(input, credentials)); 91 | } 92 | clerkGetUserStep.maxRetries = 0; 93 | 94 | export const _integrationType = "clerk"; 95 | -------------------------------------------------------------------------------- /plugins/firecrawl/steps/search.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { fetchCredentials } from "@/lib/credential-fetcher"; 4 | import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; 5 | import { getErrorMessage } from "@/lib/utils"; 6 | import type { FirecrawlCredentials } from "../credentials"; 7 | 8 | const FIRECRAWL_API_URL = "https://api.firecrawl.dev/v1"; 9 | 10 | type FirecrawlSearchResponse = { 11 | success: boolean; 12 | data?: unknown[]; 13 | error?: string; 14 | }; 15 | 16 | type SearchResult = { 17 | data?: unknown[]; 18 | }; 19 | 20 | export type FirecrawlSearchCoreInput = { 21 | query: string; 22 | limit?: number; 23 | scrapeOptions?: { 24 | formats?: ("markdown" | "html" | "rawHtml" | "links" | "screenshot")[]; 25 | }; 26 | }; 27 | 28 | export type FirecrawlSearchInput = StepInput & 29 | FirecrawlSearchCoreInput & { 30 | integrationId?: string; 31 | }; 32 | 33 | /** 34 | * Core logic 35 | */ 36 | async function stepHandler( 37 | input: FirecrawlSearchCoreInput, 38 | credentials: FirecrawlCredentials 39 | ): Promise { 40 | const apiKey = credentials.FIRECRAWL_API_KEY; 41 | 42 | if (!apiKey) { 43 | throw new Error("Firecrawl API Key is not configured."); 44 | } 45 | 46 | try { 47 | const response = await fetch(`${FIRECRAWL_API_URL}/search`, { 48 | method: "POST", 49 | headers: { 50 | "Content-Type": "application/json", 51 | Authorization: `Bearer ${apiKey}`, 52 | }, 53 | body: JSON.stringify({ 54 | query: input.query, 55 | limit: input.limit ? Number(input.limit) : undefined, 56 | scrapeOptions: input.scrapeOptions, 57 | }), 58 | }); 59 | 60 | if (!response.ok) { 61 | const errorText = await response.text(); 62 | throw new Error(`HTTP ${response.status}: ${errorText}`); 63 | } 64 | 65 | const result = (await response.json()) as FirecrawlSearchResponse; 66 | 67 | if (!result.success) { 68 | throw new Error(result.error || "Search failed"); 69 | } 70 | 71 | return { 72 | data: result.data, 73 | }; 74 | } catch (error) { 75 | throw new Error(`Failed to search: ${getErrorMessage(error)}`); 76 | } 77 | } 78 | 79 | /** 80 | * App entry point - fetches credentials and wraps with logging 81 | */ 82 | export async function firecrawlSearchStep( 83 | input: FirecrawlSearchInput 84 | ): Promise { 85 | "use step"; 86 | 87 | const credentials = input.integrationId 88 | ? await fetchCredentials(input.integrationId) 89 | : {}; 90 | 91 | return withStepLogging(input, () => stepHandler(input, credentials)); 92 | } 93 | firecrawlSearchStep.maxRetries = 0; 94 | 95 | export const _integrationType = "firecrawl"; 96 | -------------------------------------------------------------------------------- /lib/workflow-logging.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Server-only workflow logging functions 3 | * These replace the HTTP endpoint for better security 4 | */ 5 | import "server-only"; 6 | 7 | import { eq } from "drizzle-orm"; 8 | import { db } from "@/lib/db"; 9 | import { workflowExecutionLogs, workflowExecutions } from "@/lib/db/schema"; 10 | 11 | export type LogStepStartParams = { 12 | executionId: string; 13 | nodeId: string; 14 | nodeName: string; 15 | nodeType: string; 16 | input?: unknown; 17 | }; 18 | 19 | export type LogStepStartResult = { 20 | logId: string; 21 | startTime: number; 22 | }; 23 | 24 | /** 25 | * Log the start of a step execution 26 | */ 27 | export async function logStepStartDb( 28 | params: LogStepStartParams 29 | ): Promise { 30 | const [log] = await db 31 | .insert(workflowExecutionLogs) 32 | .values({ 33 | executionId: params.executionId, 34 | nodeId: params.nodeId, 35 | nodeName: params.nodeName, 36 | nodeType: params.nodeType, 37 | status: "running", 38 | input: params.input, 39 | startedAt: new Date(), 40 | }) 41 | .returning(); 42 | 43 | return { 44 | logId: log.id, 45 | startTime: Date.now(), 46 | }; 47 | } 48 | 49 | export type LogStepCompleteParams = { 50 | logId: string; 51 | startTime: number; 52 | status: "success" | "error"; 53 | output?: unknown; 54 | error?: string; 55 | }; 56 | 57 | /** 58 | * Log the completion of a step execution 59 | */ 60 | export async function logStepCompleteDb( 61 | params: LogStepCompleteParams 62 | ): Promise { 63 | const duration = Date.now() - params.startTime; 64 | 65 | await db 66 | .update(workflowExecutionLogs) 67 | .set({ 68 | status: params.status, 69 | output: params.output, 70 | error: params.error, 71 | completedAt: new Date(), 72 | duration: duration.toString(), 73 | }) 74 | .where(eq(workflowExecutionLogs.id, params.logId)); 75 | } 76 | 77 | export type LogWorkflowCompleteParams = { 78 | executionId: string; 79 | status: "success" | "error"; 80 | output?: unknown; 81 | error?: string; 82 | startTime: number; 83 | }; 84 | 85 | /** 86 | * Log the completion of a workflow execution 87 | */ 88 | export async function logWorkflowCompleteDb( 89 | params: LogWorkflowCompleteParams 90 | ): Promise { 91 | const duration = Date.now() - params.startTime; 92 | 93 | await db 94 | .update(workflowExecutions) 95 | .set({ 96 | status: params.status, 97 | output: params.output, 98 | error: params.error, 99 | completedAt: new Date(), 100 | duration: duration.toString(), 101 | }) 102 | .where(eq(workflowExecutions.id, params.executionId)); 103 | } 104 | -------------------------------------------------------------------------------- /plugins/clerk/steps/delete-user.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { fetchCredentials } from "@/lib/credential-fetcher"; 4 | import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; 5 | import { getErrorMessage } from "@/lib/utils"; 6 | import type { ClerkCredentials } from "../credentials"; 7 | 8 | type DeleteUserResult = 9 | | { success: true; data: { deleted: true } } 10 | | { success: false; error: { message: string } }; 11 | 12 | export type ClerkDeleteUserCoreInput = { 13 | userId: string; 14 | }; 15 | 16 | export type ClerkDeleteUserInput = StepInput & 17 | ClerkDeleteUserCoreInput & { 18 | integrationId?: string; 19 | }; 20 | 21 | /** 22 | * Core logic - portable between app and export 23 | */ 24 | async function stepHandler( 25 | input: ClerkDeleteUserCoreInput, 26 | credentials: ClerkCredentials 27 | ): Promise { 28 | const secretKey = credentials.CLERK_SECRET_KEY; 29 | 30 | if (!secretKey) { 31 | return { 32 | success: false, 33 | error: { 34 | message: 35 | "CLERK_SECRET_KEY is not configured. Please add it in Project Integrations.", 36 | }, 37 | }; 38 | } 39 | 40 | if (!input.userId) { 41 | return { 42 | success: false, 43 | error: { message: "User ID is required." }, 44 | }; 45 | } 46 | 47 | try { 48 | const response = await fetch( 49 | `https://api.clerk.com/v1/users/${encodeURIComponent(input.userId)}`, 50 | { 51 | method: "DELETE", 52 | headers: { 53 | Authorization: `Bearer ${secretKey}`, 54 | "Content-Type": "application/json", 55 | "User-Agent": "workflow-builder.dev", 56 | }, 57 | } 58 | ); 59 | 60 | if (!response.ok) { 61 | const errorBody = await response.json().catch(() => ({})); 62 | return { 63 | success: false, 64 | error: { 65 | message: 66 | errorBody.errors?.[0]?.message || 67 | `Failed to delete user: ${response.status}`, 68 | }, 69 | }; 70 | } 71 | 72 | return { success: true, data: { deleted: true } }; 73 | } catch (err) { 74 | return { 75 | success: false, 76 | error: { message: `Failed to delete user: ${getErrorMessage(err)}` }, 77 | }; 78 | } 79 | } 80 | 81 | /** 82 | * App entry point - fetches credentials and wraps with logging 83 | */ 84 | export async function clerkDeleteUserStep( 85 | input: ClerkDeleteUserInput 86 | ): Promise { 87 | "use step"; 88 | 89 | const credentials = input.integrationId 90 | ? await fetchCredentials(input.integrationId) 91 | : {}; 92 | 93 | return withStepLogging(input, () => stepHandler(input, credentials)); 94 | } 95 | clerkDeleteUserStep.maxRetries = 0; 96 | 97 | export const _integrationType = "clerk"; 98 | -------------------------------------------------------------------------------- /plugins/firecrawl/steps/scrape.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { fetchCredentials } from "@/lib/credential-fetcher"; 4 | import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; 5 | import { getErrorMessage } from "@/lib/utils"; 6 | import type { FirecrawlCredentials } from "../credentials"; 7 | 8 | const FIRECRAWL_API_URL = "https://api.firecrawl.dev/v1"; 9 | 10 | type FirecrawlScrapeResponse = { 11 | success: boolean; 12 | data?: { 13 | markdown?: string; 14 | metadata?: Record; 15 | }; 16 | error?: string; 17 | }; 18 | 19 | type ScrapeResult = { 20 | markdown?: string; 21 | metadata?: Record; 22 | }; 23 | 24 | export type FirecrawlScrapeCoreInput = { 25 | url: string; 26 | formats?: ("markdown" | "html" | "rawHtml" | "links" | "screenshot")[]; 27 | }; 28 | 29 | export type FirecrawlScrapeInput = StepInput & 30 | FirecrawlScrapeCoreInput & { 31 | integrationId?: string; 32 | }; 33 | 34 | /** 35 | * Core logic - portable between app and export 36 | */ 37 | async function stepHandler( 38 | input: FirecrawlScrapeCoreInput, 39 | credentials: FirecrawlCredentials 40 | ): Promise { 41 | const apiKey = credentials.FIRECRAWL_API_KEY; 42 | 43 | if (!apiKey) { 44 | throw new Error("Firecrawl API Key is not configured."); 45 | } 46 | 47 | try { 48 | const response = await fetch(`${FIRECRAWL_API_URL}/scrape`, { 49 | method: "POST", 50 | headers: { 51 | "Content-Type": "application/json", 52 | Authorization: `Bearer ${apiKey}`, 53 | }, 54 | body: JSON.stringify({ 55 | url: input.url, 56 | formats: input.formats || ["markdown"], 57 | }), 58 | }); 59 | 60 | if (!response.ok) { 61 | const errorText = await response.text(); 62 | throw new Error(`HTTP ${response.status}: ${errorText}`); 63 | } 64 | 65 | const result = (await response.json()) as FirecrawlScrapeResponse; 66 | 67 | if (!result.success) { 68 | throw new Error(result.error || "Scrape failed"); 69 | } 70 | 71 | return { 72 | markdown: result.data?.markdown, 73 | metadata: result.data?.metadata, 74 | }; 75 | } catch (error) { 76 | throw new Error(`Failed to scrape: ${getErrorMessage(error)}`); 77 | } 78 | } 79 | 80 | /** 81 | * App entry point - fetches credentials and wraps with logging 82 | */ 83 | export async function firecrawlScrapeStep( 84 | input: FirecrawlScrapeInput 85 | ): Promise { 86 | "use step"; 87 | 88 | const credentials = input.integrationId 89 | ? await fetchCredentials(input.integrationId) 90 | : {}; 91 | 92 | return withStepLogging(input, () => stepHandler(input, credentials)); 93 | } 94 | firecrawlScrapeStep.maxRetries = 0; 95 | 96 | export const _integrationType = "firecrawl"; 97 | -------------------------------------------------------------------------------- /components/overlays/settings-overlay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useCallback, useEffect, useState } from "react"; 4 | import { toast } from "sonner"; 5 | import { AccountSettings } from "@/components/settings/account-settings"; 6 | import { Spinner } from "@/components/ui/spinner"; 7 | import { api } from "@/lib/api-client"; 8 | import { Overlay } from "./overlay"; 9 | import { useOverlay } from "./overlay-provider"; 10 | 11 | type SettingsOverlayProps = { 12 | overlayId: string; 13 | }; 14 | 15 | export function SettingsOverlay({ overlayId }: SettingsOverlayProps) { 16 | const { closeAll } = useOverlay(); 17 | const [loading, setLoading] = useState(true); 18 | const [saving, setSaving] = useState(false); 19 | 20 | // Account state 21 | const [accountName, setAccountName] = useState(""); 22 | const [accountEmail, setAccountEmail] = useState(""); 23 | 24 | const loadAccount = useCallback(async () => { 25 | try { 26 | const data = await api.user.get(); 27 | setAccountName(data.name || ""); 28 | setAccountEmail(data.email || ""); 29 | } catch (error) { 30 | console.error("Failed to load account:", error); 31 | } 32 | }, []); 33 | 34 | const loadAll = useCallback(async () => { 35 | setLoading(true); 36 | try { 37 | await loadAccount(); 38 | } finally { 39 | setLoading(false); 40 | } 41 | }, [loadAccount]); 42 | 43 | useEffect(() => { 44 | loadAll(); 45 | }, [loadAll]); 46 | 47 | const saveAccount = async () => { 48 | try { 49 | setSaving(true); 50 | await api.user.update({ name: accountName, email: accountEmail }); 51 | await loadAccount(); 52 | toast.success("Settings saved"); 53 | closeAll(); 54 | } catch (error) { 55 | console.error("Failed to save account:", error); 56 | toast.error("Failed to save settings"); 57 | } finally { 58 | setSaving(false); 59 | } 60 | }; 61 | 62 | return ( 63 | 76 |

77 | Update your personal information 78 |

79 | 80 | {loading ? ( 81 |
82 | 83 |
84 | ) : ( 85 | 91 | )} 92 |
93 | ); 94 | } 95 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ai-workflow-builder-template", 3 | "version": "0.1.0", 4 | "private": true, 5 | "license": "Apache-2.0", 6 | "description": "A template for building your own AI-driven workflow automation platform", 7 | "scripts": { 8 | "dev": "pnpm discover-plugins && next dev", 9 | "build": "pnpm discover-plugins && next build", 10 | "start": "next start", 11 | "type-check": "tsc --noEmit", 12 | "check": "npx ultracite@latest check", 13 | "fix": "npx ultracite@latest fix", 14 | "db:generate": "drizzle-kit generate", 15 | "db:migrate": "drizzle-kit migrate", 16 | "db:push": "drizzle-kit push", 17 | "db:studio": "drizzle-kit studio", 18 | "discover-plugins": "tsx scripts/discover-plugins.ts", 19 | "create-plugin": "tsx scripts/create-plugin.ts", 20 | "test:e2e": "playwright test", 21 | "test:e2e:ui": "playwright test --ui" 22 | }, 23 | "dependencies": { 24 | "@ai-sdk/provider": "^2.0.0", 25 | "@linear/sdk": "^63.2.0", 26 | "@mendable/firecrawl-js": "^4.6.2", 27 | "@monaco-editor/react": "^4.7.0", 28 | "@radix-ui/react-checkbox": "^1.3.3", 29 | "@radix-ui/react-collapsible": "^1.1.12", 30 | "@radix-ui/react-context-menu": "^2.2.16", 31 | "@radix-ui/react-dialog": "^1.1.15", 32 | "@radix-ui/react-tooltip": "^1.2.8", 33 | "@slack/web-api": "^7.12.0", 34 | "@vercel/analytics": "^1.5.0", 35 | "@vercel/og": "^0.8.5", 36 | "@vercel/sdk": "^1.17.1", 37 | "@vercel/speed-insights": "^1.2.0", 38 | "@xyflow/react": "^12.9.2", 39 | "ai": "^5.0.102", 40 | "better-auth": "^1.3.34", 41 | "class-variance-authority": "^0.7.1", 42 | "clsx": "^2.1.1", 43 | "dotenv": "^17.2.3", 44 | "drizzle-orm": "^0.44.7", 45 | "jose": "^6.1.3", 46 | "jotai": "^2.15.1", 47 | "jszip": "^3.10.1", 48 | "lucide-react": "^0.552.0", 49 | "motion": "^12.23.24", 50 | "nanoid": "^5.1.6", 51 | "next": "16.0.10", 52 | "next-themes": "^0.4.6", 53 | "openai": "^6.8.1", 54 | "postgres": "^3.4.7", 55 | "radix-ui": "^1.4.3", 56 | "react": "19.2.1", 57 | "react-dom": "19.2.1", 58 | "react-resizable-panels": "^3.0.6", 59 | "resend": "^6.4.1", 60 | "server-only": "^0.0.1", 61 | "sonner": "^2.0.7", 62 | "tailwind-merge": "^3.3.1", 63 | "v0-sdk": "^0.15.1", 64 | "vaul": "^1.1.2", 65 | "workflow": "4.0.1-beta.17", 66 | "zod": "^4.1.12" 67 | }, 68 | "devDependencies": { 69 | "@inquirer/prompts": "^8.0.1", 70 | "@playwright/test": "^1.57.0", 71 | "@tailwindcss/postcss": "^4", 72 | "@types/node": "^24", 73 | "@types/react": "^19", 74 | "@types/react-dom": "^19", 75 | "drizzle-kit": "^0.31.6", 76 | "knip": "^5.70.1", 77 | "prettier": "^3.7.3", 78 | "tailwindcss": "^4", 79 | "tsx": "^4.19.0", 80 | "tw-animate-css": "^1.4.0", 81 | "typescript": "^5", 82 | "ultracite": "6.3.0" 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /components/overlays/confirm-overlay.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { AlertTriangleIcon } from "lucide-react"; 4 | import { cn } from "@/lib/utils"; 5 | import { Overlay } from "./overlay"; 6 | import { useOverlay } from "./overlay-provider"; 7 | import type { OverlayAction, OverlayActionVariant } from "./types"; 8 | 9 | type ConfirmOverlayProps = { 10 | overlayId: string; 11 | /** Title of the confirmation dialog */ 12 | title?: string; 13 | /** Description/message to display */ 14 | message: string; 15 | /** Text for the confirm button */ 16 | confirmLabel?: string; 17 | /** Text for the cancel button */ 18 | cancelLabel?: string; 19 | /** Variant for the confirm button */ 20 | confirmVariant?: OverlayActionVariant; 21 | /** Whether the action is destructive (shows warning icon) */ 22 | destructive?: boolean; 23 | /** Callback when confirmed */ 24 | onConfirm: () => void | Promise; 25 | /** Callback when cancelled */ 26 | onCancel?: () => void; 27 | }; 28 | 29 | /** 30 | * A reusable confirmation overlay. 31 | * 32 | * @example 33 | * ```tsx 34 | * const { open } = useOverlay(); 35 | * 36 | * open(ConfirmOverlay, { 37 | * title: "Delete Item", 38 | * message: "Are you sure you want to delete this item? This action cannot be undone.", 39 | * confirmLabel: "Delete", 40 | * confirmVariant: "destructive", 41 | * destructive: true, 42 | * onConfirm: async () => { 43 | * await deleteItem(id); 44 | * }, 45 | * }); 46 | * ``` 47 | */ 48 | export function ConfirmOverlay({ 49 | overlayId, 50 | title = "Confirm", 51 | message, 52 | confirmLabel = "Confirm", 53 | cancelLabel = "Cancel", 54 | confirmVariant = "default", 55 | destructive = false, 56 | onConfirm, 57 | onCancel, 58 | }: ConfirmOverlayProps) { 59 | const { pop } = useOverlay(); 60 | 61 | const handleCancel = () => { 62 | onCancel?.(); 63 | pop(); 64 | }; 65 | 66 | const handleConfirm = async () => { 67 | await onConfirm(); 68 | pop(); 69 | }; 70 | 71 | const actions: OverlayAction[] = [ 72 | { 73 | label: cancelLabel, 74 | variant: "outline", 75 | onClick: handleCancel, 76 | }, 77 | { 78 | label: confirmLabel, 79 | variant: destructive ? "destructive" : confirmVariant, 80 | onClick: handleConfirm, 81 | }, 82 | ]; 83 | 84 | return ( 85 | 86 |
87 | {destructive && ( 88 |
89 | 90 |
91 | )} 92 |

95 | {message} 96 |

97 |
98 |
99 | ); 100 | } 101 | -------------------------------------------------------------------------------- /plugins/ai-gateway/steps/generate-image.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { 4 | createGateway, 5 | experimental_generateImage as generateImage, 6 | } from "ai"; 7 | import { fetchCredentials } from "@/lib/credential-fetcher"; 8 | import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; 9 | import { getErrorMessageAsync } from "@/lib/utils"; 10 | import type { AiGatewayCredentials } from "../credentials"; 11 | 12 | type GenerateImageResult = 13 | | { success: true; base64: string } 14 | | { success: false; error: string }; 15 | 16 | export type GenerateImageCoreInput = { 17 | imageModel?: string; 18 | imagePrompt?: string; 19 | }; 20 | 21 | export type GenerateImageInput = StepInput & 22 | GenerateImageCoreInput & { 23 | integrationId?: string; 24 | }; 25 | 26 | /** 27 | * Core logic - portable between app and export 28 | */ 29 | async function stepHandler( 30 | input: GenerateImageCoreInput, 31 | credentials: AiGatewayCredentials 32 | ): Promise { 33 | const apiKey = credentials.AI_GATEWAY_API_KEY; 34 | 35 | if (!apiKey) { 36 | return { 37 | success: false, 38 | error: 39 | "AI_GATEWAY_API_KEY is not configured. Please add it in Project Integrations.", 40 | }; 41 | } 42 | 43 | const modelId = input.imageModel || "google/imagen-4.0-generate-001"; 44 | const promptText = input.imagePrompt || ""; 45 | 46 | if (!promptText || promptText.trim() === "") { 47 | return { 48 | success: false, 49 | error: "Prompt is required for image generation", 50 | }; 51 | } 52 | 53 | try { 54 | const gateway = createGateway({ 55 | apiKey, 56 | }); 57 | const result = await generateImage({ 58 | // biome-ignore lint/suspicious/noExplicitAny: AI gateway model ID is dynamic 59 | model: gateway.imageModel(modelId as any), 60 | prompt: promptText, 61 | size: "1024x1024", 62 | }); 63 | 64 | if (!result.image) { 65 | return { 66 | success: false, 67 | error: "Failed to generate image: No image returned", 68 | }; 69 | } 70 | 71 | const base64 = result.image.base64; 72 | 73 | return { success: true, base64 }; 74 | } catch (error) { 75 | const message = await getErrorMessageAsync(error); 76 | return { 77 | success: false, 78 | error: `Image generation failed: ${message}`, 79 | }; 80 | } 81 | } 82 | 83 | /** 84 | * App entry point - fetches credentials and wraps with logging 85 | */ 86 | export async function generateImageStep( 87 | input: GenerateImageInput 88 | ): Promise { 89 | "use step"; 90 | 91 | const credentials = input.integrationId 92 | ? await fetchCredentials(input.integrationId) 93 | : {}; 94 | 95 | return withStepLogging(input, () => stepHandler(input, credentials)); 96 | } 97 | generateImageStep.maxRetries = 0; 98 | 99 | export const _integrationType = "ai-gateway"; 100 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata, Viewport } from "next"; 2 | import "./globals.css"; 3 | import { Analytics } from "@vercel/analytics/react"; 4 | import { SpeedInsights } from "@vercel/speed-insights/next"; 5 | import { ReactFlowProvider } from "@xyflow/react"; 6 | import { Provider } from "jotai"; 7 | import { type ReactNode, Suspense } from "react"; 8 | import { AuthProvider } from "@/components/auth/provider"; 9 | import { GitHubStarsLoader } from "@/components/github-stars-loader"; 10 | import { GitHubStarsProvider } from "@/components/github-stars-provider"; 11 | import { GlobalModals } from "@/components/global-modals"; 12 | import { OverlayProvider } from "@/components/overlays/overlay-provider"; 13 | import { ThemeProvider } from "@/components/theme-provider"; 14 | import { Toaster } from "@/components/ui/sonner"; 15 | import { PersistentCanvas } from "@/components/workflow/persistent-canvas"; 16 | import { mono, sans } from "@/lib/fonts"; 17 | import { cn } from "@/lib/utils"; 18 | 19 | export const metadata: Metadata = { 20 | title: "AI Workflow Builder - Visual Workflow Automation", 21 | description: 22 | "Build powerful AI-driven workflow automations with a visual, node-based editor. Built with Next.js and React Flow.", 23 | }; 24 | 25 | export const viewport: Viewport = { 26 | width: "device-width", 27 | initialScale: 1, 28 | maximumScale: 1, 29 | userScalable: false, 30 | viewportFit: "cover", 31 | }; 32 | 33 | type RootLayoutProps = { 34 | children: ReactNode; 35 | }; 36 | 37 | // Inner content wrapped by GitHubStarsProvider (used for both loading and loaded states) 38 | function LayoutContent({ children }: { children: ReactNode }) { 39 | return ( 40 | 41 | 42 |
{children}
43 |
44 | ); 45 | } 46 | 47 | const RootLayout = ({ children }: RootLayoutProps) => ( 48 | 49 | 50 | 56 | 57 | 58 | 59 | 62 | {children} 63 | 64 | } 65 | > 66 | 67 | {children} 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | ); 81 | 82 | export default RootLayout; 83 | -------------------------------------------------------------------------------- /plugins/superagent/steps/redact.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { fetchCredentials } from "@/lib/credential-fetcher"; 4 | import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; 5 | import { getErrorMessage } from "@/lib/utils"; 6 | import type { SuperagentCredentials } from "../credentials"; 7 | 8 | type RedactResult = { 9 | redactedText: string; 10 | reasoning?: string; 11 | }; 12 | 13 | export type SuperagentRedactCoreInput = { 14 | text: string; 15 | entities?: string[] | string; 16 | }; 17 | 18 | export type SuperagentRedactInput = StepInput & 19 | SuperagentRedactCoreInput & { 20 | integrationId?: string; 21 | }; 22 | 23 | /** 24 | * Core logic 25 | */ 26 | async function stepHandler( 27 | input: SuperagentRedactCoreInput, 28 | credentials: SuperagentCredentials 29 | ): Promise { 30 | const apiKey = credentials.SUPERAGENT_API_KEY; 31 | 32 | if (!apiKey) { 33 | throw new Error("Superagent API Key is not configured."); 34 | } 35 | 36 | try { 37 | const body: { text: string; entities?: string[] } = { 38 | text: input.text, 39 | }; 40 | 41 | if (input.entities) { 42 | let entitiesArray: string[]; 43 | 44 | if (typeof input.entities === "string") { 45 | entitiesArray = input.entities.split(",").map((e) => e.trim()); 46 | } else if (Array.isArray(input.entities)) { 47 | entitiesArray = input.entities.map((e) => String(e).trim()); 48 | } else { 49 | entitiesArray = []; 50 | } 51 | 52 | const validEntities = entitiesArray.filter((e) => e.length > 0); 53 | 54 | if (validEntities.length > 0) { 55 | body.entities = validEntities; 56 | } 57 | } 58 | 59 | const response = await fetch("https://app.superagent.sh/api/redact", { 60 | method: "POST", 61 | headers: { 62 | "Content-Type": "application/json", 63 | Authorization: `Bearer ${apiKey}`, 64 | }, 65 | body: JSON.stringify(body), 66 | }); 67 | 68 | if (!response.ok) { 69 | const error = await response.text(); 70 | throw new Error(`Redact API error: ${error}`); 71 | } 72 | 73 | const data = await response.json(); 74 | const choice = data.choices?.[0]; 75 | 76 | return { 77 | redactedText: choice?.message?.content || input.text, 78 | reasoning: choice?.message?.reasoning, 79 | }; 80 | } catch (error) { 81 | throw new Error(`Failed to redact text: ${getErrorMessage(error)}`); 82 | } 83 | } 84 | 85 | /** 86 | * Step entry point 87 | */ 88 | export async function superagentRedactStep( 89 | input: SuperagentRedactInput 90 | ): Promise { 91 | "use step"; 92 | 93 | const credentials = input.integrationId 94 | ? await fetchCredentials(input.integrationId) 95 | : {}; 96 | 97 | return withStepLogging(input, () => stepHandler(input, credentials)); 98 | } 99 | superagentRedactStep.maxRetries = 0; 100 | 101 | export const _integrationType = "superagent"; 102 | -------------------------------------------------------------------------------- /plugins/slack/steps/send-slack-message.ts: -------------------------------------------------------------------------------- 1 | import "server-only"; 2 | 3 | import { fetchCredentials } from "@/lib/credential-fetcher"; 4 | import { type StepInput, withStepLogging } from "@/lib/steps/step-handler"; 5 | import { getErrorMessage } from "@/lib/utils"; 6 | import type { SlackCredentials } from "../credentials"; 7 | 8 | const SLACK_API_URL = "https://slack.com/api"; 9 | 10 | type SlackPostMessageResponse = { 11 | ok: boolean; 12 | ts?: string; 13 | channel?: string; 14 | error?: string; 15 | }; 16 | 17 | type SendSlackMessageResult = 18 | | { success: true; ts: string; channel: string } 19 | | { success: false; error: string }; 20 | 21 | export type SendSlackMessageCoreInput = { 22 | slackChannel: string; 23 | slackMessage: string; 24 | }; 25 | 26 | export type SendSlackMessageInput = StepInput & 27 | SendSlackMessageCoreInput & { 28 | integrationId?: string; 29 | }; 30 | 31 | /** 32 | * Core logic - portable between app and export 33 | */ 34 | async function stepHandler( 35 | input: SendSlackMessageCoreInput, 36 | credentials: SlackCredentials 37 | ): Promise { 38 | const apiKey = credentials.SLACK_API_KEY; 39 | 40 | if (!apiKey) { 41 | return { 42 | success: false, 43 | error: 44 | "SLACK_API_KEY is not configured. Please add it in Project Integrations.", 45 | }; 46 | } 47 | 48 | try { 49 | const response = await fetch(`${SLACK_API_URL}/chat.postMessage`, { 50 | method: "POST", 51 | headers: { 52 | "Content-Type": "application/json", 53 | Authorization: `Bearer ${apiKey}`, 54 | }, 55 | body: JSON.stringify({ 56 | channel: input.slackChannel, 57 | text: input.slackMessage, 58 | }), 59 | }); 60 | 61 | if (!response.ok) { 62 | return { 63 | success: false, 64 | error: `HTTP ${response.status}: Failed to send Slack message`, 65 | }; 66 | } 67 | 68 | const result = (await response.json()) as SlackPostMessageResponse; 69 | 70 | if (!result.ok) { 71 | return { 72 | success: false, 73 | error: result.error || "Failed to send Slack message", 74 | }; 75 | } 76 | 77 | return { 78 | success: true, 79 | ts: result.ts || "", 80 | channel: result.channel || "", 81 | }; 82 | } catch (error) { 83 | return { 84 | success: false, 85 | error: `Failed to send Slack message: ${getErrorMessage(error)}`, 86 | }; 87 | } 88 | } 89 | 90 | /** 91 | * App entry point - fetches credentials and wraps with logging 92 | */ 93 | export async function sendSlackMessageStep( 94 | input: SendSlackMessageInput 95 | ): Promise { 96 | "use step"; 97 | 98 | const credentials = input.integrationId 99 | ? await fetchCredentials(input.integrationId) 100 | : {}; 101 | 102 | return withStepLogging(input, () => stepHandler(input, credentials)); 103 | } 104 | sendSlackMessageStep.maxRetries = 0; 105 | 106 | export const _integrationType = "slack"; 107 | -------------------------------------------------------------------------------- /components/overlays/overlay-footer.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Loader2 } from "lucide-react"; 4 | import { Button } from "@/components/ui/button"; 5 | import { cn } from "@/lib/utils"; 6 | import type { OverlayAction, OverlayFooterProps } from "./types"; 7 | 8 | /** 9 | * Render a single action button 10 | */ 11 | function ActionButton({ action }: { action: OverlayAction }) { 12 | return ( 13 | 21 | ); 22 | } 23 | 24 | /** 25 | * Standardized footer component for overlays. 26 | * Renders action buttons in a consistent layout. 27 | */ 28 | export function OverlayFooter({ 29 | actions, 30 | className, 31 | children, 32 | }: OverlayFooterProps) { 33 | // If children are provided, render them directly 34 | if (children) { 35 | return ( 36 |
42 | {children} 43 |
44 | ); 45 | } 46 | 47 | // If no actions, render nothing 48 | if (!actions || actions.length === 0) { 49 | return null; 50 | } 51 | 52 | // Ghost buttons go on the left (additional actions like Delete) 53 | const leftActions = actions.filter((a) => a.variant === "ghost"); 54 | 55 | // Right side: secondary (outline) then primary (default/destructive) 56 | const rightSecondary = actions.filter( 57 | (a) => a.variant === "outline" || a.variant === "secondary" 58 | ); 59 | const rightPrimary = actions.filter( 60 | (a) => !a.variant || a.variant === "default" || a.variant === "destructive" 61 | ); 62 | 63 | const hasLeftActions = leftActions.length > 0; 64 | const hasRightActions = rightSecondary.length > 0 || rightPrimary.length > 0; 65 | 66 | return ( 67 |
76 | {/* Ghost actions on the left */} 77 | {hasLeftActions && ( 78 |
79 | {leftActions.map((action) => ( 80 | 81 | ))} 82 |
83 | )} 84 | 85 | {/* Secondary + Primary actions on the right: [secondary] [primary] */} 86 | {hasRightActions && ( 87 |
88 | {rightSecondary.map((action) => ( 89 | 90 | ))} 91 | {rightPrimary.map((action) => ( 92 | 93 | ))} 94 |
95 | )} 96 |
97 | ); 98 | } 99 | --------------------------------------------------------------------------------