Thank you for your interest in contributing to the Workflow Builder project! We're excited to have you here and appreciate your help in making this platform better for everyone.
- Ways to Contribute
- Getting Started
- Code of Conduct
- Development Setup
- Pull Request Process
- Adding a New Integration
- Plugin Development Guide
- Testing Guidelines
- Need Help?
There are many ways to contribute to Workflow Builder:
- Report bugs: Found something broken? Let us know!
- Suggest features: Have an idea? We'd love to hear it
- Improve documentation: Help make our docs clearer
- Add integrations: Expand the platform with new service integrations
- Fix issues: Pick up an issue from our backlog
- Improve code quality: Refactoring, tests, performance improvements
- Fork the repository on GitHub
- Clone your fork locally:
git clone https://github.com/your-username/workflow-builder-template.git cd workflow-builder-template - Install dependencies:
pnpm install
- Set up your environment:
- Copy
.env.exampleto.env.local - Add required environment variables (see below)
- Copy
- Run the development server:
pnpm dev
We're committed to providing a welcoming and inclusive environment for all contributors. Please:
- Be respectful and considerate in your interactions
- Welcome newcomers and help them get started
- Focus on what's best for the community
- Show empathy towards other community members
- Node.js 18+
- pnpm (our package manager of choice)
- PostgreSQL (for database integrations)
Required variables for development:
# Database
DATABASE_URL=postgres://localhost:5432/workflow
# Authentication
BETTER_AUTH_SECRET=your-auth-secret-here # Generate with: openssl rand -base64 32
# Credentials Encryption
INTEGRATION_ENCRYPTION_KEY=your-64-character-hex-string # Generate with: openssl rand -hex 32
# App URLs
NEXT_PUBLIC_APP_URL=http://localhost:3000Optional OAuth providers (configure at least one for authentication):
# GitHub
GITHUB_CLIENT_ID=
GITHUB_CLIENT_SECRET=
NEXT_PUBLIC_GITHUB_CLIENT_ID=
# Google
GOOGLE_CLIENT_ID=
GOOGLE_CLIENT_SECRET=
NEXT_PUBLIC_GOOGLE_CLIENT_ID=See .env.example for the complete list of available environment variables.
-
Create a new branch for your changes:
git checkout -b feature/your-feature-name
-
Make your changes and test thoroughly
-
Run quality checks:
pnpm type-check # TypeScript validation pnpm fix # Auto-fix linting issues pnpm build # Ensure it builds successfully
-
Commit your changes:
git add . git commit -m "feat: add new feature"
-
Push to your fork and create a pull request
- ✅ All tests pass
- ✅ Code follows the project's style guidelines (
pnpm fix) - ✅ TypeScript compiles without errors (
pnpm type-check) - ✅ Your changes are well-documented
- ✅ You've tested your changes thoroughly
- Clear title: Use conventional commit format (e.g.,
feat:,fix:,docs:) - Detailed description: Explain what and why, not just how
- Screenshots: Include for UI changes
- Breaking changes: Clearly document any breaking changes
- Link issues: Reference related issues (e.g., "Fixes #123")
All contributions go through a review process to ensure quality, security, and alignment with the project's goals. Our team reviews each submission with care, focusing on:
- Security: Protecting user data and system integrity
- User value: Ensuring the contribution benefits our users
- Code quality: Maintaining high standards for maintainability
- Compatibility: Ensuring it works well with existing features
We do our best to review submissions promptly, though please understand that not every contribution can be merged. We may provide feedback for improvements or, in some cases, decline contributions that don't align with the project's direction. We appreciate every contribution and will always explain our reasoning if changes are requested or a PR cannot be accepted.
The Workflow Builder uses a modular plugin-based system that makes adding integrations straightforward and organized. Each integration lives in its own directory with all its components self-contained.
Adding an integration requires:
- Create a plugin directory with modular files
- Add your integration type to the database schema
- Import your plugin
- Run database migrations
- Test your integration
That's it! The system handles registration and discovery automatically.
# 1. Create your plugin directory structure
mkdir -p plugins/my-integration/steps/my-action
mkdir -p plugins/my-integration/codegen
# 2. Copy template files
cp plugins/_template/icon.tsx.txt plugins/my-integration/icon.tsx
cp plugins/_template/settings.tsx.txt plugins/my-integration/settings.tsx
cp plugins/_template/test.ts.txt plugins/my-integration/test.ts
cp plugins/_template/steps/action/step.ts.txt plugins/my-integration/steps/my-action/step.ts
cp plugins/_template/steps/action/config.tsx.txt plugins/my-integration/steps/my-action/config.tsx
cp plugins/_template/codegen/action.ts.txt plugins/my-integration/codegen/my-action.ts
cp plugins/_template/index.tsx.txt plugins/my-integration/index.tsx
# 3. Fill in the templates with your integration details
# 4. Add your integration type to lib/db/integrations.ts
# 5. Generate and apply database migration
pnpm db:generate
pnpm db:push
# 6. Test it!
pnpm devNow let's go through each step in detail.
The plugin system uses a modular file structure where each integration is self-contained:
plugins/my-integration/
├── icon.tsx # Icon component (optional if using Lucide)
├── settings.tsx # Settings UI for credential configuration
├── test.ts # Connection test function
├── steps/ # Action implementations
│ └── my-action/
│ ├── step.ts # Server-side step function
│ └── config.tsx # Client-side UI for configuring the action
├── codegen/ # Export templates for standalone workflows
│ └── my-action.ts # Code generation template
└── index.tsx # Plugin definition (ties everything together)
Key Benefits:
- Modular: Each concern is in its own file
- Organized: All integration code in one directory
- Scalable: Easy to add new actions
- Self-contained: No scattered files across the codebase
- Discoverable: Automatically detected by the system
mkdir -p plugins/my-integration/steps/send-message
mkdir -p plugins/my-integration/codegenFile: plugins/my-integration/icon.tsx
export function MyIntegrationIcon({ className }: { className?: string }) {
return (
<svg
aria-label="My Integration"
className={className}
fill="currentColor"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<title>My Integration</title>
<path d="M12 2L2 12h3v8h6v-6h2v6h6v-8h3L12 2z" />
</svg>
);
}OR use a Lucide icon (skip this file entirely if using Lucide).
File: plugins/my-integration/settings.tsx
This component is displayed when users configure your integration in settings:
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
export function MyIntegrationSettings({
apiKey,
hasKey,
onApiKeyChange,
config,
onConfigChange,
}: {
apiKey: string;
hasKey?: boolean;
onApiKeyChange: (key: string) => void;
showCard?: boolean;
config?: Record<string, string>;
onConfigChange?: (key: string, value: string) => void;
}) {
return (
<div className="space-y-4">
<div className="space-y-2">
<Label className="ml-1" htmlFor="myIntegrationApiKey">
API Key
</Label>
<Input
className="bg-background"
id="myIntegrationApiKey"
onChange={(e) => onApiKeyChange(e.target.value)}
placeholder={hasKey ? "API key is configured" : "Enter your API key"}
type="password"
value={apiKey}
/>
<p className="text-muted-foreground text-sm">
Get your API key from{" "}
<a
className="text-primary underline"
href="https://my-integration.com/api-keys"
rel="noopener noreferrer"
target="_blank"
>
My Integration Dashboard
</a>
.
</p>
</div>
</div>
);
}File: plugins/my-integration/test.ts
This function validates that credentials work:
export async function testMyIntegration(credentials: Record<string, string>) {
try {
const apiKey = credentials.MY_INTEGRATION_API_KEY;
if (!apiKey) {
return {
success: false,
error: "MY_INTEGRATION_API_KEY is required",
};
}
const response = await fetch("https://api.my-integration.com/test", {
headers: {
Authorization: `Bearer ${apiKey}`,
},
});
if (response.ok) {
return { success: true };
}
const error = await response.text();
return { success: false, error };
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : String(error),
};
}
}File: plugins/my-integration/steps/send-message/step.ts
This runs on the server during workflow execution. Steps use the withStepLogging wrapper to automatically log execution for the workflow builder UI:
import "server-only";
import { fetchCredentials } from "@/lib/credential-fetcher";
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
import { getErrorMessage } from "@/lib/utils";
type SendMessageResult =
| { success: true; id: string; url: string }
| { success: false; error: string };
// Extend StepInput to get automatic logging context
export type SendMessageInput = StepInput & {
integrationId?: string;
message: string;
channel: string;
};
/**
* Send message logic - separated for clarity and testability
*/
async function sendMessage(input: SendMessageInput): Promise<SendMessageResult> {
const credentials = input.integrationId
? await fetchCredentials(input.integrationId)
: {};
const apiKey = credentials.MY_INTEGRATION_API_KEY;
if (!apiKey) {
return {
success: false,
error:
"MY_INTEGRATION_API_KEY is not configured. Please add it in Project Integrations.",
};
}
try {
const response = await fetch("https://api.my-integration.com/messages", {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
message: input.message,
channel: input.channel,
}),
});
if (!response.ok) {
throw new Error(`API request failed: ${response.statusText}`);
}
const result = await response.json();
return {
success: true,
id: result.id,
url: result.url,
};
} catch (error) {
return {
success: false,
error: `Failed to send message: ${getErrorMessage(error)}`,
};
}
}
/**
* Send Message Step
* Sends a message using My Integration API
*/
export async function sendMessageStep(
input: SendMessageInput
): Promise<SendMessageResult> {
"use step";
return withStepLogging(input, () => sendMessage(input));
}Key Points:
- Extend
StepInput: Your input type should extendStepInputto include the optional_contextfor logging - Separate logic function: Keep the actual logic in a separate function for clarity and testability
- Wrap with
withStepLogging: The step function just wraps the logic withwithStepLogging(input, () => logic(input)) - Return success/error objects: Steps should return
{ success: true, ... }or{ success: false, error: "..." }
File: plugins/my-integration/steps/send-message/config.tsx
This is the UI for configuring the action in the workflow builder:
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { TemplateBadgeInput } from "@/components/ui/template-badge-input";
/**
* Send Message Config Fields Component
* UI for configuring the send message action
*/
export function SendMessageConfigFields({
config,
onUpdateConfig,
disabled,
}: {
config: Record<string, unknown>;
onUpdateConfig: (key: string, value: unknown) => void;
disabled?: boolean;
}) {
return (
<>
<div className="space-y-2">
<Label htmlFor="message">Message</Label>
<TemplateBadgeInput
disabled={disabled}
id="message"
onChange={(value) => onUpdateConfig("message", value)}
placeholder="Enter message or use {{NodeName.field}}"
value={(config?.message as string) || ""}
/>
</div>
<div className="space-y-2">
<Label htmlFor="channel">Channel</Label>
<Input
disabled={disabled}
id="channel"
onChange={(e) => onUpdateConfig("channel", e.target.value)}
placeholder="#general"
value={(config?.channel as string) || ""}
/>
</div>
</>
);
}File: plugins/my-integration/codegen/send-message.ts
This template is used when users export/download their workflow as standalone code:
/**
* Code generation template for Send Message action
* Used when exporting workflows to standalone Next.js projects
*/
export const sendMessageCodegenTemplate = `
export async function sendMessageStep(input: {
message: string;
channel: string;
}) {
"use step";
const apiKey = process.env.MY_INTEGRATION_API_KEY;
if (!apiKey) {
throw new Error('MY_INTEGRATION_API_KEY environment variable is required');
}
const response = await fetch('https://api.my-integration.com/messages', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': \`Bearer \${apiKey}\`,
},
body: JSON.stringify({
message: input.message,
channel: input.channel,
}),
});
if (!response.ok) {
throw new Error(\`API request failed: \${response.statusText}\`);
}
const result = await response.json();
return {
id: result.id,
url: result.url,
success: true,
};
}`;File: plugins/my-integration/index.tsx
This ties everything together:
import { MessageSquare } from "lucide-react";
import type { IntegrationPlugin } from "../registry";
import { registerIntegration } from "../registry";
import { sendMessageCodegenTemplate } from "./codegen/send-message";
import { MyIntegrationIcon } from "./icon";
import { MyIntegrationSettings } from "./settings";
import { SendMessageConfigFields } from "./steps/send-message/config";
import { testMyIntegration } from "./test";
const myIntegrationPlugin: IntegrationPlugin = {
type: "my-integration", // Must match type in lib/db/integrations.ts
label: "My Integration",
description: "Send messages and create records",
icon: {
type: "svg", // or "lucide" for Lucide icons
value: "MyIntegrationIcon",
svgComponent: MyIntegrationIcon,
},
// For Lucide icons, use:
// icon: { type: "lucide", value: "MessageSquare" },
settingsComponent: MyIntegrationSettings,
formFields: [
{
id: "myIntegrationApiKey",
label: "API Key",
type: "password",
placeholder: "sk_...",
configKey: "myIntegrationApiKey",
helpText: "Get your API key from ",
helpLink: {
text: "my-integration.com",
url: "https://my-integration.com/api-keys",
},
},
],
credentialMapping: (config) => {
const creds: Record<string, string> = {};
if (config.myIntegrationApiKey) {
creds.MY_INTEGRATION_API_KEY = String(config.myIntegrationApiKey);
}
return creds;
},
testConfig: {
testFunction: testMyIntegration,
},
actions: [
{
id: "Send Message",
label: "Send Message",
description: "Send a message to a channel",
category: "My Integration",
icon: MessageSquare,
stepFunction: "sendMessageStep",
stepImportPath: "send-message",
configFields: SendMessageConfigFields,
codegenTemplate: sendMessageCodegenTemplate,
},
// Add more actions as needed
],
};
// Auto-register on import
registerIntegration(myIntegrationPlugin);
export default myIntegrationPlugin;Edit: lib/db/integrations.ts
export type IntegrationType =
| "resend"
| "linear"
| "slack"
| "database"
| "ai-gateway"
| "firecrawl"
| "my-integration"; // Add this
export type IntegrationConfig = {
// ... existing config
myIntegrationApiKey?: string; // Add this
};pnpm db:generate
pnpm db:pushpnpm type-check
pnpm fix
pnpm devNavigate to the app and:
- Go to Settings → Integrations
- Add your new integration
- Configure it with test credentials
- Click "Test Connection"
- Create a workflow using your new action
- Test that it executes correctly!
- Connection Test: Test function validates credentials correctly
- Workflow Execution: Action executes successfully in a workflow
- Error Handling: Invalid credentials show helpful error messages
- Code Generation: Export produces valid standalone code
- Template Variables:
{{NodeName.field}}references work correctly - Edge Cases: Test with missing/invalid inputs
# Type check
pnpm type-check
# Lint and auto-fix
pnpm fix
# Build
pnpm build
# Development server
pnpm devSee plugins/firecrawl/ for a complete, production-ready example with:
- Custom SVG icon
- Multiple actions (Scrape, Search)
- Separate step/config files for each action
- Full TypeScript types
// In index.tsx
import { Zap } from "lucide-react";
const plugin: IntegrationPlugin = {
// ...
icon: {
type: "lucide",
value: "Zap", // No icon.tsx file needed
},
// ...
};actions: [
{
id: "Send Message",
label: "Send Message",
description: "Send a message",
category: "My Integration",
icon: MessageSquare,
stepFunction: "sendMessageStep",
stepImportPath: "send-message",
configFields: SendMessageConfigFields,
codegenTemplate: sendMessageCodegenTemplate,
},
{
id: "Create Record",
label: "Create Record",
description: "Create a new record",
category: "My Integration",
icon: Plus,
stepFunction: "createRecordStep",
stepImportPath: "create-record",
configFields: CreateRecordConfigFields,
codegenTemplate: createRecordCodegenTemplate,
},
],Use TemplateBadgeInput to allow users to reference outputs from other workflow nodes:
<TemplateBadgeInput
value={config?.message || ""}
onChange={(value) => onUpdateConfig("message", value)}
placeholder="Enter message or use {{NodeName.field}}"
/>Steps follow a consistent structure with logging:
import "server-only";
import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
type MyResult = { success: true; data: string } | { success: false; error: string };
export type MyInput = StepInput & {
field1: string;
};
// 1. Logic function (no "use step" needed)
async function myLogic(input: MyInput): Promise<MyResult> {
try {
const response = await fetch(/* ... */);
if (!response.ok) {
throw new Error(`API error: ${response.statusText}`);
}
const result = await response.json();
return { success: true, data: result };
} catch (error) {
return {
success: false,
error: `Failed to execute: ${getErrorMessage(error)}`,
};
}
}
// 2. Step wrapper (has "use step", wraps with logging)
export async function myStep(input: MyInput): Promise<MyResult> {
"use step";
return withStepLogging(input, () => myLogic(input));
}credentialMapping: (config) => {
const creds: Record<string, string> = {};
if (config.apiKey) {
creds.MY_INTEGRATION_API_KEY = String(config.apiKey);
}
if (config.workspaceId) {
creds.MY_INTEGRATION_WORKSPACE_ID = String(config.workspaceId);
}
return creds;
},If you run into issues:
- Check the template files in
plugins/_template/ - Look at existing integrations like
plugins/firecrawl/ - Ensure all file names match your imports
- Run
pnpm type-checkto catch type errors - Run
pnpm fixto auto-fix linting issues - Open an issue on GitHub or start a discussion
Adding an integration requires:
- Create plugin directory with modular files (6-8 files)
- Update database schema (add your type)
- Run migration
- Test thoroughly
Each integration is self-contained in one organized directory, making it easy to develop, test, and maintain. Happy building! 🚀
- GitHub Issues: For bug reports and feature requests
- GitHub Discussions: For questions and community support
- Pull Requests: For code contributions
Thank you for contributing to Workflow Builder! Your efforts help make this platform better for everyone. 💙