diff --git a/README.md b/README.md
index 6dd549e7..27fbabf4 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,8 @@ Visit [http://localhost:3000](http://localhost:3000) to get started.
- **fal.ai**: Generate Image, Generate Video, Upscale Image, Remove Background, Image to Image
- **Firecrawl**: Scrape URL, Search Web
- **GitHub**: Create Issue, List Issues, Get Issue, Update Issue
+- **Instantly**: Create Lead, Get Lead, List Leads, Update Lead, Delete Lead, Update Lead Interest Status, Add Lead to Campaign, Create Campaign, Get Campaign, List Campaigns, Update Campaign, Delete Campaign, Activate Campaign, Pause Campaign, List Accounts, Get Account, Pause Account, Resume Account, Enable Warmup, Disable Warmup
+- **LeadMagic**: Find Email, Validate Email, Search Profile, Find Mobile, Find Role, B2B Profile Email, Search Company, Get Technographics, Get Company Funding
- **Linear**: Create Ticket, Find Issues
- **Perplexity**: Search Web, Ask Question, Research Topic
- **Resend**: Send Email
diff --git a/components/workflow/node-config-panel.tsx b/components/workflow/node-config-panel.tsx
index 96a019ed..42bfdba9 100644
--- a/components/workflow/node-config-panel.tsx
+++ b/components/workflow/node-config-panel.tsx
@@ -406,9 +406,13 @@ export const PanelInner = () => {
if (selectedNode) {
let newConfig = { ...selectedNode.data.config, [key]: value };
- // When action type changes, clear the integrationId since it may not be valid for the new action
- if (key === "actionType" && selectedNode.data.config?.integrationId) {
- newConfig = { ...newConfig, integrationId: undefined };
+ // When action type changes, clear ALL old config fields to prevent data pollution
+ // Only keep actionType and integrationId (which will be cleared below if needed)
+ if (key === "actionType") {
+ newConfig = {
+ actionType: value,
+ integrationId: undefined, // Will be auto-selected if needed
+ };
}
updateNodeData({ id: selectedNode.id, data: { config: newConfig } });
diff --git a/docs/neon-setup-guide.md b/docs/neon-setup-guide.md
new file mode 100644
index 00000000..6dc1fa1d
--- /dev/null
+++ b/docs/neon-setup-guide.md
@@ -0,0 +1,154 @@
+# Neon Database Setup Guide
+
+This guide documents how we set up Neon.tech PostgreSQL for the Workflow Builder project.
+
+## What is Neon?
+
+Neon is a serverless PostgreSQL database that offers:
+- Free tier with 0.5 GB storage
+- Auto-suspend after 5 minutes of inactivity (saves costs)
+- Instant database branching (like git for databases)
+- Connection pooling built-in
+
+## Step-by-Step Setup
+
+### 1. Create a Neon Account
+
+1. Go to [https://console.neon.tech/](https://console.neon.tech/)
+2. Sign up using GitHub, Google, or email
+3. After signing in, you'll land on your dashboard
+
+### 2. Create a New Project
+
+When you first sign up, Neon automatically creates a project for you. If you need a new one:
+
+1. Click "New Project" in the dashboard
+2. Choose a project name (e.g., "workflow-builder")
+3. Select a region closest to you (e.g., "US East")
+4. Click "Create Project"
+
+### 3. Get Your Connection String
+
+1. From your project dashboard, click the "Connect" button (top right)
+2. A dialog appears with connection details
+3. Make sure "Connection pooling" is enabled (toggle should be ON)
+4. Click "Show password" to reveal the full connection string
+5. Copy the connection string - it looks like:
+ ```
+ postgresql://username:password@ep-xxx-xxx.region.aws.neon.tech/neondb?sslmode=require
+ ```
+
+### 4. Configure Environment Variables
+
+Create a `.env.local` file in your project root (this file is gitignored):
+
+```env
+# Database - Neon PostgreSQL
+DATABASE_URL=postgresql://your-connection-string-here
+
+# Authentication (generate with: openssl rand -base64 32)
+BETTER_AUTH_SECRET=your-generated-secret
+
+# Credentials Encryption (generate with: openssl rand -hex 32)
+INTEGRATION_ENCRYPTION_KEY=your-64-char-hex-string
+
+# App URLs
+BETTER_AUTH_URL=http://localhost:3000
+NEXT_PUBLIC_APP_URL=http://localhost:3000
+```
+
+Also copy to `.env` for Drizzle migrations:
+```bash
+cp .env.local .env
+```
+
+### 5. Push Database Schema
+
+Run the following command to create all tables:
+
+```bash
+pnpm db:push
+```
+
+This uses Drizzle ORM to push the schema defined in `lib/db/schema.ts` to your database.
+
+### 6. Start the Development Server
+
+```bash
+pnpm dev
+```
+
+Visit [http://localhost:3000](http://localhost:3000) - you should see the workflow builder!
+
+## How the Database Works in This Project
+
+### Technology Stack
+
+- **Drizzle ORM**: Type-safe database toolkit for TypeScript
+- **PostgreSQL**: The database engine (hosted on Neon)
+- **Better Auth**: Authentication library that stores users/sessions in the database
+
+### Database Schema
+
+The schema is defined in `lib/db/schema.ts` and includes:
+
+| Table | Purpose |
+|-------|---------|
+| `user` | User accounts (email, name, password hash) |
+| `session` | Active login sessions |
+| `account` | OAuth provider connections |
+| `verification` | Email verification tokens |
+| `workflows` | Workflow definitions (nodes, edges, config) |
+| `workflow_executions` | Workflow run history |
+| `workflow_execution_logs` | Detailed step-by-step logs |
+| `api_keys` | User API keys for programmatic access |
+
+### Database Commands
+
+```bash
+# Push schema changes to database (no migration files)
+pnpm db:push
+
+# Generate migration files (for production)
+pnpm db:generate
+
+# Open Drizzle Studio (visual database browser)
+pnpm db:studio
+```
+
+### Connection Flow
+
+1. App reads `DATABASE_URL` from environment
+2. `lib/db/index.ts` creates a connection pool using `postgres` driver
+3. Drizzle ORM wraps the connection for type-safe queries
+4. API routes and server components use `db` to query data
+
+## Troubleshooting
+
+### "database does not exist" Error
+
+Make sure your `DATABASE_URL` points to the correct database name (usually `neondb`).
+
+### Connection Timeout
+
+Neon auto-suspends after 5 min idle. First request after suspend has ~500ms cold start. This is normal.
+
+### Schema Push Fails
+
+1. Check `DATABASE_URL` is set correctly in both `.env` and `.env.local`
+2. Ensure your Neon project is active (not suspended)
+3. Try running `pnpm db:push` again
+
+## Security Notes
+
+- Never commit `.env` or `.env.local` files
+- These files are already in `.gitignore`
+- Rotate `BETTER_AUTH_SECRET` and `INTEGRATION_ENCRYPTION_KEY` if exposed
+- Neon connection strings include passwords - keep them secret!
+
+## Useful Links
+
+- [Neon Documentation](https://neon.tech/docs)
+- [Drizzle ORM Docs](https://orm.drizzle.team/)
+- [Better Auth Docs](https://better-auth.com/)
+
diff --git a/plugins/index.ts b/plugins/index.ts
index 495c6e33..61284bdc 100644
--- a/plugins/index.ts
+++ b/plugins/index.ts
@@ -20,6 +20,8 @@ import "./clerk";
import "./fal";
import "./firecrawl";
import "./github";
+import "./instantly";
+import "./leadmagic";
import "./linear";
import "./perplexity";
import "./resend";
diff --git a/plugins/instantly/credentials.ts b/plugins/instantly/credentials.ts
new file mode 100644
index 00000000..0de497bc
--- /dev/null
+++ b/plugins/instantly/credentials.ts
@@ -0,0 +1,4 @@
+export type InstantlyCredentials = {
+ INSTANTLY_API_KEY?: string;
+};
+
diff --git a/plugins/instantly/icon.tsx b/plugins/instantly/icon.tsx
new file mode 100644
index 00000000..f23e1234
--- /dev/null
+++ b/plugins/instantly/icon.tsx
@@ -0,0 +1,11 @@
+/* eslint-disable @next/next/no-img-element */
+export function InstantlyIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
+
diff --git a/plugins/instantly/index.ts b/plugins/instantly/index.ts
new file mode 100644
index 00000000..398fb926
--- /dev/null
+++ b/plugins/instantly/index.ts
@@ -0,0 +1,601 @@
+import type { IntegrationPlugin } from "../registry";
+import { registerIntegration } from "../registry";
+import { InstantlyIcon } from "./icon";
+
+const instantlyPlugin: IntegrationPlugin = {
+ type: "instantly",
+ label: "Instantly",
+ description: "Cold email outreach and lead management platform",
+
+ icon: InstantlyIcon,
+
+ formFields: [
+ {
+ id: "apiKey",
+ label: "API Key",
+ type: "password",
+ placeholder: "Your Instantly API key",
+ configKey: "apiKey",
+ envVar: "INSTANTLY_API_KEY",
+ helpText: "Get your API key from ",
+ helpLink: {
+ text: "app.instantly.ai/app/settings/integrations",
+ url: "https://app.instantly.ai/app/settings/integrations",
+ },
+ },
+ ],
+
+ testConfig: {
+ getTestFunction: async () => {
+ const { testInstantly } = await import("./test");
+ return testInstantly;
+ },
+ },
+
+ actions: [
+ // Lead Operations
+ {
+ slug: "create-lead",
+ label: "Create Lead",
+ description: "Create a new lead in a campaign",
+ category: "Instantly",
+ stepFunction: "createLeadStep",
+ stepImportPath: "create-lead",
+ outputFields: [
+ { field: "id", description: "Lead ID" },
+ { field: "email", description: "Lead email" },
+ ],
+ configFields: [
+ {
+ key: "campaignId",
+ label: "Campaign ID",
+ type: "template-input",
+ placeholder: "Campaign UUID or {{NodeName.campaignId}}",
+ required: true,
+ },
+ {
+ key: "email",
+ label: "Email",
+ type: "template-input",
+ placeholder: "lead@example.com or {{NodeName.email}}",
+ required: true,
+ },
+ {
+ key: "firstName",
+ label: "First Name",
+ type: "template-input",
+ placeholder: "John or {{NodeName.firstName}}",
+ },
+ {
+ key: "lastName",
+ label: "Last Name",
+ type: "template-input",
+ placeholder: "Doe or {{NodeName.lastName}}",
+ },
+ {
+ key: "companyName",
+ label: "Company Name",
+ type: "template-input",
+ placeholder: "Acme Inc or {{NodeName.companyName}}",
+ },
+ {
+ type: "group",
+ label: "Additional Fields",
+ fields: [
+ {
+ key: "personalization",
+ label: "Personalization",
+ type: "template-textarea",
+ placeholder: "Custom personalization message",
+ rows: 3,
+ },
+ {
+ key: "phone",
+ label: "Phone",
+ type: "template-input",
+ placeholder: "+1234567890",
+ },
+ {
+ key: "website",
+ label: "Website",
+ type: "template-input",
+ placeholder: "https://example.com",
+ },
+ {
+ key: "customVariables",
+ label: "Custom Variables (JSON)",
+ type: "template-textarea",
+ placeholder: '{"key": "value"}',
+ rows: 3,
+ },
+ ],
+ },
+ ],
+ },
+ {
+ slug: "get-lead",
+ label: "Get Lead",
+ description: "Get a lead by ID",
+ category: "Instantly",
+ stepFunction: "getLeadStep",
+ stepImportPath: "get-lead",
+ outputFields: [
+ { field: "id", description: "Lead ID" },
+ { field: "email", description: "Lead email" },
+ { field: "firstName", description: "First name" },
+ { field: "lastName", description: "Last name" },
+ { field: "status", description: "Lead status" },
+ ],
+ configFields: [
+ {
+ key: "leadId",
+ label: "Lead ID",
+ type: "template-input",
+ placeholder: "Lead UUID or {{NodeName.leadId}}",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "list-leads",
+ label: "List Leads",
+ description: "List leads with optional filters",
+ category: "Instantly",
+ stepFunction: "listLeadsStep",
+ stepImportPath: "list-leads",
+ outputFields: [
+ { field: "leads", description: "Array of leads" },
+ { field: "total", description: "Total count" },
+ ],
+ configFields: [
+ {
+ key: "campaignId",
+ label: "Campaign ID",
+ type: "template-input",
+ placeholder: "Campaign UUID (optional)",
+ },
+ {
+ key: "email",
+ label: "Email Filter",
+ type: "template-input",
+ placeholder: "Filter by email",
+ },
+ {
+ key: "limit",
+ label: "Limit",
+ type: "number",
+ defaultValue: "100",
+ min: 1,
+ },
+ ],
+ },
+ {
+ slug: "update-lead",
+ label: "Update Lead",
+ description: "Update an existing lead",
+ category: "Instantly",
+ stepFunction: "updateLeadStep",
+ stepImportPath: "update-lead",
+ outputFields: [
+ { field: "id", description: "Lead ID" },
+ { field: "email", description: "Lead email" },
+ ],
+ configFields: [
+ {
+ key: "leadId",
+ label: "Lead ID",
+ type: "template-input",
+ placeholder: "Lead UUID or {{NodeName.leadId}}",
+ required: true,
+ },
+ {
+ key: "firstName",
+ label: "First Name",
+ type: "template-input",
+ placeholder: "John or {{NodeName.firstName}}",
+ },
+ {
+ key: "lastName",
+ label: "Last Name",
+ type: "template-input",
+ placeholder: "Doe or {{NodeName.lastName}}",
+ },
+ {
+ key: "companyName",
+ label: "Company Name",
+ type: "template-input",
+ placeholder: "Acme Inc",
+ },
+ {
+ key: "customVariables",
+ label: "Custom Variables (JSON)",
+ type: "template-textarea",
+ placeholder: '{"key": "value"}',
+ rows: 3,
+ },
+ ],
+ },
+ {
+ slug: "delete-lead",
+ label: "Delete Lead",
+ description: "Delete a lead by ID",
+ category: "Instantly",
+ stepFunction: "deleteLeadStep",
+ stepImportPath: "delete-lead",
+ outputFields: [{ field: "deleted", description: "Deletion status" }],
+ configFields: [
+ {
+ key: "leadId",
+ label: "Lead ID",
+ type: "template-input",
+ placeholder: "Lead UUID or {{NodeName.leadId}}",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "update-lead-status",
+ label: "Update Lead Interest Status",
+ description: "Update the interest status of a lead",
+ category: "Instantly",
+ stepFunction: "updateLeadStatusStep",
+ stepImportPath: "update-lead-status",
+ outputFields: [
+ { field: "id", description: "Lead ID" },
+ { field: "status", description: "New status" },
+ ],
+ configFields: [
+ {
+ key: "campaignId",
+ label: "Campaign ID",
+ type: "template-input",
+ placeholder: "Campaign UUID",
+ required: true,
+ },
+ {
+ key: "email",
+ label: "Lead Email",
+ type: "template-input",
+ placeholder: "lead@example.com",
+ required: true,
+ },
+ {
+ key: "status",
+ label: "Interest Status",
+ type: "select",
+ options: [
+ { value: "interested", label: "Interested" },
+ { value: "not_interested", label: "Not Interested" },
+ { value: "meeting_booked", label: "Meeting Booked" },
+ { value: "meeting_completed", label: "Meeting Completed" },
+ { value: "closed", label: "Closed" },
+ { value: "out_of_office", label: "Out of Office" },
+ { value: "wrong_person", label: "Wrong Person" },
+ ],
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "add-lead-to-campaign",
+ label: "Add Lead to Campaign",
+ description: "Add an existing lead to a campaign",
+ category: "Instantly",
+ stepFunction: "addLeadToCampaignStep",
+ stepImportPath: "add-lead-to-campaign",
+ outputFields: [
+ { field: "id", description: "Lead ID" },
+ { field: "campaignId", description: "Campaign ID" },
+ ],
+ configFields: [
+ {
+ key: "campaignId",
+ label: "Campaign ID",
+ type: "template-input",
+ placeholder: "Campaign UUID",
+ required: true,
+ },
+ {
+ key: "email",
+ label: "Lead Email",
+ type: "template-input",
+ placeholder: "lead@example.com",
+ required: true,
+ },
+ ],
+ },
+ // Campaign Operations
+ {
+ slug: "create-campaign",
+ label: "Create Campaign",
+ description: "Create a new email campaign",
+ category: "Instantly",
+ stepFunction: "createCampaignStep",
+ stepImportPath: "create-campaign",
+ outputFields: [
+ { field: "id", description: "Campaign ID" },
+ { field: "name", description: "Campaign name" },
+ ],
+ configFields: [
+ {
+ key: "name",
+ label: "Campaign Name",
+ type: "template-input",
+ placeholder: "My Campaign or {{NodeName.name}}",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "get-campaign",
+ label: "Get Campaign",
+ description: "Get a campaign by ID",
+ category: "Instantly",
+ stepFunction: "getCampaignStep",
+ stepImportPath: "get-campaign",
+ outputFields: [
+ { field: "id", description: "Campaign ID" },
+ { field: "name", description: "Campaign name" },
+ { field: "status", description: "Campaign status" },
+ ],
+ configFields: [
+ {
+ key: "campaignId",
+ label: "Campaign ID",
+ type: "template-input",
+ placeholder: "Campaign UUID or {{NodeName.campaignId}}",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "list-campaigns",
+ label: "List Campaigns",
+ description: "List all campaigns",
+ category: "Instantly",
+ stepFunction: "listCampaignsStep",
+ stepImportPath: "list-campaigns",
+ outputFields: [
+ { field: "campaigns", description: "Array of campaigns" },
+ { field: "total", description: "Total count" },
+ ],
+ configFields: [
+ {
+ key: "status",
+ label: "Status Filter",
+ type: "select",
+ options: [
+ { value: "all", label: "All" },
+ { value: "active", label: "Active" },
+ { value: "paused", label: "Paused" },
+ { value: "completed", label: "Completed" },
+ { value: "draft", label: "Draft" },
+ ],
+ },
+ {
+ key: "limit",
+ label: "Limit",
+ type: "number",
+ defaultValue: "100",
+ min: 1,
+ },
+ ],
+ },
+ {
+ slug: "update-campaign",
+ label: "Update Campaign",
+ description: "Update an existing campaign",
+ category: "Instantly",
+ stepFunction: "updateCampaignStep",
+ stepImportPath: "update-campaign",
+ outputFields: [
+ { field: "id", description: "Campaign ID" },
+ { field: "name", description: "Campaign name" },
+ ],
+ configFields: [
+ {
+ key: "campaignId",
+ label: "Campaign ID",
+ type: "template-input",
+ placeholder: "Campaign UUID",
+ required: true,
+ },
+ {
+ key: "name",
+ label: "Campaign Name",
+ type: "template-input",
+ placeholder: "New campaign name",
+ },
+ ],
+ },
+ {
+ slug: "delete-campaign",
+ label: "Delete Campaign",
+ description: "Delete a campaign by ID",
+ category: "Instantly",
+ stepFunction: "deleteCampaignStep",
+ stepImportPath: "delete-campaign",
+ outputFields: [{ field: "deleted", description: "Deletion status" }],
+ configFields: [
+ {
+ key: "campaignId",
+ label: "Campaign ID",
+ type: "template-input",
+ placeholder: "Campaign UUID",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "activate-campaign",
+ label: "Activate Campaign",
+ description: "Launch/activate a campaign",
+ category: "Instantly",
+ stepFunction: "activateCampaignStep",
+ stepImportPath: "activate-campaign",
+ outputFields: [
+ { field: "id", description: "Campaign ID" },
+ { field: "status", description: "Campaign status" },
+ ],
+ configFields: [
+ {
+ key: "campaignId",
+ label: "Campaign ID",
+ type: "template-input",
+ placeholder: "Campaign UUID",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "pause-campaign",
+ label: "Pause Campaign",
+ description: "Pause a running campaign",
+ category: "Instantly",
+ stepFunction: "pauseCampaignStep",
+ stepImportPath: "pause-campaign",
+ outputFields: [
+ { field: "id", description: "Campaign ID" },
+ { field: "status", description: "Campaign status" },
+ ],
+ configFields: [
+ {
+ key: "campaignId",
+ label: "Campaign ID",
+ type: "template-input",
+ placeholder: "Campaign UUID",
+ required: true,
+ },
+ ],
+ },
+ // Account Operations
+ {
+ slug: "list-accounts",
+ label: "List Accounts",
+ description: "List all email accounts",
+ category: "Instantly",
+ stepFunction: "listAccountsStep",
+ stepImportPath: "list-accounts",
+ outputFields: [
+ { field: "accounts", description: "Array of accounts" },
+ { field: "total", description: "Total count" },
+ ],
+ configFields: [
+ {
+ key: "limit",
+ label: "Limit",
+ type: "number",
+ defaultValue: "100",
+ min: 1,
+ },
+ ],
+ },
+ {
+ slug: "get-account",
+ label: "Get Account",
+ description: "Get an email account by email address",
+ category: "Instantly",
+ stepFunction: "getAccountStep",
+ stepImportPath: "get-account",
+ outputFields: [
+ { field: "email", description: "Account email" },
+ { field: "status", description: "Account status" },
+ { field: "warmupEnabled", description: "Warmup enabled status" },
+ ],
+ configFields: [
+ {
+ key: "email",
+ label: "Account Email",
+ type: "template-input",
+ placeholder: "account@example.com",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "pause-account",
+ label: "Pause Account",
+ description: "Pause an email account",
+ category: "Instantly",
+ stepFunction: "pauseAccountStep",
+ stepImportPath: "pause-account",
+ outputFields: [
+ { field: "email", description: "Account email" },
+ { field: "status", description: "Account status" },
+ ],
+ configFields: [
+ {
+ key: "email",
+ label: "Account Email",
+ type: "template-input",
+ placeholder: "account@example.com",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "resume-account",
+ label: "Resume Account",
+ description: "Resume a paused email account",
+ category: "Instantly",
+ stepFunction: "resumeAccountStep",
+ stepImportPath: "resume-account",
+ outputFields: [
+ { field: "email", description: "Account email" },
+ { field: "status", description: "Account status" },
+ ],
+ configFields: [
+ {
+ key: "email",
+ label: "Account Email",
+ type: "template-input",
+ placeholder: "account@example.com",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "enable-warmup",
+ label: "Enable Warmup",
+ description: "Enable warmup for email accounts",
+ category: "Instantly",
+ stepFunction: "enableWarmupStep",
+ stepImportPath: "enable-warmup",
+ outputFields: [{ field: "enabled", description: "Warmup enabled status" }],
+ configFields: [
+ {
+ key: "emails",
+ label: "Account Emails",
+ type: "template-textarea",
+ placeholder: "account1@example.com\naccount2@example.com",
+ rows: 4,
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "disable-warmup",
+ label: "Disable Warmup",
+ description: "Disable warmup for email accounts",
+ category: "Instantly",
+ stepFunction: "disableWarmupStep",
+ stepImportPath: "disable-warmup",
+ outputFields: [{ field: "disabled", description: "Warmup disabled status" }],
+ configFields: [
+ {
+ key: "emails",
+ label: "Account Emails",
+ type: "template-textarea",
+ placeholder: "account1@example.com\naccount2@example.com",
+ rows: 4,
+ required: true,
+ },
+ ],
+ },
+ ],
+};
+
+registerIntegration(instantlyPlugin);
+
+export default instantlyPlugin;
+
diff --git a/plugins/instantly/steps/activate-campaign.ts b/plugins/instantly/steps/activate-campaign.ts
new file mode 100644
index 00000000..0eda9039
--- /dev/null
+++ b/plugins/instantly/steps/activate-campaign.ts
@@ -0,0 +1,85 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type ActivateCampaignResult =
+ | { success: true; data: { id: string; status: string } }
+ | { success: false; error: { message: string } };
+
+export type ActivateCampaignCoreInput = {
+ campaignId: string;
+};
+
+export type ActivateCampaignInput = StepInput &
+ ActivateCampaignCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: ActivateCampaignCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.campaignId) {
+ return { success: false, error: { message: "Campaign ID is required" } };
+ }
+
+ try {
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/campaigns/${input.campaignId}/activate`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Campaign not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to activate campaign: ${response.status} - ${errorText}` },
+ };
+ }
+
+ return {
+ success: true,
+ data: {
+ id: input.campaignId,
+ status: "active",
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to activate campaign: ${message}` } };
+ }
+}
+
+export async function activateCampaignStep(
+ input: ActivateCampaignInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+activateCampaignStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/add-lead-to-campaign.ts b/plugins/instantly/steps/add-lead-to-campaign.ts
new file mode 100644
index 00000000..35de777f
--- /dev/null
+++ b/plugins/instantly/steps/add-lead-to-campaign.ts
@@ -0,0 +1,95 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type AddLeadToCampaignResult =
+ | { success: true; data: { id: string; campaignId: string } }
+ | { success: false; error: { message: string } };
+
+export type AddLeadToCampaignCoreInput = {
+ campaignId: string;
+ email: string;
+};
+
+export type AddLeadToCampaignInput = StepInput &
+ AddLeadToCampaignCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: AddLeadToCampaignCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.campaignId) {
+ return { success: false, error: { message: "Campaign ID is required" } };
+ }
+
+ if (!input.email) {
+ return { success: false, error: { message: "Email is required" } };
+ }
+
+ try {
+ // Add lead to campaign by creating a new lead entry
+ const response = await fetch(`${INSTANTLY_API_URL}/leads`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ campaign: input.campaignId,
+ email: input.email,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to add lead to campaign: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as { id: string };
+
+ return {
+ success: true,
+ data: {
+ id: responseData.id || input.email,
+ campaignId: input.campaignId,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return {
+ success: false,
+ error: { message: `Failed to add lead to campaign: ${message}` },
+ };
+ }
+}
+
+export async function addLeadToCampaignStep(
+ input: AddLeadToCampaignInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+addLeadToCampaignStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/create-campaign.ts b/plugins/instantly/steps/create-campaign.ts
new file mode 100644
index 00000000..9fcf4c29
--- /dev/null
+++ b/plugins/instantly/steps/create-campaign.ts
@@ -0,0 +1,110 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type CreateCampaignResult =
+ | { success: true; data: { id: string; name: string } }
+ | { success: false; error: { message: string } };
+
+export type CreateCampaignCoreInput = {
+ name: string;
+};
+
+export type CreateCampaignInput = StepInput &
+ CreateCampaignCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: CreateCampaignCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.name) {
+ return { success: false, error: { message: "Campaign name is required" } };
+ }
+
+ try {
+ // Default schedule: Monday-Friday, 9am-5pm in America/Chicago (Central Time)
+ // Note: Instantly API only accepts specific timezone values from their enum
+ const defaultSchedule = {
+ schedules: [
+ {
+ name: "Default Schedule",
+ timing: {
+ from: "09:00",
+ to: "17:00",
+ },
+ days: {
+ "0": false, // Sunday
+ "1": true, // Monday
+ "2": true, // Tuesday
+ "3": true, // Wednesday
+ "4": true, // Thursday
+ "5": true, // Friday
+ "6": false, // Saturday
+ },
+ timezone: "America/Chicago",
+ },
+ ],
+ };
+
+ const response = await fetch(`${INSTANTLY_API_URL}/campaigns`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ name: input.name,
+ campaign_schedule: defaultSchedule,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to create campaign: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as { id: string; name: string };
+
+ return {
+ success: true,
+ data: {
+ id: responseData.id,
+ name: responseData.name,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to create campaign: ${message}` } };
+ }
+}
+
+export async function createCampaignStep(
+ input: CreateCampaignInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+createCampaignStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/create-lead.ts b/plugins/instantly/steps/create-lead.ts
new file mode 100644
index 00000000..e17d5606
--- /dev/null
+++ b/plugins/instantly/steps/create-lead.ts
@@ -0,0 +1,116 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type CreateLeadResult =
+ | { success: true; data: { id: string; email: string } }
+ | { success: false; error: { message: string } };
+
+export type CreateLeadCoreInput = {
+ campaignId: string;
+ email: string;
+ firstName?: string;
+ lastName?: string;
+ companyName?: string;
+ personalization?: string;
+ phone?: string;
+ website?: string;
+ customVariables?: string;
+};
+
+export type CreateLeadInput = StepInput &
+ CreateLeadCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: CreateLeadCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.campaignId) {
+ return { success: false, error: { message: "Campaign ID is required" } };
+ }
+
+ if (!input.email) {
+ return { success: false, error: { message: "Email is required" } };
+ }
+
+ try {
+ let customVars: Record = {};
+ if (input.customVariables) {
+ try {
+ customVars = JSON.parse(input.customVariables);
+ } catch {
+ return { success: false, error: { message: "Invalid JSON in custom variables" } };
+ }
+ }
+
+ const leadData: Record = {
+ campaign: input.campaignId,
+ email: input.email,
+ ...(input.firstName && { first_name: input.firstName }),
+ ...(input.lastName && { last_name: input.lastName }),
+ ...(input.companyName && { company_name: input.companyName }),
+ ...(input.personalization && { personalization: input.personalization }),
+ ...(input.phone && { phone: input.phone }),
+ ...(input.website && { website: input.website }),
+ ...(Object.keys(customVars).length > 0 && { custom_variables: customVars }),
+ };
+
+ const response = await fetch(`${INSTANTLY_API_URL}/leads`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(leadData),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to create lead: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as { id: string; email: string };
+
+ return {
+ success: true,
+ data: {
+ id: responseData.id,
+ email: responseData.email,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to create lead: ${message}` } };
+ }
+}
+
+export async function createLeadStep(
+ input: CreateLeadInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+createLeadStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/delete-campaign.ts b/plugins/instantly/steps/delete-campaign.ts
new file mode 100644
index 00000000..7f1da41a
--- /dev/null
+++ b/plugins/instantly/steps/delete-campaign.ts
@@ -0,0 +1,82 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type DeleteCampaignResult =
+ | { success: true; data: { deleted: boolean } }
+ | { success: false; error: { message: string } };
+
+export type DeleteCampaignCoreInput = {
+ campaignId: string;
+};
+
+export type DeleteCampaignInput = StepInput &
+ DeleteCampaignCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: DeleteCampaignCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.campaignId) {
+ return { success: false, error: { message: "Campaign ID is required" } };
+ }
+
+ try {
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/campaigns/${input.campaignId}`,
+ {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Campaign not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to delete campaign: ${response.status} - ${errorText}` },
+ };
+ }
+
+ return {
+ success: true,
+ data: { deleted: true },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to delete campaign: ${message}` } };
+ }
+}
+
+export async function deleteCampaignStep(
+ input: DeleteCampaignInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+deleteCampaignStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/delete-lead.ts b/plugins/instantly/steps/delete-lead.ts
new file mode 100644
index 00000000..fe89e3b9
--- /dev/null
+++ b/plugins/instantly/steps/delete-lead.ts
@@ -0,0 +1,79 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type DeleteLeadResult =
+ | { success: true; data: { deleted: boolean } }
+ | { success: false; error: { message: string } };
+
+export type DeleteLeadCoreInput = {
+ leadId: string;
+};
+
+export type DeleteLeadInput = StepInput &
+ DeleteLeadCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: DeleteLeadCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.leadId) {
+ return { success: false, error: { message: "Lead ID is required" } };
+ }
+
+ try {
+ const response = await fetch(`${INSTANTLY_API_URL}/leads/${input.leadId}`, {
+ method: "DELETE",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Lead not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to delete lead: ${response.status} - ${errorText}` },
+ };
+ }
+
+ return {
+ success: true,
+ data: { deleted: true },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to delete lead: ${message}` } };
+ }
+}
+
+export async function deleteLeadStep(
+ input: DeleteLeadInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+deleteLeadStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/disable-warmup.ts b/plugins/instantly/steps/disable-warmup.ts
new file mode 100644
index 00000000..31aa4ed7
--- /dev/null
+++ b/plugins/instantly/steps/disable-warmup.ts
@@ -0,0 +1,93 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type DisableWarmupResult =
+ | { success: true; data: { disabled: boolean } }
+ | { success: false; error: { message: string } };
+
+export type DisableWarmupCoreInput = {
+ emails: string;
+};
+
+export type DisableWarmupInput = StepInput &
+ DisableWarmupCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: DisableWarmupCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.emails) {
+ return { success: false, error: { message: "At least one email is required" } };
+ }
+
+ try {
+ // Parse emails from newline-separated string
+ const emailList = input.emails
+ .split("\n")
+ .map((e) => e.trim())
+ .filter((e) => e.length > 0);
+
+ if (emailList.length === 0) {
+ return { success: false, error: { message: "At least one valid email is required" } };
+ }
+
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/accounts/warmup/disable`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ emails: emailList,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to disable warmup: ${response.status} - ${errorText}` },
+ };
+ }
+
+ return {
+ success: true,
+ data: { disabled: true },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to disable warmup: ${message}` } };
+ }
+}
+
+export async function disableWarmupStep(
+ input: DisableWarmupInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+disableWarmupStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/enable-warmup.ts b/plugins/instantly/steps/enable-warmup.ts
new file mode 100644
index 00000000..28071b97
--- /dev/null
+++ b/plugins/instantly/steps/enable-warmup.ts
@@ -0,0 +1,90 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type EnableWarmupResult =
+ | { success: true; data: { enabled: boolean } }
+ | { success: false; error: { message: string } };
+
+export type EnableWarmupCoreInput = {
+ emails: string;
+};
+
+export type EnableWarmupInput = StepInput &
+ EnableWarmupCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: EnableWarmupCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.emails) {
+ return { success: false, error: { message: "At least one email is required" } };
+ }
+
+ try {
+ // Parse emails from newline-separated string
+ const emailList = input.emails
+ .split("\n")
+ .map((e) => e.trim())
+ .filter((e) => e.length > 0);
+
+ if (emailList.length === 0) {
+ return { success: false, error: { message: "At least one valid email is required" } };
+ }
+
+ const response = await fetch(`${INSTANTLY_API_URL}/accounts/warmup/enable`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ emails: emailList,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to enable warmup: ${response.status} - ${errorText}` },
+ };
+ }
+
+ return {
+ success: true,
+ data: { enabled: true },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to enable warmup: ${message}` } };
+ }
+}
+
+export async function enableWarmupStep(
+ input: EnableWarmupInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+enableWarmupStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/get-account.ts b/plugins/instantly/steps/get-account.ts
new file mode 100644
index 00000000..d57a620c
--- /dev/null
+++ b/plugins/instantly/steps/get-account.ts
@@ -0,0 +1,93 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type GetAccountResult =
+ | { success: true; data: { email: string; status: string; warmupEnabled: boolean } }
+ | { success: false; error: { message: string } };
+
+export type GetAccountCoreInput = {
+ email: string;
+};
+
+export type GetAccountInput = StepInput &
+ GetAccountCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: GetAccountCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.email) {
+ return { success: false, error: { message: "Email is required" } };
+ }
+
+ try {
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/accounts/${encodeURIComponent(input.email)}`,
+ {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Account not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to get account: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as {
+ email: string;
+ status: string;
+ warmup_enabled?: boolean;
+ };
+
+ return {
+ success: true,
+ data: {
+ email: responseData.email,
+ status: responseData.status || "unknown",
+ warmupEnabled: responseData.warmup_enabled || false,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to get account: ${message}` } };
+ }
+}
+
+export async function getAccountStep(
+ input: GetAccountInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+getAccountStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/get-campaign.ts b/plugins/instantly/steps/get-campaign.ts
new file mode 100644
index 00000000..a3ad06dd
--- /dev/null
+++ b/plugins/instantly/steps/get-campaign.ts
@@ -0,0 +1,93 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type GetCampaignResult =
+ | { success: true; data: { id: string; name: string; status: string } }
+ | { success: false; error: { message: string } };
+
+export type GetCampaignCoreInput = {
+ campaignId: string;
+};
+
+export type GetCampaignInput = StepInput &
+ GetCampaignCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: GetCampaignCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.campaignId) {
+ return { success: false, error: { message: "Campaign ID is required" } };
+ }
+
+ try {
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/campaigns/${input.campaignId}`,
+ {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Campaign not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to get campaign: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as {
+ id: string;
+ name: string;
+ status: string;
+ };
+
+ return {
+ success: true,
+ data: {
+ id: responseData.id,
+ name: responseData.name,
+ status: responseData.status || "unknown",
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to get campaign: ${message}` } };
+ }
+}
+
+export async function getCampaignStep(
+ input: GetCampaignInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+getCampaignStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/get-lead.ts b/plugins/instantly/steps/get-lead.ts
new file mode 100644
index 00000000..df9fae80
--- /dev/null
+++ b/plugins/instantly/steps/get-lead.ts
@@ -0,0 +1,103 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type GetLeadResult =
+ | {
+ success: true;
+ data: {
+ id: string;
+ email: string;
+ firstName?: string;
+ lastName?: string;
+ status?: string;
+ };
+ }
+ | { success: false; error: { message: string } };
+
+export type GetLeadCoreInput = {
+ leadId: string;
+};
+
+export type GetLeadInput = StepInput &
+ GetLeadCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: GetLeadCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.leadId) {
+ return { success: false, error: { message: "Lead ID is required" } };
+ }
+
+ try {
+ const response = await fetch(`${INSTANTLY_API_URL}/leads/${input.leadId}`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Lead not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to get lead: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as {
+ id: string;
+ email: string;
+ first_name?: string;
+ last_name?: string;
+ lead_status?: string;
+ };
+
+ return {
+ success: true,
+ data: {
+ id: responseData.id,
+ email: responseData.email,
+ firstName: responseData.first_name,
+ lastName: responseData.last_name,
+ status: responseData.lead_status,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to get lead: ${message}` } };
+ }
+}
+
+export async function getLeadStep(
+ input: GetLeadInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+getLeadStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/list-accounts.ts b/plugins/instantly/steps/list-accounts.ts
new file mode 100644
index 00000000..7557e20f
--- /dev/null
+++ b/plugins/instantly/steps/list-accounts.ts
@@ -0,0 +1,103 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type Account = {
+ email: string;
+ status: string;
+ warmupEnabled: boolean;
+};
+
+type ListAccountsResult =
+ | { success: true; data: { accounts: Account[]; total: number } }
+ | { success: false; error: { message: string } };
+
+export type ListAccountsCoreInput = {
+ limit?: number;
+};
+
+export type ListAccountsInput = StepInput &
+ ListAccountsCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: ListAccountsCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ try {
+ const params = new URLSearchParams();
+ params.append("limit", String(input.limit || 100));
+
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/accounts?${params.toString()}`,
+ {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to list accounts: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as {
+ items: Array<{
+ email: string;
+ status: string;
+ warmup_enabled?: boolean;
+ }>;
+ total_count?: number;
+ };
+
+ const accounts: Account[] = responseData.items.map((item) => ({
+ email: item.email,
+ status: item.status || "unknown",
+ warmupEnabled: item.warmup_enabled || false,
+ }));
+
+ return {
+ success: true,
+ data: {
+ accounts,
+ total: responseData.total_count || accounts.length,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to list accounts: ${message}` } };
+ }
+}
+
+export async function listAccountsStep(
+ input: ListAccountsInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+listAccountsStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/list-campaigns.ts b/plugins/instantly/steps/list-campaigns.ts
new file mode 100644
index 00000000..695d290e
--- /dev/null
+++ b/plugins/instantly/steps/list-campaigns.ts
@@ -0,0 +1,114 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type Campaign = {
+ id: string;
+ name: string;
+ status: string;
+};
+
+type ListCampaignsResult =
+ | { success: true; data: { campaigns: Campaign[]; total: number } }
+ | { success: false; error: { message: string } };
+
+export type ListCampaignsCoreInput = {
+ status?: string;
+ limit?: number;
+};
+
+export type ListCampaignsInput = StepInput &
+ ListCampaignsCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: ListCampaignsCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ try {
+ const params = new URLSearchParams();
+ params.append("limit", String(input.limit || 100));
+
+ // Status values: 0 = Draft, 1 = Active, 2 = Paused, 3 = Completed
+ if (input.status && input.status !== "all" && input.status.trim() !== "") {
+ const statusMap: Record = {
+ draft: "0",
+ active: "1",
+ paused: "2",
+ completed: "3",
+ };
+ const statusValue = statusMap[input.status.toLowerCase()];
+ if (statusValue) {
+ params.append("status", statusValue);
+ }
+ }
+
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/campaigns?${params.toString()}`,
+ {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ }
+ );
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to list campaigns: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as {
+ items: Array<{ id: string; name: string; status: string }>;
+ total_count?: number;
+ };
+
+ const campaigns: Campaign[] = responseData.items.map((item) => ({
+ id: item.id,
+ name: item.name,
+ status: item.status || "unknown",
+ }));
+
+ return {
+ success: true,
+ data: {
+ campaigns,
+ total: responseData.total_count || campaigns.length,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to list campaigns: ${message}` } };
+ }
+}
+
+export async function listCampaignsStep(
+ input: ListCampaignsInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+listCampaignsStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/list-leads.ts b/plugins/instantly/steps/list-leads.ts
new file mode 100644
index 00000000..7b066481
--- /dev/null
+++ b/plugins/instantly/steps/list-leads.ts
@@ -0,0 +1,118 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type Lead = {
+ id: string;
+ email: string;
+ firstName?: string;
+ lastName?: string;
+ status?: string;
+};
+
+type ListLeadsResult =
+ | { success: true; data: { leads: Lead[]; total: number } }
+ | { success: false; error: { message: string } };
+
+export type ListLeadsCoreInput = {
+ campaignId?: string;
+ email?: string;
+ limit?: number;
+};
+
+export type ListLeadsInput = StepInput &
+ ListLeadsCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: ListLeadsCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ try {
+ const requestBody: Record = {
+ limit: input.limit || 100,
+ };
+
+ if (input.campaignId) {
+ requestBody.campaign_id = input.campaignId;
+ }
+
+ if (input.email) {
+ requestBody.email = input.email;
+ }
+
+ const response = await fetch(`${INSTANTLY_API_URL}/leads/list`, {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(requestBody),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to list leads: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as {
+ items: Array<{
+ id: string;
+ email: string;
+ first_name?: string;
+ last_name?: string;
+ lead_status?: string;
+ }>;
+ total_count?: number;
+ };
+
+ const leads: Lead[] = responseData.items.map((item) => ({
+ id: item.id,
+ email: item.email,
+ firstName: item.first_name,
+ lastName: item.last_name,
+ status: item.lead_status,
+ }));
+
+ return {
+ success: true,
+ data: {
+ leads,
+ total: responseData.total_count || leads.length,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to list leads: ${message}` } };
+ }
+}
+
+export async function listLeadsStep(
+ input: ListLeadsInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+listLeadsStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/pause-account.ts b/plugins/instantly/steps/pause-account.ts
new file mode 100644
index 00000000..bf7fc957
--- /dev/null
+++ b/plugins/instantly/steps/pause-account.ts
@@ -0,0 +1,85 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type PauseAccountResult =
+ | { success: true; data: { email: string; status: string } }
+ | { success: false; error: { message: string } };
+
+export type PauseAccountCoreInput = {
+ email: string;
+};
+
+export type PauseAccountInput = StepInput &
+ PauseAccountCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: PauseAccountCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.email) {
+ return { success: false, error: { message: "Email is required" } };
+ }
+
+ try {
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/accounts/${encodeURIComponent(input.email)}/pause`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Account not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to pause account: ${response.status} - ${errorText}` },
+ };
+ }
+
+ return {
+ success: true,
+ data: {
+ email: input.email,
+ status: "paused",
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to pause account: ${message}` } };
+ }
+}
+
+export async function pauseAccountStep(
+ input: PauseAccountInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+pauseAccountStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/pause-campaign.ts b/plugins/instantly/steps/pause-campaign.ts
new file mode 100644
index 00000000..9d1f6673
--- /dev/null
+++ b/plugins/instantly/steps/pause-campaign.ts
@@ -0,0 +1,85 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type PauseCampaignResult =
+ | { success: true; data: { id: string; status: string } }
+ | { success: false; error: { message: string } };
+
+export type PauseCampaignCoreInput = {
+ campaignId: string;
+};
+
+export type PauseCampaignInput = StepInput &
+ PauseCampaignCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: PauseCampaignCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.campaignId) {
+ return { success: false, error: { message: "Campaign ID is required" } };
+ }
+
+ try {
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/campaigns/${input.campaignId}/pause`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Campaign not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to pause campaign: ${response.status} - ${errorText}` },
+ };
+ }
+
+ return {
+ success: true,
+ data: {
+ id: input.campaignId,
+ status: "paused",
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to pause campaign: ${message}` } };
+ }
+}
+
+export async function pauseCampaignStep(
+ input: PauseCampaignInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+pauseCampaignStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/resume-account.ts b/plugins/instantly/steps/resume-account.ts
new file mode 100644
index 00000000..551008c2
--- /dev/null
+++ b/plugins/instantly/steps/resume-account.ts
@@ -0,0 +1,85 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type ResumeAccountResult =
+ | { success: true; data: { email: string; status: string } }
+ | { success: false; error: { message: string } };
+
+export type ResumeAccountCoreInput = {
+ email: string;
+};
+
+export type ResumeAccountInput = StepInput &
+ ResumeAccountCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: ResumeAccountCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.email) {
+ return { success: false, error: { message: "Email is required" } };
+ }
+
+ try {
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/accounts/${encodeURIComponent(input.email)}/resume`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ },
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Account not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to resume account: ${response.status} - ${errorText}` },
+ };
+ }
+
+ return {
+ success: true,
+ data: {
+ email: input.email,
+ status: "active",
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to resume account: ${message}` } };
+ }
+}
+
+export async function resumeAccountStep(
+ input: ResumeAccountInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+resumeAccountStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/update-campaign.ts b/plugins/instantly/steps/update-campaign.ts
new file mode 100644
index 00000000..5ff25993
--- /dev/null
+++ b/plugins/instantly/steps/update-campaign.ts
@@ -0,0 +1,96 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type UpdateCampaignResult =
+ | { success: true; data: { id: string; name: string } }
+ | { success: false; error: { message: string } };
+
+export type UpdateCampaignCoreInput = {
+ campaignId: string;
+ name?: string;
+};
+
+export type UpdateCampaignInput = StepInput &
+ UpdateCampaignCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: UpdateCampaignCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.campaignId) {
+ return { success: false, error: { message: "Campaign ID is required" } };
+ }
+
+ try {
+ const updateData: Record = {};
+
+ if (input.name) {
+ updateData.name = input.name;
+ }
+
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/campaigns/${input.campaignId}`,
+ {
+ method: "PATCH",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(updateData),
+ }
+ );
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Campaign not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to update campaign: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as { id: string; name: string };
+
+ return {
+ success: true,
+ data: {
+ id: responseData.id,
+ name: responseData.name,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to update campaign: ${message}` } };
+ }
+}
+
+export async function updateCampaignStep(
+ input: UpdateCampaignInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+updateCampaignStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/update-lead-status.ts b/plugins/instantly/steps/update-lead-status.ts
new file mode 100644
index 00000000..cc9d013f
--- /dev/null
+++ b/plugins/instantly/steps/update-lead-status.ts
@@ -0,0 +1,113 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type UpdateLeadStatusResult =
+ | { success: true; data: { id: string; status: string } }
+ | { success: false; error: { message: string } };
+
+export type UpdateLeadStatusCoreInput = {
+ campaignId: string;
+ email: string;
+ status: string;
+};
+
+export type UpdateLeadStatusInput = StepInput &
+ UpdateLeadStatusCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: UpdateLeadStatusCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.campaignId) {
+ return { success: false, error: { message: "Campaign ID is required" } };
+ }
+
+ if (!input.email) {
+ return { success: false, error: { message: "Email is required" } };
+ }
+
+ if (!input.status) {
+ return { success: false, error: { message: "Status is required" } };
+ }
+
+ // Map string status to numeric value
+ const statusMap: Record = {
+ interested: 1,
+ not_interested: -1,
+ meeting_booked: 2,
+ meeting_completed: 3,
+ closed: 4,
+ out_of_office: 0,
+ wrong_person: -2,
+ };
+
+ const interestValue = statusMap[input.status] ?? 1;
+
+ try {
+ const response = await fetch(
+ `${INSTANTLY_API_URL}/leads/update-interest-status`,
+ {
+ method: "POST",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify({
+ campaign_id: input.campaignId,
+ lead_email: input.email,
+ interest_value: interestValue,
+ }),
+ }
+ );
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to update lead status: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as { id?: string };
+
+ return {
+ success: true,
+ data: {
+ id: responseData.id || input.email,
+ status: input.status,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to update lead status: ${message}` } };
+ }
+}
+
+export async function updateLeadStatusStep(
+ input: UpdateLeadStatusInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+updateLeadStatusStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/steps/update-lead.ts b/plugins/instantly/steps/update-lead.ts
new file mode 100644
index 00000000..09e9f4c9
--- /dev/null
+++ b/plugins/instantly/steps/update-lead.ts
@@ -0,0 +1,106 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { InstantlyCredentials } from "../credentials";
+
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+type UpdateLeadResult =
+ | { success: true; data: { id: string; email: string } }
+ | { success: false; error: { message: string } };
+
+export type UpdateLeadCoreInput = {
+ leadId: string;
+ firstName?: string;
+ lastName?: string;
+ companyName?: string;
+ customVariables?: string;
+};
+
+export type UpdateLeadInput = StepInput &
+ UpdateLeadCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: UpdateLeadCoreInput,
+ credentials: InstantlyCredentials
+): Promise {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "INSTANTLY_API_KEY is required" } };
+ }
+
+ if (!input.leadId) {
+ return { success: false, error: { message: "Lead ID is required" } };
+ }
+
+ try {
+ let customVars: Record = {};
+ if (input.customVariables) {
+ try {
+ customVars = JSON.parse(input.customVariables);
+ } catch {
+ return { success: false, error: { message: "Invalid JSON in custom variables" } };
+ }
+ }
+
+ const updateData: Record = {
+ ...(input.firstName && { first_name: input.firstName }),
+ ...(input.lastName && { last_name: input.lastName }),
+ ...(input.companyName && { company_name: input.companyName }),
+ ...(Object.keys(customVars).length > 0 && { custom_variables: customVars }),
+ };
+
+ const response = await fetch(`${INSTANTLY_API_URL}/leads/${input.leadId}`, {
+ method: "PATCH",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ body: JSON.stringify(updateData),
+ });
+
+ if (!response.ok) {
+ if (response.status === 404) {
+ return { success: false, error: { message: "Lead not found" } };
+ }
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `Failed to update lead: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const responseData = (await response.json()) as { id: string; email: string };
+
+ return {
+ success: true,
+ data: {
+ id: responseData.id,
+ email: responseData.email,
+ },
+ };
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ return { success: false, error: { message: `Failed to update lead: ${message}` } };
+ }
+}
+
+export async function updateLeadStep(
+ input: UpdateLeadInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+updateLeadStep.maxRetries = 0;
+
+export const _integrationType = "instantly";
+
diff --git a/plugins/instantly/test.ts b/plugins/instantly/test.ts
new file mode 100644
index 00000000..58e29cf3
--- /dev/null
+++ b/plugins/instantly/test.ts
@@ -0,0 +1,51 @@
+const INSTANTLY_API_URL = "https://api.instantly.ai/api/v2";
+
+export async function testInstantly(credentials: Record) {
+ try {
+ const apiKey = credentials.INSTANTLY_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error: "INSTANTLY_API_KEY is required",
+ };
+ }
+
+ // Validate API key by fetching campaigns (lightweight endpoint)
+ const response = await fetch(`${INSTANTLY_API_URL}/campaigns?limit=1`, {
+ method: "GET",
+ headers: {
+ Authorization: `Bearer ${apiKey}`,
+ "Content-Type": "application/json",
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ return {
+ success: false,
+ error: "Invalid API key. Please check your Instantly API key.",
+ };
+ }
+ if (response.status === 403) {
+ return {
+ success: false,
+ error:
+ "Access denied. Please ensure your API key has the required permissions.",
+ };
+ }
+ return {
+ success: false,
+ error: `API validation failed: HTTP ${response.status}`,
+ };
+ }
+
+ return { success: true };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+}
+
diff --git a/plugins/leadmagic/credentials.ts b/plugins/leadmagic/credentials.ts
new file mode 100644
index 00000000..78819a38
--- /dev/null
+++ b/plugins/leadmagic/credentials.ts
@@ -0,0 +1,3 @@
+export type LeadMagicCredentials = {
+ LEADMAGIC_API_KEY?: string;
+};
diff --git a/plugins/leadmagic/icon.tsx b/plugins/leadmagic/icon.tsx
new file mode 100644
index 00000000..d48c9962
--- /dev/null
+++ b/plugins/leadmagic/icon.tsx
@@ -0,0 +1,10 @@
+/* eslint-disable @next/next/no-img-element */
+export function LeadMagicIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/plugins/leadmagic/index.ts b/plugins/leadmagic/index.ts
new file mode 100644
index 00000000..f7ef0384
--- /dev/null
+++ b/plugins/leadmagic/index.ts
@@ -0,0 +1,275 @@
+import type { IntegrationPlugin } from "../registry";
+import { registerIntegration } from "../registry";
+import { LeadMagicIcon } from "./icon";
+
+const leadmagicPlugin: IntegrationPlugin = {
+ type: "leadmagic",
+ label: "LeadMagic",
+ description: "B2B data enrichment and lead intelligence platform",
+
+ icon: LeadMagicIcon,
+
+ formFields: [
+ {
+ id: "apiKey",
+ label: "API Key",
+ type: "password",
+ placeholder: "Your LeadMagic API key",
+ configKey: "apiKey",
+ envVar: "LEADMAGIC_API_KEY",
+ helpText: "Get your API key from ",
+ helpLink: {
+ text: "leadmagic.io/dashboard",
+ url: "https://leadmagic.io/dashboard",
+ },
+ },
+ ],
+
+ testConfig: {
+ getTestFunction: async () => {
+ const { testLeadMagic } = await import("./test");
+ return testLeadMagic;
+ },
+ },
+
+ actions: [
+ // People Operations
+ {
+ slug: "email-finder",
+ label: "Find Email",
+ description: "Find a person's email address using their LinkedIn profile URL",
+ category: "LeadMagic",
+ stepFunction: "emailFinderStep",
+ stepImportPath: "email-finder",
+ outputFields: [
+ { field: "email", description: "Email address" },
+ { field: "email_status", description: "Email status" },
+ { field: "first_name", description: "First name" },
+ { field: "last_name", description: "Last name" },
+ { field: "company", description: "Company" },
+ { field: "job_title", description: "Job title" },
+ ],
+ configFields: [
+ {
+ key: "profile_url",
+ label: "LinkedIn Profile URL",
+ type: "template-input",
+ placeholder: "https://linkedin.com/in/username or {{NodeName.profileUrl}}",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "email-validation",
+ label: "Validate Email",
+ description: "Validate an email address and check deliverability",
+ category: "LeadMagic",
+ stepFunction: "emailValidationStep",
+ stepImportPath: "email-validation",
+ outputFields: [
+ { field: "email", description: "Email address" },
+ { field: "status", description: "Validation status" },
+ { field: "is_valid", description: "Is valid" },
+ { field: "is_deliverable", description: "Is deliverable" },
+ { field: "is_catch_all", description: "Is catch-all" },
+ { field: "is_disposable", description: "Is disposable" },
+ ],
+ configFields: [
+ {
+ key: "email",
+ label: "Email Address",
+ type: "template-input",
+ placeholder: "john@example.com or {{NodeName.email}}",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "profile-search",
+ label: "Search Profile",
+ description: "Get detailed profile information from a LinkedIn URL",
+ category: "LeadMagic",
+ stepFunction: "profileSearchStep",
+ stepImportPath: "profile-search",
+ outputFields: [
+ { field: "first_name", description: "First name" },
+ { field: "last_name", description: "Last name" },
+ { field: "headline", description: "Headline" },
+ { field: "location", description: "Location" },
+ { field: "company", description: "Company" },
+ { field: "job_title", description: "Job title" },
+ { field: "profile_picture", description: "Profile picture URL" },
+ ],
+ configFields: [
+ {
+ key: "profile_url",
+ label: "LinkedIn Profile URL",
+ type: "template-input",
+ placeholder: "https://linkedin.com/in/username or {{NodeName.profileUrl}}",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "mobile-finder",
+ label: "Find Mobile",
+ description: "Find a person's mobile phone number from their LinkedIn profile",
+ category: "LeadMagic",
+ stepFunction: "mobileFinderStep",
+ stepImportPath: "mobile-finder",
+ outputFields: [
+ { field: "mobile", description: "Mobile number" },
+ { field: "mobile_status", description: "Mobile status" },
+ { field: "first_name", description: "First name" },
+ { field: "last_name", description: "Last name" },
+ ],
+ configFields: [
+ {
+ key: "profile_url",
+ label: "LinkedIn Profile URL",
+ type: "template-input",
+ placeholder: "https://linkedin.com/in/username or {{NodeName.profileUrl}}",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "role-finder",
+ label: "Find Role",
+ description: "Find a person at a company by their role/title",
+ category: "LeadMagic",
+ stepFunction: "roleFinderStep",
+ stepImportPath: "role-finder",
+ outputFields: [
+ { field: "first_name", description: "First name" },
+ { field: "last_name", description: "Last name" },
+ { field: "email", description: "Email address" },
+ { field: "job_title", description: "Job title" },
+ { field: "linkedin_url", description: "LinkedIn URL" },
+ ],
+ configFields: [
+ {
+ key: "company_name",
+ label: "Company Name",
+ type: "template-input",
+ placeholder: "Acme Inc or {{NodeName.companyName}}",
+ required: true,
+ },
+ {
+ key: "role",
+ label: "Role/Title",
+ type: "template-input",
+ placeholder: "CEO or {{NodeName.role}}",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "b2b-profile-email",
+ label: "B2B Profile Email",
+ description: "Get B2B profile data and email from a LinkedIn URL",
+ category: "LeadMagic",
+ stepFunction: "b2bProfileEmailStep",
+ stepImportPath: "b2b-profile-email",
+ outputFields: [
+ { field: "email", description: "Email address" },
+ { field: "first_name", description: "First name" },
+ { field: "last_name", description: "Last name" },
+ { field: "company", description: "Company" },
+ { field: "job_title", description: "Job title" },
+ { field: "linkedin_url", description: "LinkedIn URL" },
+ ],
+ configFields: [
+ {
+ key: "profile_url",
+ label: "LinkedIn Profile URL",
+ type: "template-input",
+ placeholder: "https://linkedin.com/in/username or {{NodeName.profileUrl}}",
+ required: true,
+ },
+ ],
+ },
+ // Company Operations
+ {
+ slug: "company-search",
+ label: "Search Company",
+ description: "Get company information from a domain or LinkedIn URL",
+ category: "LeadMagic",
+ stepFunction: "companySearchStep",
+ stepImportPath: "company-search",
+ outputFields: [
+ { field: "name", description: "Company name" },
+ { field: "domain", description: "Domain" },
+ { field: "industry", description: "Industry" },
+ { field: "employee_count", description: "Employee count" },
+ { field: "location", description: "Location" },
+ { field: "description", description: "Description" },
+ { field: "linkedin_url", description: "LinkedIn URL" },
+ ],
+ configFields: [
+ {
+ key: "domain",
+ label: "Company Domain",
+ type: "template-input",
+ placeholder: "example.com or {{NodeName.domain}}",
+ },
+ {
+ key: "linkedin_url",
+ label: "LinkedIn Company URL",
+ type: "template-input",
+ placeholder: "https://linkedin.com/company/example or {{NodeName.linkedinUrl}}",
+ },
+ ],
+ },
+ {
+ slug: "technographics",
+ label: "Get Technographics",
+ description: "Get technology stack information for a company",
+ category: "LeadMagic",
+ stepFunction: "technographicsStep",
+ stepImportPath: "technographics",
+ outputFields: [
+ { field: "domain", description: "Domain" },
+ { field: "technologies", description: "Technologies" },
+ { field: "categories", description: "Categories" },
+ ],
+ configFields: [
+ {
+ key: "domain",
+ label: "Company Domain",
+ type: "template-input",
+ placeholder: "example.com or {{NodeName.domain}}",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "company-funding",
+ label: "Get Company Funding",
+ description: "Get funding information for a company",
+ category: "LeadMagic",
+ stepFunction: "companyFundingStep",
+ stepImportPath: "company-funding",
+ outputFields: [
+ { field: "company_name", description: "Company name" },
+ { field: "total_funding", description: "Total funding" },
+ { field: "last_funding_date", description: "Last funding date" },
+ { field: "last_funding_amount", description: "Last funding amount" },
+ { field: "funding_rounds", description: "Funding rounds" },
+ ],
+ configFields: [
+ {
+ key: "domain",
+ label: "Company Domain",
+ type: "template-input",
+ placeholder: "example.com or {{NodeName.domain}}",
+ required: true,
+ },
+ ],
+ },
+ ],
+};
+
+registerIntegration(leadmagicPlugin);
+
+export default leadmagicPlugin;
diff --git a/plugins/leadmagic/steps/b2b-profile-email.ts b/plugins/leadmagic/steps/b2b-profile-email.ts
new file mode 100644
index 00000000..f552b871
--- /dev/null
+++ b/plugins/leadmagic/steps/b2b-profile-email.ts
@@ -0,0 +1,100 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { LeadMagicCredentials } from "../credentials";
+
+const LEADMAGIC_API_URL = "https://api.leadmagic.io";
+
+type B2bProfileEmailResult =
+ | { success: true; data: { email: string | null; first_name: string | null; last_name: string | null; company: string | null; job_title: string | null; linkedin_url: string | null } }
+ | { success: false; error: { message: string } };
+
+export type B2bProfileEmailCoreInput = {
+ profile_url: string;
+};
+
+export type B2bProfileEmailInput = StepInput &
+ B2bProfileEmailCoreInput & {
+ integrationId?: string;
+ };
+
+interface B2BProfileEmailResponse {
+ email?: string;
+ first_name?: string;
+ last_name?: string;
+ company?: string;
+ job_title?: string;
+ linkedin_url?: string;
+ [key: string]: unknown;
+}
+
+async function stepHandler(
+ input: B2bProfileEmailCoreInput,
+ credentials: LeadMagicCredentials
+): Promise {
+ const apiKey = credentials.LEADMAGIC_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "API key is required" } };
+ }
+
+ if (!input.profile_url) {
+ return { success: false, error: { message: "LinkedIn profile URL is required" } };
+ }
+
+ try {
+ const response = await fetch(`${LEADMAGIC_API_URL}/v1/people/b2b-profile-email`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-API-Key": apiKey,
+ },
+ body: JSON.stringify({
+ profile_url: input.profile_url,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `API error: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const data = (await response.json()) as B2BProfileEmailResponse;
+
+ return {
+ success: true,
+ data: {
+ email: data.email ?? null,
+ first_name: data.first_name ?? null,
+ last_name: data.last_name ?? null,
+ company: data.company ?? null,
+ job_title: data.job_title ?? null,
+ linkedin_url: data.linkedin_url ?? null,
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: { message: error instanceof Error ? error.message : String(error) },
+ };
+ }
+}
+
+export async function b2bProfileEmailStep(
+ input: B2bProfileEmailInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+b2bProfileEmailStep.maxRetries = 0;
+
+export const _integrationType = "leadmagic";
diff --git a/plugins/leadmagic/steps/company-funding.ts b/plugins/leadmagic/steps/company-funding.ts
new file mode 100644
index 00000000..f6733c42
--- /dev/null
+++ b/plugins/leadmagic/steps/company-funding.ts
@@ -0,0 +1,98 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { LeadMagicCredentials } from "../credentials";
+
+const LEADMAGIC_API_URL = "https://api.leadmagic.io";
+
+type CompanyFundingResult =
+ | { success: true; data: { company_name: string | null; total_funding: string | null; last_funding_date: string | null; last_funding_amount: string | null; funding_rounds: unknown } }
+ | { success: false; error: { message: string } };
+
+export type CompanyFundingCoreInput = {
+ domain: string;
+};
+
+export type CompanyFundingInput = StepInput &
+ CompanyFundingCoreInput & {
+ integrationId?: string;
+ };
+
+interface CompanyFundingResponse {
+ company_name?: string;
+ total_funding?: string;
+ last_funding_date?: string;
+ last_funding_amount?: string;
+ funding_rounds?: number;
+ [key: string]: unknown;
+}
+
+async function stepHandler(
+ input: CompanyFundingCoreInput,
+ credentials: LeadMagicCredentials
+): Promise {
+ const apiKey = credentials.LEADMAGIC_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "API key is required" } };
+ }
+
+ if (!input.domain) {
+ return { success: false, error: { message: "Company domain is required" } };
+ }
+
+ try {
+ const response = await fetch(`${LEADMAGIC_API_URL}/v1/companies/company-funding`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-API-Key": apiKey,
+ },
+ body: JSON.stringify({
+ company_domain: input.domain,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `API error: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const data = (await response.json()) as CompanyFundingResponse;
+
+ return {
+ success: true,
+ data: {
+ company_name: data.company_name ?? null,
+ total_funding: data.total_funding ?? null,
+ last_funding_date: data.last_funding_date ?? null,
+ last_funding_amount: data.last_funding_amount ?? null,
+ funding_rounds: data.funding_rounds ?? null,
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: { message: error instanceof Error ? error.message : String(error) },
+ };
+ }
+}
+
+export async function companyFundingStep(
+ input: CompanyFundingInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+companyFundingStep.maxRetries = 0;
+
+export const _integrationType = "leadmagic";
diff --git a/plugins/leadmagic/steps/company-search.ts b/plugins/leadmagic/steps/company-search.ts
new file mode 100644
index 00000000..f34dadc1
--- /dev/null
+++ b/plugins/leadmagic/steps/company-search.ts
@@ -0,0 +1,105 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { LeadMagicCredentials } from "../credentials";
+
+const LEADMAGIC_API_URL = "https://api.leadmagic.io";
+
+type CompanySearchResult =
+ | { success: true; data: { name: string | null; domain: string | null; industry: string | null; employee_count: number | null; location: string | null; description: string | null; linkedin_url: string | null } }
+ | { success: false; error: { message: string } };
+
+export type CompanySearchCoreInput = {
+ domain?: string;
+ linkedin_url?: string;
+};
+
+export type CompanySearchInput = StepInput &
+ CompanySearchCoreInput & {
+ integrationId?: string;
+ };
+
+interface CompanySearchResponse {
+ name?: string;
+ domain?: string;
+ industry?: string;
+ employee_count?: number;
+ location?: string;
+ description?: string;
+ linkedin_url?: string;
+ [key: string]: unknown;
+}
+
+async function stepHandler(
+ input: CompanySearchCoreInput,
+ credentials: LeadMagicCredentials
+): Promise {
+ const apiKey = credentials.LEADMAGIC_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "API key is required" } };
+ }
+
+ if (!input.domain && !input.linkedin_url) {
+ return { success: false, error: { message: "Either domain or LinkedIn URL is required" } };
+ }
+
+ try {
+ const body: Record = {};
+ if (input.domain) body.company_domain = input.domain;
+ if (input.linkedin_url) body.profile_url = input.linkedin_url;
+
+ const response = await fetch(`${LEADMAGIC_API_URL}/v1/companies/company-search`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-API-Key": apiKey,
+ },
+ body: JSON.stringify(body),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `API error: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const data = (await response.json()) as CompanySearchResponse;
+
+ return {
+ success: true,
+ data: {
+ name: data.name ?? null,
+ domain: data.domain ?? null,
+ industry: data.industry ?? null,
+ employee_count: data.employee_count ?? null,
+ location: data.location ?? null,
+ description: data.description ?? null,
+ linkedin_url: data.linkedin_url ?? null,
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: { message: error instanceof Error ? error.message : String(error) },
+ };
+ }
+}
+
+export async function companySearchStep(
+ input: CompanySearchInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+companySearchStep.maxRetries = 0;
+
+export const _integrationType = "leadmagic";
diff --git a/plugins/leadmagic/steps/email-finder.ts b/plugins/leadmagic/steps/email-finder.ts
new file mode 100644
index 00000000..7728701c
--- /dev/null
+++ b/plugins/leadmagic/steps/email-finder.ts
@@ -0,0 +1,100 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { LeadMagicCredentials } from "../credentials";
+
+const LEADMAGIC_API_URL = "https://api.leadmagic.io";
+
+type EmailFinderResult =
+ | { success: true; data: { email: string | null; email_status: string | null; first_name: string | null; last_name: string | null; company: string | null; job_title: string | null } }
+ | { success: false; error: { message: string } };
+
+export type EmailFinderCoreInput = {
+ profile_url: string;
+};
+
+export type EmailFinderInput = StepInput &
+ EmailFinderCoreInput & {
+ integrationId?: string;
+ };
+
+interface EmailFinderResponse {
+ email?: string;
+ email_status?: string;
+ first_name?: string;
+ last_name?: string;
+ company?: string;
+ job_title?: string;
+ [key: string]: unknown;
+}
+
+async function stepHandler(
+ input: EmailFinderCoreInput,
+ credentials: LeadMagicCredentials
+): Promise {
+ const apiKey = credentials.LEADMAGIC_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "API key is required" } };
+ }
+
+ if (!input.profile_url) {
+ return { success: false, error: { message: "LinkedIn profile URL is required" } };
+ }
+
+ try {
+ const response = await fetch(`${LEADMAGIC_API_URL}/v1/people/b2b-profile-email`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-API-Key": apiKey,
+ },
+ body: JSON.stringify({
+ profile_url: input.profile_url,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `API error: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const data = (await response.json()) as EmailFinderResponse;
+
+ return {
+ success: true,
+ data: {
+ email: data.email ?? null,
+ email_status: data.email_status ?? null,
+ first_name: data.first_name ?? null,
+ last_name: data.last_name ?? null,
+ company: data.company ?? null,
+ job_title: data.job_title ?? null,
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: { message: error instanceof Error ? error.message : String(error) },
+ };
+ }
+}
+
+export async function emailFinderStep(
+ input: EmailFinderInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+emailFinderStep.maxRetries = 0;
+
+export const _integrationType = "leadmagic";
diff --git a/plugins/leadmagic/steps/email-validation.ts b/plugins/leadmagic/steps/email-validation.ts
new file mode 100644
index 00000000..9133aef9
--- /dev/null
+++ b/plugins/leadmagic/steps/email-validation.ts
@@ -0,0 +1,100 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { LeadMagicCredentials } from "../credentials";
+
+const LEADMAGIC_API_URL = "https://api.leadmagic.io";
+
+type EmailValidationResult =
+ | { success: true; data: { email: string | null; status: string | null; is_valid: boolean | null; is_deliverable: boolean | null; is_catch_all: boolean | null; is_disposable: boolean | null } }
+ | { success: false; error: { message: string } };
+
+export type EmailValidationCoreInput = {
+ email: string;
+};
+
+export type EmailValidationInput = StepInput &
+ EmailValidationCoreInput & {
+ integrationId?: string;
+ };
+
+interface EmailValidationResponse {
+ email?: string;
+ status?: string;
+ is_valid?: boolean;
+ is_deliverable?: boolean;
+ is_catch_all?: boolean;
+ is_disposable?: boolean;
+ [key: string]: unknown;
+}
+
+async function stepHandler(
+ input: EmailValidationCoreInput,
+ credentials: LeadMagicCredentials
+): Promise {
+ const apiKey = credentials.LEADMAGIC_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "API key is required" } };
+ }
+
+ if (!input.email) {
+ return { success: false, error: { message: "Email address is required" } };
+ }
+
+ try {
+ const response = await fetch(`${LEADMAGIC_API_URL}/v1/people/email-validation`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-API-Key": apiKey,
+ },
+ body: JSON.stringify({
+ email: input.email,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `API error: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const data = (await response.json()) as EmailValidationResponse;
+
+ return {
+ success: true,
+ data: {
+ email: data.email ?? null,
+ status: data.status ?? null,
+ is_valid: data.is_valid ?? null,
+ is_deliverable: data.is_deliverable ?? null,
+ is_catch_all: data.is_catch_all ?? null,
+ is_disposable: data.is_disposable ?? null,
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: { message: error instanceof Error ? error.message : String(error) },
+ };
+ }
+}
+
+export async function emailValidationStep(
+ input: EmailValidationInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+emailValidationStep.maxRetries = 0;
+
+export const _integrationType = "leadmagic";
diff --git a/plugins/leadmagic/steps/mobile-finder.ts b/plugins/leadmagic/steps/mobile-finder.ts
new file mode 100644
index 00000000..40719432
--- /dev/null
+++ b/plugins/leadmagic/steps/mobile-finder.ts
@@ -0,0 +1,96 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { LeadMagicCredentials } from "../credentials";
+
+const LEADMAGIC_API_URL = "https://api.leadmagic.io";
+
+type MobileFinderResult =
+ | { success: true; data: { mobile: string | null; mobile_status: string | null; first_name: string | null; last_name: string | null } }
+ | { success: false; error: { message: string } };
+
+export type MobileFinderCoreInput = {
+ profile_url: string;
+};
+
+export type MobileFinderInput = StepInput &
+ MobileFinderCoreInput & {
+ integrationId?: string;
+ };
+
+interface MobileFinderResponse {
+ mobile?: string;
+ mobile_status?: string;
+ first_name?: string;
+ last_name?: string;
+ [key: string]: unknown;
+}
+
+async function stepHandler(
+ input: MobileFinderCoreInput,
+ credentials: LeadMagicCredentials
+): Promise {
+ const apiKey = credentials.LEADMAGIC_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "API key is required" } };
+ }
+
+ if (!input.profile_url) {
+ return { success: false, error: { message: "LinkedIn profile URL is required" } };
+ }
+
+ try {
+ const response = await fetch(`${LEADMAGIC_API_URL}/v1/people/mobile-finder`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-API-Key": apiKey,
+ },
+ body: JSON.stringify({
+ profile_url: input.profile_url,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `API error: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const data = (await response.json()) as MobileFinderResponse;
+
+ return {
+ success: true,
+ data: {
+ mobile: data.mobile ?? null,
+ mobile_status: data.mobile_status ?? null,
+ first_name: data.first_name ?? null,
+ last_name: data.last_name ?? null,
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: { message: error instanceof Error ? error.message : String(error) },
+ };
+ }
+}
+
+export async function mobileFinderStep(
+ input: MobileFinderInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+mobileFinderStep.maxRetries = 0;
+
+export const _integrationType = "leadmagic";
diff --git a/plugins/leadmagic/steps/profile-search.ts b/plugins/leadmagic/steps/profile-search.ts
new file mode 100644
index 00000000..65d839a0
--- /dev/null
+++ b/plugins/leadmagic/steps/profile-search.ts
@@ -0,0 +1,102 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { LeadMagicCredentials } from "../credentials";
+
+const LEADMAGIC_API_URL = "https://api.leadmagic.io";
+
+type ProfileSearchResult =
+ | { success: true; data: { first_name: string | null; last_name: string | null; headline: string | null; location: string | null; company: string | null; job_title: string | null; profile_picture: string | null } }
+ | { success: false; error: { message: string } };
+
+export type ProfileSearchCoreInput = {
+ profile_url: string;
+};
+
+export type ProfileSearchInput = StepInput &
+ ProfileSearchCoreInput & {
+ integrationId?: string;
+ };
+
+interface ProfileSearchResponse {
+ first_name?: string;
+ last_name?: string;
+ headline?: string;
+ location?: string;
+ company?: string;
+ job_title?: string;
+ profile_picture?: string;
+ [key: string]: unknown;
+}
+
+async function stepHandler(
+ input: ProfileSearchCoreInput,
+ credentials: LeadMagicCredentials
+): Promise {
+ const apiKey = credentials.LEADMAGIC_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "API key is required" } };
+ }
+
+ if (!input.profile_url) {
+ return { success: false, error: { message: "LinkedIn profile URL is required" } };
+ }
+
+ try {
+ const response = await fetch(`${LEADMAGIC_API_URL}/v1/people/profile-search`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-API-Key": apiKey,
+ },
+ body: JSON.stringify({
+ profile_url: input.profile_url,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `API error: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const data = (await response.json()) as ProfileSearchResponse;
+
+ return {
+ success: true,
+ data: {
+ first_name: data.first_name ?? null,
+ last_name: data.last_name ?? null,
+ headline: data.headline ?? null,
+ location: data.location ?? null,
+ company: data.company ?? null,
+ job_title: data.job_title ?? null,
+ profile_picture: data.profile_picture ?? null,
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: { message: error instanceof Error ? error.message : String(error) },
+ };
+ }
+}
+
+export async function profileSearchStep(
+ input: ProfileSearchInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+profileSearchStep.maxRetries = 0;
+
+export const _integrationType = "leadmagic";
diff --git a/plugins/leadmagic/steps/role-finder.ts b/plugins/leadmagic/steps/role-finder.ts
new file mode 100644
index 00000000..6f06595c
--- /dev/null
+++ b/plugins/leadmagic/steps/role-finder.ts
@@ -0,0 +1,104 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { LeadMagicCredentials } from "../credentials";
+
+const LEADMAGIC_API_URL = "https://api.leadmagic.io";
+
+type RoleFinderResult =
+ | { success: true; data: { first_name: string | null; last_name: string | null; email: string | null; job_title: string | null; linkedin_url: string | null } }
+ | { success: false; error: { message: string } };
+
+export type RoleFinderCoreInput = {
+ company_name: string;
+ role: string;
+};
+
+export type RoleFinderInput = StepInput &
+ RoleFinderCoreInput & {
+ integrationId?: string;
+ };
+
+interface RoleFinderResponse {
+ first_name?: string;
+ last_name?: string;
+ email?: string;
+ job_title?: string;
+ linkedin_url?: string;
+ [key: string]: unknown;
+}
+
+async function stepHandler(
+ input: RoleFinderCoreInput,
+ credentials: LeadMagicCredentials
+): Promise {
+ const apiKey = credentials.LEADMAGIC_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "API key is required" } };
+ }
+
+ if (!input.company_name) {
+ return { success: false, error: { message: "Company name is required" } };
+ }
+
+ if (!input.role) {
+ return { success: false, error: { message: "Role/title is required" } };
+ }
+
+ try {
+ const response = await fetch(`${LEADMAGIC_API_URL}/v1/people/role-finder`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-API-Key": apiKey,
+ },
+ body: JSON.stringify({
+ company_name: input.company_name,
+ job_title: input.role,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `API error: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const data = (await response.json()) as RoleFinderResponse;
+
+ return {
+ success: true,
+ data: {
+ first_name: data.first_name ?? null,
+ last_name: data.last_name ?? null,
+ email: data.email ?? null,
+ job_title: data.job_title ?? null,
+ linkedin_url: data.linkedin_url ?? null,
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: { message: error instanceof Error ? error.message : String(error) },
+ };
+ }
+}
+
+export async function roleFinderStep(
+ input: RoleFinderInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+roleFinderStep.maxRetries = 0;
+
+export const _integrationType = "leadmagic";
diff --git a/plugins/leadmagic/steps/technographics.ts b/plugins/leadmagic/steps/technographics.ts
new file mode 100644
index 00000000..d1843b35
--- /dev/null
+++ b/plugins/leadmagic/steps/technographics.ts
@@ -0,0 +1,94 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import type { LeadMagicCredentials } from "../credentials";
+
+const LEADMAGIC_API_URL = "https://api.leadmagic.io";
+
+type TechnographicsResult =
+ | { success: true; data: { domain: string | null; technologies: unknown; categories: unknown } }
+ | { success: false; error: { message: string } };
+
+export type TechnographicsCoreInput = {
+ domain: string;
+};
+
+export type TechnographicsInput = StepInput &
+ TechnographicsCoreInput & {
+ integrationId?: string;
+ };
+
+interface TechnographicsResponse {
+ domain?: string;
+ technologies?: string[];
+ categories?: string[];
+ [key: string]: unknown;
+}
+
+async function stepHandler(
+ input: TechnographicsCoreInput,
+ credentials: LeadMagicCredentials
+): Promise {
+ const apiKey = credentials.LEADMAGIC_API_KEY;
+
+ if (!apiKey) {
+ return { success: false, error: { message: "API key is required" } };
+ }
+
+ if (!input.domain) {
+ return { success: false, error: { message: "Company domain is required" } };
+ }
+
+ try {
+ const response = await fetch(`${LEADMAGIC_API_URL}/v1/companies/technographics`, {
+ method: "POST",
+ headers: {
+ "Content-Type": "application/json",
+ "X-API-Key": apiKey,
+ },
+ body: JSON.stringify({
+ company_domain: input.domain,
+ }),
+ });
+
+ if (!response.ok) {
+ const errorText = await response.text();
+ return {
+ success: false,
+ error: { message: `API error: ${response.status} - ${errorText}` },
+ };
+ }
+
+ const data = (await response.json()) as TechnographicsResponse;
+
+ return {
+ success: true,
+ data: {
+ domain: data.domain ?? null,
+ technologies: data.technologies ?? [],
+ categories: data.categories ?? [],
+ },
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: { message: error instanceof Error ? error.message : String(error) },
+ };
+ }
+}
+
+export async function technographicsStep(
+ input: TechnographicsInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+technographicsStep.maxRetries = 0;
+
+export const _integrationType = "leadmagic";
diff --git a/plugins/leadmagic/test.ts b/plugins/leadmagic/test.ts
new file mode 100644
index 00000000..3ed72c81
--- /dev/null
+++ b/plugins/leadmagic/test.ts
@@ -0,0 +1,45 @@
+const LEADMAGIC_API_URL = "https://api.leadmagic.io";
+
+export async function testLeadMagic(credentials: Record) {
+ try {
+ const apiKey = credentials.LEADMAGIC_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error: "API key is required",
+ };
+ }
+
+ const response = await fetch(`${LEADMAGIC_API_URL}/v1/credits`, {
+ method: "GET",
+ headers: {
+ "X-API-Key": apiKey,
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ return {
+ success: false,
+ error: "Invalid API key. Please check your LeadMagic API key.",
+ };
+ }
+ return {
+ success: false,
+ error: `API validation failed: HTTP ${response.status}`,
+ };
+ }
+
+ const data = (await response.json()) as { credits?: number };
+ return {
+ success: true,
+ message: `Connected. ${data.credits ?? 0} credits available.`,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error instanceof Error ? error.message : String(error),
+ };
+ }
+}
diff --git a/public/instantly-logo.png b/public/instantly-logo.png
new file mode 100644
index 00000000..f37c500d
Binary files /dev/null and b/public/instantly-logo.png differ
diff --git a/public/leadmagic-logo.png b/public/leadmagic-logo.png
new file mode 100644
index 00000000..a9b2d956
Binary files /dev/null and b/public/leadmagic-logo.png differ