Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions app/api/ai/generate/route.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { streamText } from "ai";
import { NextResponse } from "next/server";
import { auth } from "@/lib/auth";
import { generateAIActionPrompts } from "@/plugins";

// Simple type for operations
type Operation = {
Expand Down Expand Up @@ -130,7 +131,9 @@ async function processOperationStream(
);
}

const system = `You are a workflow automation expert. Generate a workflow based on the user's description.
function getSystemPrompt(): string {
const pluginActionPrompts = generateAIActionPrompts();
return `You are a workflow automation expert. Generate a workflow based on the user's description.

CRITICAL: Output your workflow as INDIVIDUAL OPERATIONS, one per line in JSONL format.
Each line must be a complete, separate JSON object.
Expand Down Expand Up @@ -185,19 +188,13 @@ Trigger types:
- Webhook: {"triggerType": "Webhook", "webhookPath": "/webhooks/name", ...}
- Schedule: {"triggerType": "Schedule", "scheduleCron": "0 9 * * *", ...}

Action types:
- Send Email: {"actionType": "Send Email", "emailTo": "user@example.com", "emailSubject": "Subject", "emailBody": "Body"}
- Send Slack Message: {"actionType": "Send Slack Message", "slackChannel": "#general", "slackMessage": "Message"}
- Create Ticket: {"actionType": "Create Ticket", "ticketTitle": "Title", "ticketDescription": "Description", "ticketPriority": "2"}
System action types (built-in):
- Database Query: {"actionType": "Database Query", "dbQuery": "SELECT * FROM table", "dbTable": "table"}
- HTTP Request: {"actionType": "HTTP Request", "httpMethod": "POST", "endpoint": "https://api.example.com", "httpHeaders": "{}", "httpBody": "{}"}
- Generate Text: {"actionType": "Generate Text", "aiModel": "meta/llama-4-scout", "aiFormat": "text", "aiPrompt": "Your prompt here"}
- Generate Image: {"actionType": "Generate Image", "imageModel": "google/imagen-4.0-generate", "imagePrompt": "Image description"}
- Scrape: {"actionType": "Scrape", "url": "https://example.com"}
- Search: {"actionType": "Search", "query": "search query", "limit": 10}
- Condition: {"actionType": "Condition", "condition": "{{@nodeId:Label.field}} === 'value'"}
- Create Chat (v0): {"actionType": "Create Chat", "message": "Create a line graph showing DAU over time", "system": "You are an expert coder"} - Use v0 for generating UI components, visualizations (charts, graphs, dashboards), landing pages, or any React/Next.js code. PREFER v0 over Generate Text/Image for any visual output like charts, graphs, or UI.
- Send Message (v0): {"actionType": "Send Message", "chatId": "{{@nodeId:Label.chatId}}", "message": "Add dark mode"} - Use this to continue a v0 chat conversation

Plugin action types (from integrations):
${pluginActionPrompts}

CRITICAL ABOUT CONDITION NODES:
- Condition nodes evaluate a boolean expression
Expand Down Expand Up @@ -247,6 +244,7 @@ Example output (branching workflow with 250px vertical spacing):
{"op": "addEdge", "edge": {"id": "e2", "source": "trigger-1", "target": "branch-b", "type": "default"}}

REMEMBER: After adding all nodes, you MUST add edges to connect them! Every node should be reachable from the trigger.`;
}

export async function POST(request: Request) {
try {
Expand Down Expand Up @@ -328,7 +326,7 @@ Example: If user says "connect node A to node B", output:

const result = streamText({
model: "openai/gpt-5.1-instant",
system,
system: getSystemPrompt(),
prompt: userPrompt,
});

Expand Down
90 changes: 47 additions & 43 deletions app/api/workflows/[workflowId]/download/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { db } from "@/lib/db";
import { workflows } from "@/lib/db/schema";
import { generateWorkflowModule } from "@/lib/workflow-codegen";
import type { WorkflowEdge, WorkflowNode } from "@/lib/workflow-store";
import { getAllEnvVars, getDependenciesForActions } from "@/plugins";

// Path to the Next.js boilerplate directory
const BOILERPLATE_PATH = join(process.cwd(), "lib", "next-boilerplate");
Expand Down Expand Up @@ -137,35 +138,57 @@ export async function POST(request: Request) {

/**
* Get npm dependencies based on workflow nodes
* Uses the plugin registry to dynamically determine required dependencies
*/
function getIntegrationDependencies(
nodes: WorkflowNode[]
): Record<string, string> {
const deps: Record<string, string> = {};

for (const node of nodes) {
const actionType = node.data.config?.actionType as string;

if (actionType === "Send Email") {
deps.resend = "^6.4.0";
} else if (actionType === "Create Ticket" || actionType === "Find Issues") {
deps["@linear/sdk"] = "^63.2.0";
} else if (actionType === "Send Slack Message") {
deps["@slack/web-api"] = "^7.12.0";
} else if (
actionType === "Generate Text" ||
actionType === "Generate Image"
) {
deps.ai = "^5.0.86";
deps.openai = "^6.8.0";
deps["@google/genai"] = "^1.28.0";
deps.zod = "^4.1.12";
} else if (actionType === "Scrape" || actionType === "Search") {
deps["@mendable/firecrawl-js"] = "^4.6.2";
// Collect all action types used in the workflow
const actionTypes = nodes
.filter((node) => node.data.type === "action")
.map((node) => node.data.config?.actionType as string)
.filter(Boolean);

// Get dependencies from plugin registry
return getDependenciesForActions(actionTypes);
}

/**
* Generate .env.example content based on registered integrations
*/
function generateEnvExample(): string {
const lines = ["# Add your environment variables here\n"];

// Add system integration env vars
lines.push("# For database integrations");
lines.push("DATABASE_URL=your_database_url\n");

// Add plugin env vars from registry
const envVars = getAllEnvVars();
const groupedByPrefix: Record<
string,
Array<{ name: string; description: string }>
> = {};

for (const envVar of envVars) {
const prefix = envVar.name.split("_")[0];
if (!groupedByPrefix[prefix]) {
groupedByPrefix[prefix] = [];
}
groupedByPrefix[prefix].push(envVar);
}

for (const [prefix, vars] of Object.entries(groupedByPrefix)) {
lines.push(
`# For ${prefix.charAt(0) + prefix.slice(1).toLowerCase()} integration`
);
for (const v of vars) {
lines.push(`${v.name}=your_${v.name.toLowerCase()}`);
}
lines.push("");
}

return deps;
return lines.join("\n");
}

/**
Expand Down Expand Up @@ -303,27 +326,8 @@ vercel deploy
For more information, visit the [Workflow documentation](https://workflow.is).
`;

// Add .env.example file
allFiles[".env.example"] = `# Add your environment variables here
# For Resend email integration
RESEND_API_KEY=your_resend_api_key

# For Linear integration
LINEAR_API_KEY=your_linear_api_key

# For Slack integration
SLACK_BOT_TOKEN=your_slack_bot_token

# For AI integrations
OPENAI_API_KEY=your_openai_api_key
GOOGLE_AI_API_KEY=your_google_ai_api_key

# For database integrations
DATABASE_URL=your_database_url

# For Firecrawl integration
FIRECRAWL_API_KEY=your_firecrawl_api_key
`;
// Add .env.example file (dynamically generated from plugin registry)
allFiles[".env.example"] = generateEnvExample();

return NextResponse.json({
success: true,
Expand Down
44 changes: 42 additions & 2 deletions lib/step-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,42 +25,82 @@ export type StepImporter = {
* These imports are statically analyzable by the bundler
*/
export const PLUGIN_STEP_IMPORTERS: Record<string, StepImporter> = {
"ai-gateway/generate-text": {
importer: () => import("@/plugins/ai-gateway/steps/generate-text/step"),
stepFunction: "generateTextStep",
},
"Generate Text": {
importer: () => import("@/plugins/ai-gateway/steps/generate-text/step"),
stepFunction: "generateTextStep",
},
"ai-gateway/generate-image": {
importer: () => import("@/plugins/ai-gateway/steps/generate-image/step"),
stepFunction: "generateImageStep",
},
"Generate Image": {
importer: () => import("@/plugins/ai-gateway/steps/generate-image/step"),
stepFunction: "generateImageStep",
},
Scrape: {
"firecrawl/scrape": {
importer: () => import("@/plugins/firecrawl/steps/scrape/step"),
stepFunction: "firecrawlScrapeStep",
},
Search: {
"Scrape URL": {
importer: () => import("@/plugins/firecrawl/steps/scrape/step"),
stepFunction: "firecrawlScrapeStep",
},
"firecrawl/search": {
importer: () => import("@/plugins/firecrawl/steps/search/step"),
stepFunction: "firecrawlSearchStep",
},
"Search Web": {
importer: () => import("@/plugins/firecrawl/steps/search/step"),
stepFunction: "firecrawlSearchStep",
},
"linear/create-ticket": {
importer: () => import("@/plugins/linear/steps/create-ticket/step"),
stepFunction: "createTicketStep",
},
"Create Ticket": {
importer: () => import("@/plugins/linear/steps/create-ticket/step"),
stepFunction: "createTicketStep",
},
"linear/find-issues": {
importer: () => import("@/plugins/linear/steps/find-issues/step"),
stepFunction: "findIssuesStep",
},
"Find Issues": {
importer: () => import("@/plugins/linear/steps/find-issues/step"),
stepFunction: "findIssuesStep",
},
"resend/send-email": {
importer: () => import("@/plugins/resend/steps/send-email/step"),
stepFunction: "sendEmailStep",
},
"Send Email": {
importer: () => import("@/plugins/resend/steps/send-email/step"),
stepFunction: "sendEmailStep",
},
"slack/send-message": {
importer: () => import("@/plugins/slack/steps/send-slack-message/step"),
stepFunction: "sendSlackMessageStep",
},
"Send Slack Message": {
importer: () => import("@/plugins/slack/steps/send-slack-message/step"),
stepFunction: "sendSlackMessageStep",
},
"v0/create-chat": {
importer: () => import("@/plugins/v0/steps/create-chat/step"),
stepFunction: "createChatStep",
},
"Create Chat": {
importer: () => import("@/plugins/v0/steps/create-chat/step"),
stepFunction: "createChatStep",
},
"v0/send-message": {
importer: () => import("@/plugins/v0/steps/send-message/step"),
stepFunction: "sendMessageStep",
},
"Send Message": {
importer: () => import("@/plugins/v0/steps/send-message/step"),
stepFunction: "sendMessageStep",
Expand Down
10 changes: 6 additions & 4 deletions lib/workflow-executor.workflow.ts
Original file line number Diff line number Diff line change
Expand Up @@ -233,14 +233,14 @@ async function executeActionStep(input: {

return {
success: false,
error: `Step function "${stepImporter.stepFunction}" not found in module`,
error: `Step function "${stepImporter.stepFunction}" not found in module for action "${actionType}". Check that the plugin exports the correct function name.`,
};
}

// Fallback for unknown action types
return {
success: false,
error: `Unknown action type: ${actionType}`,
error: `Unknown action type: "${actionType}". This action is not registered in the plugin system. Available system actions: ${Object.keys(SYSTEM_ACTIONS).join(", ")}.`,
};
}

Expand Down Expand Up @@ -532,7 +532,9 @@ export async function executeWorkflow(input: WorkflowExecutionInput) {
const errorResult = stepResult as { success: false; error?: string };
result = {
success: false,
error: errorResult.error || "Step execution failed",
error:
errorResult.error ||
`Step "${actionType}" in node "${node.data.label || node.id}" failed without a specific error message.`,
};
} else {
result = {
Expand All @@ -544,7 +546,7 @@ export async function executeWorkflow(input: WorkflowExecutionInput) {
console.log("[Workflow Executor] Unknown node type:", node.data.type);
result = {
success: false,
error: `Unknown node type: ${node.data.type}`,
error: `Unknown node type "${node.data.type}" in node "${node.data.label || node.id}". Expected "trigger" or "action".`,
};
}

Expand Down
19 changes: 17 additions & 2 deletions plugins/ai-gateway/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,22 @@ const aiGatewayPlugin: IntegrationPlugin = {
},
},

dependencies: {
ai: "^5.0.86",
openai: "^6.8.0",
"@google/genai": "^1.28.0",
zod: "^4.1.12",
},

envVars: [
{ name: "AI_GATEWAY_API_KEY", description: "AI Gateway API key" },
{ name: "OPENAI_API_KEY", description: "OpenAI API key (alternative)" },
{ name: "GOOGLE_AI_API_KEY", description: "Google AI API key (for Gemini)" },
],

actions: [
{
id: "Generate Text",
slug: "generate-text",
label: "Generate Text",
description: "Generate text using AI models",
category: "AI Gateway",
Expand All @@ -64,9 +77,10 @@ const aiGatewayPlugin: IntegrationPlugin = {
stepImportPath: "generate-text",
configFields: GenerateTextConfigFields,
codegenTemplate: generateTextCodegenTemplate,
aiPrompt: `{"actionType": "ai-gateway/generate-text", "aiModel": "meta/llama-4-scout", "aiFormat": "text", "aiPrompt": "Your prompt here"}`,
},
{
id: "Generate Image",
slug: "generate-image",
label: "Generate Image",
description: "Generate images using AI models",
category: "AI Gateway",
Expand All @@ -75,6 +89,7 @@ const aiGatewayPlugin: IntegrationPlugin = {
stepImportPath: "generate-image",
configFields: GenerateImageConfigFields,
codegenTemplate: generateImageCodegenTemplate,
aiPrompt: `{"actionType": "ai-gateway/generate-image", "imageModel": "google/imagen-4.0-generate", "imagePrompt": "Image description"}`,
},
],
};
Expand Down
14 changes: 12 additions & 2 deletions plugins/firecrawl/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,17 @@ const firecrawlPlugin: IntegrationPlugin = {
},
},

dependencies: {
"@mendable/firecrawl-js": "^4.6.2",
},

envVars: [
{ name: "FIRECRAWL_API_KEY", description: "Firecrawl API key" },
],

actions: [
{
id: "Scrape",
slug: "scrape",
label: "Scrape URL",
description: "Scrape content from a URL",
category: "Firecrawl",
Expand All @@ -62,9 +70,10 @@ const firecrawlPlugin: IntegrationPlugin = {
stepImportPath: "scrape",
configFields: ScrapeConfigFields,
codegenTemplate: scrapeCodegenTemplate,
aiPrompt: `{"actionType": "firecrawl/scrape", "url": "https://example.com"}`,
},
{
id: "Search",
slug: "search",
label: "Search Web",
description: "Search the web with Firecrawl",
category: "Firecrawl",
Expand All @@ -73,6 +82,7 @@ const firecrawlPlugin: IntegrationPlugin = {
stepImportPath: "search",
configFields: SearchConfigFields,
codegenTemplate: searchCodegenTemplate,
aiPrompt: `{"actionType": "firecrawl/search", "query": "search query", "limit": 10}`,
},
],
};
Expand Down
Loading