diff --git a/plugins/index.ts b/plugins/index.ts
index c2b41249..ef2ebba4 100644
--- a/plugins/index.ts
+++ b/plugins/index.ts
@@ -26,6 +26,7 @@ import "./slack";
import "./stripe";
import "./superagent";
import "./v0";
+import "./webflow";
export type {
ActionConfigField,
diff --git a/plugins/webflow/credentials.ts b/plugins/webflow/credentials.ts
new file mode 100644
index 00000000..63e54a5a
--- /dev/null
+++ b/plugins/webflow/credentials.ts
@@ -0,0 +1,3 @@
+export type WebflowCredentials = {
+ WEBFLOW_API_KEY?: string;
+};
diff --git a/plugins/webflow/icon.tsx b/plugins/webflow/icon.tsx
new file mode 100644
index 00000000..a119039c
--- /dev/null
+++ b/plugins/webflow/icon.tsx
@@ -0,0 +1,14 @@
+export function WebflowIcon({ className }: { className?: string }) {
+ return (
+
+ );
+}
diff --git a/plugins/webflow/index.ts b/plugins/webflow/index.ts
new file mode 100644
index 00000000..26b5c9d7
--- /dev/null
+++ b/plugins/webflow/index.ts
@@ -0,0 +1,119 @@
+import type { IntegrationPlugin } from "../registry";
+import { registerIntegration } from "../registry";
+import { WebflowIcon } from "./icon";
+
+const webflowPlugin: IntegrationPlugin = {
+ type: "webflow",
+ label: "Webflow",
+ description: "Publish and manage Webflow sites",
+
+ icon: WebflowIcon,
+
+ formFields: [
+ {
+ id: "apiKey",
+ label: "API Token",
+ type: "password",
+ placeholder: "your-api-token",
+ configKey: "apiKey",
+ envVar: "WEBFLOW_API_KEY",
+ helpText: "Generate an API token from ",
+ helpLink: {
+ text: "Webflow Dashboard",
+ url: "https://webflow.com/dashboard",
+ },
+ },
+ ],
+
+ testConfig: {
+ getTestFunction: async () => {
+ const { testWebflow } = await import("./test");
+ return testWebflow;
+ },
+ },
+
+ actions: [
+ {
+ slug: "list-sites",
+ label: "List Sites",
+ description: "Get all sites accessible with the API token",
+ category: "Webflow",
+ stepFunction: "listSitesStep",
+ stepImportPath: "list-sites",
+ outputFields: [
+ { field: "sites", description: "Array of site objects" },
+ { field: "count", description: "Number of sites returned" },
+ ],
+ configFields: [],
+ },
+ {
+ slug: "get-site",
+ label: "Get Site",
+ description: "Get details of a specific Webflow site",
+ category: "Webflow",
+ stepFunction: "getSiteStep",
+ stepImportPath: "get-site",
+ outputFields: [
+ { field: "id", description: "Site ID" },
+ { field: "displayName", description: "Display name of the site" },
+ { field: "shortName", description: "Short name (subdomain)" },
+ { field: "previewUrl", description: "Preview URL" },
+ { field: "lastPublished", description: "Last published timestamp" },
+ { field: "customDomains", description: "Array of custom domains" },
+ ],
+ configFields: [
+ {
+ key: "siteId",
+ label: "Site ID",
+ type: "template-input",
+ placeholder: "site-id or {{NodeName.id}}",
+ example: "580e63e98c9a982ac9b8b741",
+ required: true,
+ },
+ ],
+ },
+ {
+ slug: "publish-site",
+ label: "Publish Site",
+ description: "Publish a site to one or more domains",
+ category: "Webflow",
+ stepFunction: "publishSiteStep",
+ stepImportPath: "publish-site",
+ outputFields: [
+ { field: "publishedDomains", description: "Array of published domain URLs" },
+ { field: "publishedToSubdomain", description: "Whether published to Webflow subdomain" },
+ ],
+ configFields: [
+ {
+ key: "siteId",
+ label: "Site ID",
+ type: "template-input",
+ placeholder: "site-id or {{NodeName.id}}",
+ example: "580e63e98c9a982ac9b8b741",
+ required: true,
+ },
+ {
+ key: "publishToWebflowSubdomain",
+ label: "Publish to Webflow Subdomain",
+ type: "select",
+ options: [
+ { value: "true", label: "Yes" },
+ { value: "false", label: "No" },
+ ],
+ defaultValue: "true",
+ },
+ {
+ key: "customDomainIds",
+ label: "Custom Domain IDs (comma-separated)",
+ type: "template-input",
+ placeholder: "domain-id-1, domain-id-2",
+ example: "589a331aa51e760df7ccb89d",
+ },
+ ],
+ },
+ ],
+};
+
+registerIntegration(webflowPlugin);
+
+export default webflowPlugin;
diff --git a/plugins/webflow/steps/get-site.ts b/plugins/webflow/steps/get-site.ts
new file mode 100644
index 00000000..ba8aa6cc
--- /dev/null
+++ b/plugins/webflow/steps/get-site.ts
@@ -0,0 +1,128 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import { getErrorMessage } from "@/lib/utils";
+import type { WebflowCredentials } from "../credentials";
+
+const WEBFLOW_API_URL = "https://api.webflow.com/v2";
+
+type WebflowSiteResponse = {
+ id: string;
+ workspaceId: string;
+ createdOn: string;
+ displayName: string;
+ shortName: string;
+ lastPublished?: string;
+ lastUpdated: string;
+ previewUrl: string;
+ timeZone: string;
+ customDomains?: Array<{
+ id: string;
+ url: string;
+ lastPublished?: string;
+ }>;
+};
+
+type GetSiteResult =
+ | {
+ success: true;
+ id: string;
+ displayName: string;
+ shortName: string;
+ previewUrl: string;
+ lastPublished?: string;
+ lastUpdated: string;
+ timeZone: string;
+ customDomains: Array<{
+ id: string;
+ url: string;
+ lastPublished?: string;
+ }>;
+ }
+ | { success: false; error: string };
+
+export type GetSiteCoreInput = {
+ siteId: string;
+};
+
+export type GetSiteInput = StepInput &
+ GetSiteCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: GetSiteCoreInput,
+ credentials: WebflowCredentials
+): Promise {
+ const apiKey = credentials.WEBFLOW_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error:
+ "WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!input.siteId) {
+ return {
+ success: false,
+ error: "Site ID is required",
+ };
+ }
+
+ try {
+ const response = await fetch(
+ `${WEBFLOW_API_URL}/sites/${encodeURIComponent(input.siteId)}`,
+ {
+ method: "GET",
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${apiKey}`,
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = (await response.json()) as { message?: string };
+ return {
+ success: false,
+ error: errorData.message || `HTTP ${response.status}`,
+ };
+ }
+
+ const site = (await response.json()) as WebflowSiteResponse;
+
+ return {
+ success: true,
+ id: site.id,
+ displayName: site.displayName,
+ shortName: site.shortName,
+ previewUrl: site.previewUrl,
+ lastPublished: site.lastPublished,
+ lastUpdated: site.lastUpdated,
+ timeZone: site.timeZone,
+ customDomains: site.customDomains || [],
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to get site: ${getErrorMessage(error)}`,
+ };
+ }
+}
+
+export async function getSiteStep(
+ input: GetSiteInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+getSiteStep.maxRetries = 0;
+
+export const _integrationType = "webflow";
diff --git a/plugins/webflow/steps/list-sites.ts b/plugins/webflow/steps/list-sites.ts
new file mode 100644
index 00000000..5d9682eb
--- /dev/null
+++ b/plugins/webflow/steps/list-sites.ts
@@ -0,0 +1,119 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import { getErrorMessage } from "@/lib/utils";
+import type { WebflowCredentials } from "../credentials";
+
+const WEBFLOW_API_URL = "https://api.webflow.com/v2";
+
+type WebflowSite = {
+ id: string;
+ workspaceId: string;
+ createdOn: string;
+ displayName: string;
+ shortName: string;
+ lastPublished?: string;
+ lastUpdated: string;
+ previewUrl: string;
+ timeZone: string;
+ customDomains?: Array<{
+ id: string;
+ url: string;
+ lastPublished?: string;
+ }>;
+};
+
+type ListSitesResult =
+ | {
+ success: true;
+ sites: Array<{
+ id: string;
+ displayName: string;
+ shortName: string;
+ previewUrl: string;
+ lastPublished?: string;
+ lastUpdated: string;
+ customDomains: string[];
+ }>;
+ count: number;
+ }
+ | { success: false; error: string };
+
+export type ListSitesCoreInput = Record;
+
+export type ListSitesInput = StepInput &
+ ListSitesCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ _input: ListSitesCoreInput,
+ credentials: WebflowCredentials
+): Promise {
+ const apiKey = credentials.WEBFLOW_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error:
+ "WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ try {
+ const response = await fetch(`${WEBFLOW_API_URL}/sites`, {
+ method: "GET",
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${apiKey}`,
+ },
+ });
+
+ if (!response.ok) {
+ const errorData = (await response.json()) as { message?: string };
+ return {
+ success: false,
+ error: errorData.message || `HTTP ${response.status}`,
+ };
+ }
+
+ const data = (await response.json()) as { sites: WebflowSite[] };
+
+ const sites = data.sites.map((site) => ({
+ id: site.id,
+ displayName: site.displayName,
+ shortName: site.shortName,
+ previewUrl: site.previewUrl,
+ lastPublished: site.lastPublished,
+ lastUpdated: site.lastUpdated,
+ customDomains: site.customDomains?.map((d) => d.url) || [],
+ }));
+
+ return {
+ success: true,
+ sites,
+ count: sites.length,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to list sites: ${getErrorMessage(error)}`,
+ };
+ }
+}
+
+export async function listSitesStep(
+ input: ListSitesInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+listSitesStep.maxRetries = 0;
+
+export const _integrationType = "webflow";
diff --git a/plugins/webflow/steps/publish-site.ts b/plugins/webflow/steps/publish-site.ts
new file mode 100644
index 00000000..8a9d2468
--- /dev/null
+++ b/plugins/webflow/steps/publish-site.ts
@@ -0,0 +1,137 @@
+import "server-only";
+
+import { fetchCredentials } from "@/lib/credential-fetcher";
+import { type StepInput, withStepLogging } from "@/lib/steps/step-handler";
+import { getErrorMessage } from "@/lib/utils";
+import type { WebflowCredentials } from "../credentials";
+
+const WEBFLOW_API_URL = "https://api.webflow.com/v2";
+
+type PublishResponse = {
+ customDomains?: Array<{
+ id: string;
+ url: string;
+ lastPublished?: string;
+ }>;
+ publishToWebflowSubdomain?: boolean;
+};
+
+type PublishSiteResult =
+ | {
+ success: true;
+ publishedDomains: string[];
+ publishedToSubdomain: boolean;
+ }
+ | { success: false; error: string };
+
+export type PublishSiteCoreInput = {
+ siteId: string;
+ publishToWebflowSubdomain?: string;
+ customDomainIds?: string;
+};
+
+export type PublishSiteInput = StepInput &
+ PublishSiteCoreInput & {
+ integrationId?: string;
+ };
+
+async function stepHandler(
+ input: PublishSiteCoreInput,
+ credentials: WebflowCredentials
+): Promise {
+ const apiKey = credentials.WEBFLOW_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error:
+ "WEBFLOW_API_KEY is not configured. Please add it in Project Integrations.",
+ };
+ }
+
+ if (!input.siteId) {
+ return {
+ success: false,
+ error: "Site ID is required",
+ };
+ }
+
+ try {
+ const body: {
+ publishToWebflowSubdomain?: boolean;
+ customDomains?: string[];
+ } = {};
+
+ // Parse custom domain IDs if provided
+ const customDomains = input.customDomainIds
+ ? input.customDomainIds
+ .split(",")
+ .map((id) => id.trim())
+ .filter(Boolean)
+ : [];
+
+ if (customDomains.length > 0) {
+ body.customDomains = customDomains;
+ }
+
+ // Default to publishing to subdomain if no custom domains specified
+ // or if explicitly set to true
+ const publishToSubdomain =
+ input.publishToWebflowSubdomain === "false" ? false : true;
+
+ if (publishToSubdomain || customDomains.length === 0) {
+ body.publishToWebflowSubdomain = true;
+ } else {
+ body.publishToWebflowSubdomain = false;
+ }
+
+ const response = await fetch(
+ `${WEBFLOW_API_URL}/sites/${encodeURIComponent(input.siteId)}/publish`,
+ {
+ method: "POST",
+ headers: {
+ Accept: "application/json",
+ "Content-Type": "application/json",
+ Authorization: `Bearer ${apiKey}`,
+ },
+ body: JSON.stringify(body),
+ }
+ );
+
+ if (!response.ok) {
+ const errorData = (await response.json()) as { message?: string };
+ return {
+ success: false,
+ error: errorData.message || `HTTP ${response.status}`,
+ };
+ }
+
+ const result = (await response.json()) as PublishResponse;
+
+ return {
+ success: true,
+ publishedDomains: result.customDomains?.map((d) => d.url) || [],
+ publishedToSubdomain: result.publishToWebflowSubdomain ?? false,
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: `Failed to publish site: ${getErrorMessage(error)}`,
+ };
+ }
+}
+
+export async function publishSiteStep(
+ input: PublishSiteInput
+): Promise {
+ "use step";
+
+ const credentials = input.integrationId
+ ? await fetchCredentials(input.integrationId)
+ : {};
+
+ return withStepLogging(input, () => stepHandler(input, credentials));
+}
+publishSiteStep.maxRetries = 0;
+
+export const _integrationType = "webflow";
diff --git a/plugins/webflow/test.ts b/plugins/webflow/test.ts
new file mode 100644
index 00000000..905ee96c
--- /dev/null
+++ b/plugins/webflow/test.ts
@@ -0,0 +1,43 @@
+const WEBFLOW_API_URL = "https://api.webflow.com/v2";
+
+export async function testWebflow(credentials: Record) {
+ try {
+ const apiKey = credentials.WEBFLOW_API_KEY;
+
+ if (!apiKey) {
+ return {
+ success: false,
+ error: "WEBFLOW_API_KEY is required",
+ };
+ }
+
+ // Use the list sites endpoint to validate the API key
+ const response = await fetch(`${WEBFLOW_API_URL}/sites`, {
+ method: "GET",
+ headers: {
+ Accept: "application/json",
+ Authorization: `Bearer ${apiKey}`,
+ },
+ });
+
+ if (!response.ok) {
+ if (response.status === 401) {
+ return {
+ success: false,
+ error: "Invalid API key. Please check your Webflow API token.",
+ };
+ }
+ 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),
+ };
+ }
+}