diff --git a/apps/docs/content/docs/en/tools/meta.json b/apps/docs/content/docs/en/tools/meta.json index fb1694bd0f..992b6d4b01 100644 --- a/apps/docs/content/docs/en/tools/meta.json +++ b/apps/docs/content/docs/en/tools/meta.json @@ -66,6 +66,7 @@ "typeform", "vision", "wealthbox", + "webflow", "webhook", "whatsapp", "wikipedia", diff --git a/apps/docs/content/docs/en/tools/webflow.mdx b/apps/docs/content/docs/en/tools/webflow.mdx new file mode 100644 index 0000000000..8153bfea29 --- /dev/null +++ b/apps/docs/content/docs/en/tools/webflow.mdx @@ -0,0 +1,150 @@ +--- +title: Webflow +description: Manage Webflow CMS collections +--- + +import { BlockInfoCard } from "@/components/ui/block-info-card" + + + + `} +/> + +{/* MANUAL-CONTENT-START:intro */} +[Webflow](https://webflow.com/) is a powerful visual web design platform that enables you to build responsive websites without writing code. It combines a visual design interface with a robust CMS (Content Management System) that allows you to create, manage, and publish dynamic content for your websites. + +With Webflow, you can: + +- **Design visually**: Create custom websites with a visual editor that generates clean, semantic HTML/CSS code +- **Manage content dynamically**: Use the CMS to create collections of structured content like blog posts, products, team members, or any custom data +- **Publish instantly**: Deploy your sites to Webflow's hosting or export the code for custom hosting +- **Create responsive designs**: Build sites that work seamlessly across desktop, tablet, and mobile devices +- **Customize collections**: Define custom fields and data structures for your content types +- **Automate content updates**: Programmatically manage your CMS content through APIs + +In Sim, the Webflow integration enables your agents to seamlessly interact with your Webflow CMS collections through API authentication. This allows for powerful automation scenarios such as automatically creating blog posts from AI-generated content, updating product information, managing team member profiles, and retrieving CMS items for dynamic content generation. Your agents can list existing items to browse your content, retrieve specific items by ID, create new entries to add fresh content, update existing items to keep information current, and delete outdated content. This integration bridges the gap between your AI workflows and your Webflow CMS, enabling automated content management, dynamic website updates, and streamlined content workflows that keep your sites fresh and up-to-date without manual intervention. +{/* MANUAL-CONTENT-END */} + + +## Usage Instructions + +Integrates Webflow CMS into the workflow. Can create, get, list, update, or delete items in Webflow CMS collections. Manage your Webflow content programmatically. Can be used in trigger mode to trigger workflows when collection items change or forms are submitted. + + + +## Tools + +### `webflow_list_items` + +List all items from a Webflow CMS collection + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `collectionId` | string | Yes | ID of the collection | +| `offset` | number | No | Offset for pagination \(optional\) | +| `limit` | number | No | Maximum number of items to return \(optional, default: 100\) | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `items` | json | Array of collection items | +| `metadata` | json | Metadata about the query | + +### `webflow_get_item` + +Get a single item from a Webflow CMS collection + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `collectionId` | string | Yes | ID of the collection | +| `itemId` | string | Yes | ID of the item to retrieve | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | json | The retrieved item object | +| `metadata` | json | Metadata about the retrieved item | + +### `webflow_create_item` + +Create a new item in a Webflow CMS collection + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `collectionId` | string | Yes | ID of the collection | +| `fieldData` | json | Yes | Field data for the new item as a JSON object. Keys should match collection field names. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | json | The created item object | +| `metadata` | json | Metadata about the created item | + +### `webflow_update_item` + +Update an existing item in a Webflow CMS collection + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `collectionId` | string | Yes | ID of the collection | +| `itemId` | string | Yes | ID of the item to update | +| `fieldData` | json | Yes | Field data to update as a JSON object. Only include fields you want to change. | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `item` | json | The updated item object | +| `metadata` | json | Metadata about the updated item | + +### `webflow_delete_item` + +Delete an item from a Webflow CMS collection + +#### Input + +| Parameter | Type | Required | Description | +| --------- | ---- | -------- | ----------- | +| `collectionId` | string | Yes | ID of the collection | +| `itemId` | string | Yes | ID of the item to delete | + +#### Output + +| Parameter | Type | Description | +| --------- | ---- | ----------- | +| `success` | boolean | Whether the deletion was successful | +| `metadata` | json | Metadata about the deletion | + + + +## Notes + +- Category: `tools` +- Type: `webflow` diff --git a/apps/sim/app/api/tools/webflow/collections/route.ts b/apps/sim/app/api/tools/webflow/collections/route.ts new file mode 100644 index 0000000000..79c1d4605e --- /dev/null +++ b/apps/sim/app/api/tools/webflow/collections/route.ts @@ -0,0 +1,69 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console/logger' +import { getOAuthToken } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('WebflowCollectionsAPI') + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const { searchParams } = new URL(request.url) + const siteId = searchParams.get('siteId') + + if (!siteId) { + return NextResponse.json({ error: 'Missing siteId parameter' }, { status: 400 }) + } + + const accessToken = await getOAuthToken(session.user.id, 'webflow') + + if (!accessToken) { + return NextResponse.json( + { error: 'No Webflow access token found. Please connect your Webflow account.' }, + { status: 404 } + ) + } + + const response = await fetch(`https://api.webflow.com/v2/sites/${siteId}/collections`, { + headers: { + Authorization: `Bearer ${accessToken}`, + accept: 'application/json', + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Webflow collections', { + status: response.status, + error: errorData, + siteId, + }) + return NextResponse.json( + { error: 'Failed to fetch Webflow collections', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const collections = data.collections || [] + + const formattedCollections = collections.map((collection: any) => ({ + id: collection.id, + name: collection.displayName || collection.slug || collection.id, + })) + + return NextResponse.json({ collections: formattedCollections }, { status: 200 }) + } catch (error: any) { + logger.error('Error fetching Webflow collections', error) + return NextResponse.json( + { error: 'Internal server error', details: error.message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/tools/webflow/sites/route.ts b/apps/sim/app/api/tools/webflow/sites/route.ts new file mode 100644 index 0000000000..f94c3e3406 --- /dev/null +++ b/apps/sim/app/api/tools/webflow/sites/route.ts @@ -0,0 +1,61 @@ +import { type NextRequest, NextResponse } from 'next/server' +import { getSession } from '@/lib/auth' +import { createLogger } from '@/lib/logs/console/logger' +import { getOAuthToken } from '@/app/api/auth/oauth/utils' + +const logger = createLogger('WebflowSitesAPI') + +export const dynamic = 'force-dynamic' + +export async function GET(request: NextRequest) { + try { + const session = await getSession() + if (!session?.user?.id) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }) + } + + const accessToken = await getOAuthToken(session.user.id, 'webflow') + + if (!accessToken) { + return NextResponse.json( + { error: 'No Webflow access token found. Please connect your Webflow account.' }, + { status: 404 } + ) + } + + const response = await fetch('https://api.webflow.com/v2/sites', { + headers: { + Authorization: `Bearer ${accessToken}`, + accept: 'application/json', + }, + }) + + if (!response.ok) { + const errorData = await response.json().catch(() => ({})) + logger.error('Failed to fetch Webflow sites', { + status: response.status, + error: errorData, + }) + return NextResponse.json( + { error: 'Failed to fetch Webflow sites', details: errorData }, + { status: response.status } + ) + } + + const data = await response.json() + const sites = data.sites || [] + + const formattedSites = sites.map((site: any) => ({ + id: site.id, + name: site.displayName || site.shortName || site.id, + })) + + return NextResponse.json({ sites: formattedSites }, { status: 200 }) + } catch (error: any) { + logger.error('Error fetching Webflow sites', error) + return NextResponse.json( + { error: 'Internal server error', details: error.message }, + { status: 500 } + ) + } +} diff --git a/apps/sim/app/api/webhooks/route.ts b/apps/sim/app/api/webhooks/route.ts index f4fcfbe94c..5e2d6ba94f 100644 --- a/apps/sim/app/api/webhooks/route.ts +++ b/apps/sim/app/api/webhooks/route.ts @@ -428,6 +428,26 @@ export async function POST(request: NextRequest) { } // --- End Outlook specific logic --- + // --- Webflow webhook setup --- + if (savedWebhook && provider === 'webflow') { + logger.info( + `[${requestId}] Webflow provider detected. Attempting to create webhook in Webflow.` + ) + try { + await createWebflowWebhookSubscription(request, userId, savedWebhook, requestId) + } catch (err) { + logger.error(`[${requestId}] Error creating Webflow webhook`, err) + return NextResponse.json( + { + error: 'Failed to create webhook in Webflow', + details: err instanceof Error ? err.message : 'Unknown error', + }, + { status: 500 } + ) + } + } + // --- End Webflow specific logic --- + const status = targetWebhookId ? 200 : 201 return NextResponse.json({ webhook: savedWebhook }, { status }) } catch (error: any) { @@ -548,3 +568,136 @@ async function createAirtableWebhookSubscription( ) } } +// Helper function to create the webhook subscription in Webflow +async function createWebflowWebhookSubscription( + request: NextRequest, + userId: string, + webhookData: any, + requestId: string +) { + try { + const { path, providerConfig } = webhookData + const { siteId, triggerId, collectionId, formId } = providerConfig || {} + + if (!siteId) { + logger.warn(`[${requestId}] Missing siteId for Webflow webhook creation.`, { + webhookId: webhookData.id, + }) + throw new Error('Site ID is required to create Webflow webhook') + } + + if (!triggerId) { + logger.warn(`[${requestId}] Missing triggerId for Webflow webhook creation.`, { + webhookId: webhookData.id, + }) + throw new Error('Trigger type is required to create Webflow webhook') + } + + const accessToken = await getOAuthToken(userId, 'webflow') + if (!accessToken) { + logger.warn( + `[${requestId}] Could not retrieve Webflow access token for user ${userId}. Cannot create webhook in Webflow.` + ) + throw new Error( + 'Webflow account connection required. Please connect your Webflow account in the trigger configuration and try again.' + ) + } + + const notificationUrl = `${getBaseUrl()}/api/webhooks/trigger/${path}` + + // Map trigger IDs to Webflow trigger types + const triggerTypeMap: Record = { + webflow_collection_item_created: 'collection_item_created', + webflow_collection_item_changed: 'collection_item_changed', + webflow_collection_item_deleted: 'collection_item_deleted', + webflow_form_submission: 'form_submission', + } + + const webflowTriggerType = triggerTypeMap[triggerId] + if (!webflowTriggerType) { + logger.warn(`[${requestId}] Invalid triggerId for Webflow: ${triggerId}`, { + webhookId: webhookData.id, + }) + throw new Error(`Invalid Webflow trigger type: ${triggerId}`) + } + + const webflowApiUrl = `https://api.webflow.com/v2/sites/${siteId}/webhooks` + + const requestBody: any = { + triggerType: webflowTriggerType, + url: notificationUrl, + } + + // Add filter for collection-based triggers + if (collectionId && webflowTriggerType.startsWith('collection_item_')) { + requestBody.filter = { + resource_type: 'collection', + resource_id: collectionId, + } + } + + // Add filter for form submissions + if (formId && webflowTriggerType === 'form_submission') { + requestBody.filter = { + resource_type: 'form', + resource_id: formId, + } + } + + const webflowResponse = await fetch(webflowApiUrl, { + method: 'POST', + headers: { + Authorization: `Bearer ${accessToken}`, + 'Content-Type': 'application/json', + accept: 'application/json', + }, + body: JSON.stringify(requestBody), + }) + + const responseBody = await webflowResponse.json() + + if (!webflowResponse.ok || responseBody.error) { + const errorMessage = responseBody.message || responseBody.error || 'Unknown Webflow API error' + logger.error( + `[${requestId}] Failed to create webhook in Webflow for webhook ${webhookData.id}. Status: ${webflowResponse.status}`, + { message: errorMessage, response: responseBody } + ) + throw new Error(errorMessage) + } + + logger.info( + `[${requestId}] Successfully created webhook in Webflow for webhook ${webhookData.id}.`, + { + webflowWebhookId: responseBody.id || responseBody._id, + } + ) + + // Store the Webflow webhook ID in the providerConfig + try { + const currentConfig = (webhookData.providerConfig as Record) || {} + const updatedConfig = { + ...currentConfig, + externalId: responseBody.id || responseBody._id, + } + await db + .update(webhook) + .set({ providerConfig: updatedConfig, updatedAt: new Date() }) + .where(eq(webhook.id, webhookData.id)) + } catch (dbError: any) { + logger.error( + `[${requestId}] Failed to store externalId in providerConfig for webhook ${webhookData.id}.`, + dbError + ) + // Even if saving fails, the webhook exists in Webflow. Log and continue. + } + } catch (error: any) { + logger.error( + `[${requestId}] Exception during Webflow webhook creation for webhook ${webhookData.id}.`, + { + message: error.message, + stack: error.stack, + } + ) + throw error + } +} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx index eb9df9cb67..19d1dd67fb 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/credential-selector/components/oauth-required-modal.tsx @@ -115,6 +115,10 @@ const SCOPE_DESCRIPTIONS: Record = { 'users:read': 'View workspace users', 'files:write': 'Upload files', 'canvases:write': 'Create canvas documents', + 'sites:read': 'View your Webflow sites', + 'sites:write': 'Manage webhooks and site settings', + 'cms:read': 'View your CMS content', + 'cms:write': 'Manage your CMS content', } // Convert OAuth scope to user-friendly description diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx index 881cc8c4d8..b1c787b3a0 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-config-section.tsx @@ -35,6 +35,7 @@ interface TriggerConfigSectionProps { onChange: (fieldId: string, value: any) => void webhookUrl: string dynamicOptions?: Record | string[]> + loadingFields?: Record } export function TriggerConfigSection({ @@ -44,6 +45,7 @@ export function TriggerConfigSection({ onChange, webhookUrl, dynamicOptions = {}, + loadingFields = {}, }: TriggerConfigSectionProps) { const [showSecrets, setShowSecrets] = useState>({}) const [copied, setCopied] = useState(null) @@ -81,27 +83,43 @@ export function TriggerConfigSection({ ) case 'select': { + const rawOptions = dynamicOptions?.[fieldId] || fieldDef.options || [] + const isLoading = loadingFields[fieldId] || false + + const availableOptions = Array.isArray(rawOptions) + ? rawOptions.map((option: any) => { + if (typeof option === 'string') { + return { id: option, name: option } + } + return option + }) + : [] + return (
-
) } @@ -352,9 +370,14 @@ export function TriggerConfigSection({ } } + // Show webhook URL only for manual webhooks (have webhook config but no OAuth auto-registration) + // Auto-registered webhooks (like Webflow, Airtable) have requiresCredentials and register via API + // Polling triggers (like Gmail) don't have webhook property at all + const shouldShowWebhookUrl = webhookUrl && triggerDef.webhook && !triggerDef.requiresCredentials + return (
- {webhookUrl && ( + {shouldShowWebhookUrl && (
diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx index 5be21dd2a6..36cee6948b 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/components/trigger-modal.tsx @@ -96,6 +96,7 @@ export function TriggerModal({ const [dynamicOptions, setDynamicOptions] = useState< Record> >({}) + const [loadingFields, setLoadingFields] = useState>({}) const lastCredentialIdRef = useRef(null) const [testUrl, setTestUrl] = useState(null) const [testUrlExpiresAt, setTestUrlExpiresAt] = useState(null) @@ -113,6 +114,9 @@ export function TriggerModal({ } else if (triggerDef.provider === 'airtable') { if (typeof next.baseId === 'string') next.baseId = '' if (typeof next.tableId === 'string') next.tableId = '' + } else if (triggerDef.provider === 'webflow') { + if (typeof next.siteId === 'string') next.siteId = '' + if (typeof next.collectionId === 'string') next.collectionId = '' } return next }) @@ -177,6 +181,8 @@ export function TriggerModal({ void loadGmailLabels(currentCredentialId) } else if (triggerDef.provider === 'outlook') { void loadOutlookFolders(currentCredentialId) + } else if (triggerDef.provider === 'webflow') { + void loadWebflowSites() } } return @@ -197,6 +203,8 @@ export function TriggerModal({ void loadGmailLabels(currentCredentialId) } else if (triggerDef.provider === 'outlook') { void loadOutlookFolders(currentCredentialId) + } else if (triggerDef.provider === 'webflow') { + void loadWebflowSites() } } } @@ -262,9 +270,57 @@ export function TriggerModal({ } } - // Generate webhook path and URL + const loadWebflowSites = async () => { + setLoadingFields((prev) => ({ ...prev, siteId: true })) + try { + const response = await fetch('/api/tools/webflow/sites') + if (response.ok) { + const data = await response.json() + if (data.sites && Array.isArray(data.sites)) { + setDynamicOptions((prev) => ({ + ...prev, + siteId: data.sites, + })) + } + } else { + logger.error('Failed to load Webflow sites:', response.statusText) + } + } catch (error) { + logger.error('Error loading Webflow sites:', error) + } finally { + setLoadingFields((prev) => ({ ...prev, siteId: false })) + } + } + + const loadWebflowCollections = async (siteId: string) => { + setLoadingFields((prev) => ({ ...prev, collectionId: true })) + try { + const response = await fetch(`/api/tools/webflow/collections?siteId=${siteId}`) + if (response.ok) { + const data = await response.json() + if (data.collections && Array.isArray(data.collections)) { + setDynamicOptions((prev) => ({ + ...prev, + collectionId: data.collections, + })) + } + } else { + logger.error('Failed to load Webflow collections:', response.statusText) + } + } catch (error) { + logger.error('Error loading Webflow collections:', error) + } finally { + setLoadingFields((prev) => ({ ...prev, collectionId: false })) + } + } + + useEffect(() => { + if (triggerDef.provider === 'webflow' && config.siteId) { + void loadWebflowCollections(config.siteId) + } + }, [config.siteId, triggerDef.provider]) + useEffect(() => { - // For triggers that don't use webhooks (like Gmail polling), skip URL generation if (triggerDef.requiresCredentials && !triggerDef.webhook) { setWebhookUrl('') setGeneratedPath('') @@ -273,14 +329,11 @@ export function TriggerModal({ let finalPath = triggerPath - // If no path exists and we haven't generated one yet, generate one if (!finalPath && !generatedPath) { - // Use UUID format consistent with other webhooks const newPath = crypto.randomUUID() setGeneratedPath(newPath) finalPath = newPath } else if (generatedPath && !triggerPath) { - // Use the already generated path finalPath = generatedPath } @@ -538,6 +591,7 @@ export function TriggerModal({ onChange={handleConfigChange} webhookUrl={webhookUrl} dynamicOptions={dynamicOptions} + loadingFields={loadingFields} /> {triggerDef.webhook && ( @@ -684,17 +738,15 @@ export function TriggerModal({ (!(hasConfigChanged || hasCredentialChanged) && !!triggerId) } className={cn( - 'h-9 rounded-[8px]', + 'w-[140px] rounded-[8px]', isConfigValid() && (hasConfigChanged || hasCredentialChanged || !triggerId) ? 'bg-primary hover:bg-primary/90' - : '', - isSaving && - 'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20' + : '' )} - size='default' + size='sm' > {isSaving && ( -
+
)} {isSaving ? 'Saving...' : 'Save Changes'} diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/trigger-config.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/trigger-config.tsx index 3307556864..eb1a25a72a 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/trigger-config.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/trigger-config/trigger-config.tsx @@ -145,21 +145,14 @@ export function TriggerConfig({ throw new Error('Trigger definition not found') } - // Set the trigger path and config in the block state if (path && path !== triggerPath) { setTriggerPath(path) } setTriggerConfig(config) setStoredTriggerId(effectiveTriggerId) - // Map trigger ID to webhook provider name - const webhookProvider = effectiveTriggerId - .replace(/_chat_subscription$/, '') - .replace(/_webhook$/, '') - .replace(/_poller$/, '') - .replace(/_subscription$/, '') // e.g., 'slack_webhook' -> 'slack', 'gmail_poller' -> 'gmail', 'microsoftteams_chat_subscription' -> 'microsoftteams' + const webhookProvider = triggerDef.provider - // Include selected credential from the modal (if any) const selectedCredentialId = (useSubBlockStore.getState().getValue(blockId, 'triggerCredentials') as string | null) || null diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/index.ts b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/index.ts index 554e5c652e..3609d47f6d 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/index.ts +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/index.ts @@ -7,4 +7,5 @@ export { OutlookConfig } from './outlook' export { SlackConfig } from './slack' export { StripeConfig } from './stripe' export { TelegramConfig } from './telegram' +export { WebflowConfig } from './webflow' export { WhatsAppConfig } from './whatsapp' diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/webflow.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/webflow.tsx new file mode 100644 index 0000000000..eb8be7c12a --- /dev/null +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components/providers/webflow.tsx @@ -0,0 +1,145 @@ +import { Input, Skeleton } from '@/components/ui' +import { + ConfigField, + ConfigSection, + InstructionsSection, + TestResultDisplay as WebhookTestResult, +} from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/components/webhook/components' + +interface WebflowConfigProps { + siteId: string + setSiteId: (value: string) => void + collectionId?: string + setCollectionId?: (value: string) => void + formId?: string + setFormId?: (value: string) => void + isLoadingToken: boolean + testResult: any + copied: string | null + copyToClipboard: (text: string, type: string) => void + testWebhook?: () => void + webhookId?: string + triggerType?: string // The selected trigger type to show relevant fields +} + +export function WebflowConfig({ + siteId, + setSiteId, + collectionId, + setCollectionId, + formId, + setFormId, + isLoadingToken, + testResult, + copied, + copyToClipboard, + testWebhook, + webhookId, + triggerType, +}: WebflowConfigProps) { + const isCollectionTrigger = triggerType?.includes('collection_item') || !triggerType + const isFormTrigger = triggerType?.includes('form_submission') + + return ( +
+ + + {isLoadingToken ? ( + + ) : ( + setSiteId(e.target.value)} + placeholder='6c3592' + required + /> + )} + + + {isCollectionTrigger && setCollectionId && ( + + {isLoadingToken ? ( + + ) : ( + setCollectionId(e.target.value)} + placeholder='68f9666257aa8abaa9b0b6d6' + /> + )} + + )} + + {isFormTrigger && setFormId && ( + + {isLoadingToken ? ( + + ) : ( + setFormId(e.target.value)} + placeholder='form-contact' + /> + )} + + )} + + + {testResult && ( + + )} + + +
    +
  1. Connect your Webflow account using the credential selector above.
  2. +
  3. Enter your Webflow Site ID (found in the site URL or site settings).
  4. + {isCollectionTrigger && ( + <> +
  5. + Optionally enter a Collection ID to monitor only specific collections (leave empty + to monitor all). +
  6. +
  7. + The webhook will trigger when items are created, changed, or deleted in the + specified collection(s). +
  8. + + )} + {isFormTrigger && ( + <> +
  9. + Optionally enter a Form ID to monitor only a specific form (leave empty to monitor + all forms). +
  10. +
  11. The webhook will trigger whenever a form is submitted on your site.
  12. + + )} +
  13. + Sim will automatically register the webhook with Webflow when you save this + configuration. +
  14. +
  15. Make sure your Webflow account has appropriate permissions for the site.
  16. +
+
+
+ ) +} diff --git a/apps/sim/blocks/blocks/discord.ts b/apps/sim/blocks/blocks/discord.ts index f3948da47b..78a48619d9 100644 --- a/apps/sim/blocks/blocks/discord.ts +++ b/apps/sim/blocks/blocks/discord.ts @@ -11,7 +11,7 @@ export const DiscordBlock: BlockConfig = { longDescription: 'Integrate Discord into the workflow. Can send and get messages, get server information, and get a user’s information.', category: 'tools', - bgColor: '#E0E0E0', + bgColor: '#5865F2', icon: DiscordIcon, subBlocks: [ { diff --git a/apps/sim/blocks/blocks/webflow.ts b/apps/sim/blocks/blocks/webflow.ts new file mode 100644 index 0000000000..29daa2c91a --- /dev/null +++ b/apps/sim/blocks/blocks/webflow.ts @@ -0,0 +1,190 @@ +import { WebflowIcon } from '@/components/icons' +import type { BlockConfig } from '@/blocks/types' +import { AuthMode } from '@/blocks/types' +import type { WebflowResponse } from '@/tools/webflow/types' + +export const WebflowBlock: BlockConfig = { + type: 'webflow', + name: 'Webflow', + description: 'Manage Webflow CMS collections', + authMode: AuthMode.OAuth, + longDescription: + 'Integrates Webflow CMS into the workflow. Can create, get, list, update, or delete items in Webflow CMS collections. Manage your Webflow content programmatically. Can be used in trigger mode to trigger workflows when collection items change or forms are submitted.', + docsLink: 'https://docs.sim.ai/tools/webflow', + category: 'tools', + triggerAllowed: true, + bgColor: '#E0E0E0', + icon: WebflowIcon, + subBlocks: [ + { + id: 'operation', + title: 'Operation', + type: 'dropdown', + layout: 'full', + options: [ + { label: 'List Items', id: 'list' }, + { label: 'Get Item', id: 'get' }, + { label: 'Create Item', id: 'create' }, + { label: 'Update Item', id: 'update' }, + { label: 'Delete Item', id: 'delete' }, + ], + value: () => 'list', + }, + { + id: 'credential', + title: 'Webflow Account', + type: 'oauth-input', + layout: 'full', + provider: 'webflow', + serviceId: 'webflow', + requiredScopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'], + placeholder: 'Select Webflow account', + required: true, + }, + { + id: 'collectionId', + title: 'Collection ID', + type: 'short-input', + layout: 'full', + placeholder: 'Enter collection ID', + dependsOn: ['credential'], + required: true, + }, + { + id: 'itemId', + title: 'Item ID', + type: 'short-input', + layout: 'full', + placeholder: 'ID of the item', + condition: { field: 'operation', value: ['get', 'update', 'delete'] }, + required: true, + }, + { + id: 'offset', + title: 'Offset', + type: 'short-input', + layout: 'half', + placeholder: 'Pagination offset (optional)', + condition: { field: 'operation', value: 'list' }, + }, + { + id: 'limit', + title: 'Limit', + type: 'short-input', + layout: 'half', + placeholder: 'Max items to return (optional)', + condition: { field: 'operation', value: 'list' }, + }, + { + id: 'fieldData', + title: 'Field Data', + type: 'code', + layout: 'full', + language: 'json', + placeholder: 'Field data as JSON: `{ "name": "Item Name", "slug": "item-slug" }`', + condition: { field: 'operation', value: ['create', 'update'] }, + required: true, + }, + { + id: 'triggerConfig', + title: 'Trigger Configuration', + type: 'trigger-config', + layout: 'full', + triggerProvider: 'webflow', + availableTriggers: [ + 'webflow_collection_item_created', + 'webflow_collection_item_changed', + 'webflow_collection_item_deleted', + 'webflow_form_submission', + ], + }, + ], + tools: { + access: [ + 'webflow_list_items', + 'webflow_get_item', + 'webflow_create_item', + 'webflow_update_item', + 'webflow_delete_item', + ], + config: { + tool: (params) => { + switch (params.operation) { + case 'list': + return 'webflow_list_items' + case 'get': + return 'webflow_get_item' + case 'create': + return 'webflow_create_item' + case 'update': + return 'webflow_update_item' + case 'delete': + return 'webflow_delete_item' + default: + throw new Error(`Invalid Webflow operation: ${params.operation}`) + } + }, + params: (params) => { + const { credential, fieldData, ...rest } = params + let parsedFieldData: any | undefined + + try { + if (fieldData && (params.operation === 'create' || params.operation === 'update')) { + parsedFieldData = JSON.parse(fieldData) + } + } catch (error: any) { + throw new Error(`Invalid JSON input for ${params.operation} operation: ${error.message}`) + } + + const baseParams = { + credential, + ...rest, + } + + switch (params.operation) { + case 'create': + case 'update': + return { ...baseParams, fieldData: parsedFieldData } + default: + return baseParams + } + }, + }, + }, + inputs: { + operation: { type: 'string', description: 'Operation to perform' }, + credential: { type: 'string', description: 'Webflow OAuth access token' }, + collectionId: { type: 'string', description: 'Webflow collection identifier' }, + // Conditional inputs + itemId: { type: 'string', description: 'Item identifier' }, // Required for get/update/delete + offset: { type: 'number', description: 'Pagination offset' }, // Optional for list + limit: { type: 'number', description: 'Maximum items to return' }, // Optional for list + fieldData: { type: 'json', description: 'Item field data' }, // Required for create/update + }, + outputs: { + items: { type: 'json', description: 'Array of items (list operation)' }, + item: { type: 'json', description: 'Single item data (get/create/update operations)' }, + success: { type: 'boolean', description: 'Operation success status (delete operation)' }, + metadata: { type: 'json', description: 'Operation metadata' }, + // Trigger outputs + siteId: { type: 'string', description: 'Site ID where event occurred' }, + workspaceId: { type: 'string', description: 'Workspace ID where event occurred' }, + collectionId: { type: 'string', description: 'Collection ID (for collection events)' }, + payload: { type: 'json', description: 'Event payload data (item data for collection events)' }, + name: { type: 'string', description: 'Form name (for form submissions)' }, + id: { type: 'string', description: 'Submission ID (for form submissions)' }, + submittedAt: { type: 'string', description: 'Submission timestamp (for form submissions)' }, + data: { type: 'json', description: 'Form field data (for form submissions)' }, + schema: { type: 'json', description: 'Form schema (for form submissions)' }, + formElementId: { type: 'string', description: 'Form element ID (for form submissions)' }, + }, + triggers: { + enabled: true, + available: [ + 'webflow_collection_item_created', + 'webflow_collection_item_changed', + 'webflow_collection_item_deleted', + 'webflow_form_submission', + ], + }, +} diff --git a/apps/sim/blocks/registry.ts b/apps/sim/blocks/registry.ts index f84c842f32..f0d4465cd6 100644 --- a/apps/sim/blocks/registry.ts +++ b/apps/sim/blocks/registry.ts @@ -1,8 +1,3 @@ -/** - * Blocks Registry - * - */ - import { AgentBlock } from '@/blocks/blocks/agent' import { AirtableBlock } from '@/blocks/blocks/airtable' import { ApiBlock } from '@/blocks/blocks/api' @@ -80,6 +75,7 @@ import { TwilioSMSBlock } from '@/blocks/blocks/twilio' import { TypeformBlock } from '@/blocks/blocks/typeform' import { VisionBlock } from '@/blocks/blocks/vision' import { WealthboxBlock } from '@/blocks/blocks/wealthbox' +import { WebflowBlock } from '@/blocks/blocks/webflow' import { WebhookBlock } from '@/blocks/blocks/webhook' import { WhatsAppBlock } from '@/blocks/blocks/whatsapp' import { WikipediaBlock } from '@/blocks/blocks/wikipedia' @@ -171,6 +167,7 @@ export const registry: Record = { typeform: TypeformBlock, vision: VisionBlock, wealthbox: WealthboxBlock, + webflow: WebflowBlock, webhook: WebhookBlock, whatsapp: WhatsAppBlock, wikipedia: WikipediaBlock, diff --git a/apps/sim/components/icons.tsx b/apps/sim/components/icons.tsx index 38fe8b0057..294024300e 100644 --- a/apps/sim/components/icons.tsx +++ b/apps/sim/components/icons.tsx @@ -3775,3 +3775,23 @@ export function ZepIcon(props: SVGProps) { ) } + +export function WebflowIcon(props: SVGProps) { + return ( + + + + ) +} diff --git a/apps/sim/lib/auth.ts b/apps/sim/lib/auth.ts index 74030c7c3a..1bc7bc06b9 100644 --- a/apps/sim/lib/auth.ts +++ b/apps/sim/lib/auth.ts @@ -152,6 +152,7 @@ export const auth = betterAuth({ 'microsoft', 'slack', 'reddit', + 'webflow', // Common SSO provider patterns ...SSO_TRUSTED_PROVIDERS, @@ -1214,6 +1215,56 @@ export const auth = betterAuth({ } }, }, + + // Webflow provider + { + providerId: 'webflow', + clientId: env.WEBFLOW_CLIENT_ID as string, + clientSecret: env.WEBFLOW_CLIENT_SECRET as string, + authorizationUrl: 'https://webflow.com/oauth/authorize', + tokenUrl: 'https://api.webflow.com/oauth/access_token', + userInfoUrl: 'https://api.webflow.com/v2/token/introspect', + scopes: ['sites:read', 'sites:write', 'cms:read', 'cms:write'], + responseType: 'code', + redirectURI: `${getBaseUrl()}/api/auth/oauth2/callback/webflow`, + getUserInfo: async (tokens) => { + try { + logger.info('Fetching Webflow user info') + + const response = await fetch('https://api.webflow.com/v2/token/introspect', { + headers: { + Authorization: `Bearer ${tokens.accessToken}`, + }, + }) + + if (!response.ok) { + logger.error('Error fetching Webflow user info:', { + status: response.status, + statusText: response.statusText, + }) + return null + } + + const data = await response.json() + const now = new Date() + + const userId = data.user_id || `webflow-${Date.now()}` + const uniqueId = `webflow-${userId}` + + return { + id: uniqueId, + name: data.user_name || 'Webflow User', + email: `${uniqueId.replace(/[^a-zA-Z0-9]/g, '')}@webflow.user`, + emailVerified: false, + createdAt: now, + updatedAt: now, + } + } catch (error) { + logger.error('Error in Webflow getUserInfo:', { error }) + return null + } + }, + }, ], }), // Include SSO plugin when enabled diff --git a/apps/sim/lib/env.ts b/apps/sim/lib/env.ts index a200e419de..6820a5832c 100644 --- a/apps/sim/lib/env.ts +++ b/apps/sim/lib/env.ts @@ -201,6 +201,8 @@ export const env = createEnv({ SLACK_CLIENT_SECRET: z.string().optional(), // Slack OAuth client secret REDDIT_CLIENT_ID: z.string().optional(), // Reddit OAuth client ID REDDIT_CLIENT_SECRET: z.string().optional(), // Reddit OAuth client secret + WEBFLOW_CLIENT_ID: z.string().optional(), // Webflow OAuth client ID + WEBFLOW_CLIENT_SECRET: z.string().optional(), // Webflow OAuth client secret // E2B Remote Code Execution E2B_ENABLED: z.string().optional(), // Enable E2B remote code execution diff --git a/apps/sim/lib/oauth/oauth.ts b/apps/sim/lib/oauth/oauth.ts index a253d02b71..7dec2068e3 100644 --- a/apps/sim/lib/oauth/oauth.ts +++ b/apps/sim/lib/oauth/oauth.ts @@ -25,6 +25,7 @@ import { SlackIcon, SupabaseIcon, WealthboxIcon, + WebflowIcon, xIcon, } from '@/components/icons' import { env } from '@/lib/env' @@ -47,6 +48,7 @@ export type OAuthProvider = | 'slack' | 'reddit' | 'wealthbox' + | 'webflow' | string export type OAuthService = @@ -76,6 +78,7 @@ export type OAuthService = | 'reddit' | 'wealthbox' | 'onedrive' + | 'webflow' export interface OAuthProviderConfig { id: OAuthProvider name: string @@ -500,6 +503,23 @@ export const OAUTH_PROVIDERS: Record = { }, defaultService: 'wealthbox', }, + webflow: { + id: 'webflow', + name: 'Webflow', + icon: (props) => WebflowIcon(props), + services: { + webflow: { + id: 'webflow', + name: 'Webflow', + description: 'Manage Webflow CMS collections, sites, and content.', + providerId: 'webflow', + icon: (props) => WebflowIcon(props), + baseProviderIcon: (props) => WebflowIcon(props), + scopes: ['cms:read', 'cms:write', 'sites:read', 'sites:write'], + }, + }, + defaultService: 'webflow', + }, } // Helper function to get a service by provider and service ID @@ -584,6 +604,8 @@ export function getServiceIdFromScopes(provider: OAuthProvider, scopes: string[] return 'reddit' } else if (provider === 'wealthbox') { return 'wealthbox' + } else if (provider === 'webflow') { + return 'webflow' } return providerConfig.defaultService @@ -886,6 +908,19 @@ function getProviderAuthConfig(provider: string): ProviderAuthConfig { supportsRefreshTokenRotation: true, } } + case 'webflow': { + const { clientId, clientSecret } = getCredentials( + env.WEBFLOW_CLIENT_ID, + env.WEBFLOW_CLIENT_SECRET + ) + return { + tokenEndpoint: 'https://api.webflow.com/oauth/access_token', + clientId, + clientSecret, + useBasicAuth: false, + supportsRefreshTokenRotation: false, + } + } default: throw new Error(`Unsupported provider: ${provider}`) } diff --git a/apps/sim/lib/webhooks/utils.ts b/apps/sim/lib/webhooks/utils.ts index 6a57571b99..bb031dde86 100644 --- a/apps/sim/lib/webhooks/utils.ts +++ b/apps/sim/lib/webhooks/utils.ts @@ -900,6 +900,57 @@ export async function formatWebhookInput( } } + if (foundWebhook.provider === 'webflow') { + const triggerType = body?.triggerType || 'unknown' + const siteId = body?.siteId || '' + const workspaceId = body?.workspaceId || '' + const collectionId = body?.collectionId || '' + const payload = body?.payload || {} + const formId = body?.formId || '' + const formName = body?.name || '' + const formSubmissionId = body?.id || '' + const submittedAt = body?.submittedAt || '' + const formData = body?.data || {} + const schema = body?.schema || {} + + return { + siteId, + workspaceId, + collectionId, + payload, + triggerType, + + formId, + name: formName, + id: formSubmissionId, + submittedAt, + data: formData, + schema, + formElementId: body?.formElementId || '', + + webflow: { + siteId, + workspaceId, + collectionId, + payload, + triggerType, + raw: body, + }, + + webhook: { + data: { + provider: 'webflow', + path: foundWebhook.path, + providerConfig: foundWebhook.providerConfig, + payload: body, + headers: Object.fromEntries(request.headers.entries()), + method: request.method, + }, + }, + workflowId: foundWorkflow.id, + } + } + if (foundWebhook.provider === 'generic') { return body } diff --git a/apps/sim/tools/registry.ts b/apps/sim/tools/registry.ts index 9f93d4271a..05673c1c94 100644 --- a/apps/sim/tools/registry.ts +++ b/apps/sim/tools/registry.ts @@ -205,6 +205,13 @@ import { wealthboxWriteNoteTool, wealthboxWriteTaskTool, } from '@/tools/wealthbox' +import { + webflowCreateItemTool, + webflowDeleteItemTool, + webflowGetItemTool, + webflowListItemsTool, + webflowUpdateItemTool, +} from '@/tools/webflow' import { whatsappSendMessageTool } from '@/tools/whatsapp' import { wikipediaPageContentTool, @@ -420,6 +427,11 @@ export const tools: Record = { wealthbox_write_task: wealthboxWriteTaskTool, wealthbox_read_note: wealthboxReadNoteTool, wealthbox_write_note: wealthboxWriteNoteTool, + webflow_list_items: webflowListItemsTool, + webflow_get_item: webflowGetItemTool, + webflow_create_item: webflowCreateItemTool, + webflow_update_item: webflowUpdateItemTool, + webflow_delete_item: webflowDeleteItemTool, wikipedia_summary: wikipediaPageSummaryTool, wikipedia_search: wikipediaSearchTool, wikipedia_content: wikipediaPageContentTool, diff --git a/apps/sim/tools/webflow/create_item.ts b/apps/sim/tools/webflow/create_item.ts new file mode 100644 index 0000000000..7dd736b23b --- /dev/null +++ b/apps/sim/tools/webflow/create_item.ts @@ -0,0 +1,73 @@ +import type { ToolConfig } from '@/tools/types' +import type { WebflowCreateItemParams, WebflowCreateItemResponse } from '@/tools/webflow/types' + +export const webflowCreateItemTool: ToolConfig = + { + id: 'webflow_create_item', + name: 'Webflow Create Item', + description: 'Create a new item in a Webflow CMS collection', + version: '1.0.0', + + oauth: { + required: true, + provider: 'webflow', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + collectionId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ID of the collection', + }, + fieldData: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Field data for the new item as a JSON object. Keys should match collection field names.', + }, + }, + + request: { + url: (params) => `https://api.webflow.com/v2/collections/${params.collectionId}/items`, + method: 'POST', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + fieldData: params.fieldData, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + item: data, + metadata: { + itemId: data.id || 'unknown', + }, + }, + } + }, + + outputs: { + item: { + type: 'json', + description: 'The created item object', + }, + metadata: { + type: 'json', + description: 'Metadata about the created item', + }, + }, + } diff --git a/apps/sim/tools/webflow/delete_item.ts b/apps/sim/tools/webflow/delete_item.ts new file mode 100644 index 0000000000..f90e73c42a --- /dev/null +++ b/apps/sim/tools/webflow/delete_item.ts @@ -0,0 +1,70 @@ +import type { ToolConfig } from '@/tools/types' +import type { WebflowDeleteItemParams, WebflowDeleteItemResponse } from '@/tools/webflow/types' + +export const webflowDeleteItemTool: ToolConfig = + { + id: 'webflow_delete_item', + name: 'Webflow Delete Item', + description: 'Delete an item from a Webflow CMS collection', + version: '1.0.0', + + oauth: { + required: true, + provider: 'webflow', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + collectionId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ID of the collection', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the item to delete', + }, + }, + + request: { + url: (params) => + `https://api.webflow.com/v2/collections/${params.collectionId}/items/${params.itemId}`, + method: 'DELETE', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response) => { + const isSuccess = response.status === 204 || response.ok + + return { + success: isSuccess, + output: { + success: isSuccess, + metadata: { + deleted: isSuccess, + }, + }, + } + }, + + outputs: { + success: { + type: 'boolean', + description: 'Whether the deletion was successful', + }, + metadata: { + type: 'json', + description: 'Metadata about the deletion', + }, + }, + } diff --git a/apps/sim/tools/webflow/get_item.ts b/apps/sim/tools/webflow/get_item.ts new file mode 100644 index 0000000000..08559693aa --- /dev/null +++ b/apps/sim/tools/webflow/get_item.ts @@ -0,0 +1,68 @@ +import type { ToolConfig } from '@/tools/types' +import type { WebflowGetItemParams, WebflowGetItemResponse } from '@/tools/webflow/types' + +export const webflowGetItemTool: ToolConfig = { + id: 'webflow_get_item', + name: 'Webflow Get Item', + description: 'Get a single item from a Webflow CMS collection', + version: '1.0.0', + + oauth: { + required: true, + provider: 'webflow', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + collectionId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ID of the collection', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the item to retrieve', + }, + }, + + request: { + url: (params) => + `https://api.webflow.com/v2/collections/${params.collectionId}/items/${params.itemId}`, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + item: data, + metadata: { + itemId: data.id || 'unknown', + }, + }, + } + }, + + outputs: { + item: { + type: 'json', + description: 'The retrieved item object', + }, + metadata: { + type: 'json', + description: 'Metadata about the retrieved item', + }, + }, +} diff --git a/apps/sim/tools/webflow/index.ts b/apps/sim/tools/webflow/index.ts new file mode 100644 index 0000000000..c46fa5610f --- /dev/null +++ b/apps/sim/tools/webflow/index.ts @@ -0,0 +1,13 @@ +import { webflowCreateItemTool } from '@/tools/webflow/create_item' +import { webflowDeleteItemTool } from '@/tools/webflow/delete_item' +import { webflowGetItemTool } from '@/tools/webflow/get_item' +import { webflowListItemsTool } from '@/tools/webflow/list_items' +import { webflowUpdateItemTool } from '@/tools/webflow/update_item' + +export { + webflowCreateItemTool, + webflowDeleteItemTool, + webflowGetItemTool, + webflowListItemsTool, + webflowUpdateItemTool, +} diff --git a/apps/sim/tools/webflow/list_items.ts b/apps/sim/tools/webflow/list_items.ts new file mode 100644 index 0000000000..10f3262f28 --- /dev/null +++ b/apps/sim/tools/webflow/list_items.ts @@ -0,0 +1,88 @@ +import type { ToolConfig } from '@/tools/types' +import type { WebflowListItemsParams, WebflowListItemsResponse } from '@/tools/webflow/types' + +export const webflowListItemsTool: ToolConfig = { + id: 'webflow_list_items', + name: 'Webflow List Items', + description: 'List all items from a Webflow CMS collection', + version: '1.0.0', + + oauth: { + required: true, + provider: 'webflow', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + collectionId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ID of the collection', + }, + offset: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Offset for pagination (optional)', + }, + limit: { + type: 'number', + required: false, + visibility: 'user-or-llm', + description: 'Maximum number of items to return (optional, default: 100)', + }, + }, + + request: { + url: (params) => { + const baseUrl = `https://api.webflow.com/v2/collections/${params.collectionId}/items` + const queryParams = new URLSearchParams() + + if (params.offset !== undefined) { + queryParams.append('offset', params.offset.toString()) + } + if (params.limit !== undefined) { + queryParams.append('limit', params.limit.toString()) + } + + const query = queryParams.toString() + return query ? `${baseUrl}?${query}` : baseUrl + }, + method: 'GET', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + items: data.items || [], + metadata: { + itemCount: (data.items || []).length, + offset: data.offset, + limit: data.limit, + }, + }, + } + }, + + outputs: { + items: { + type: 'json', + description: 'Array of collection items', + }, + metadata: { + type: 'json', + description: 'Metadata about the query', + }, + }, +} diff --git a/apps/sim/tools/webflow/types.ts b/apps/sim/tools/webflow/types.ts new file mode 100644 index 0000000000..03af0c25ec --- /dev/null +++ b/apps/sim/tools/webflow/types.ts @@ -0,0 +1,95 @@ +export interface WebflowBaseParams { + accessToken: string + collectionId: string +} + +export interface WebflowListItemsParams extends WebflowBaseParams { + offset?: number + limit?: number +} + +export interface WebflowListItemsOutput { + items: any[] + metadata: { + itemCount: number + offset?: number + limit?: number + } +} + +export interface WebflowListItemsResponse { + success: boolean + output: WebflowListItemsOutput +} + +export interface WebflowGetItemParams extends WebflowBaseParams { + itemId: string +} + +export interface WebflowGetItemOutput { + item: any + metadata: { + itemId: string + } +} + +export interface WebflowGetItemResponse { + success: boolean + output: WebflowGetItemOutput +} + +export interface WebflowCreateItemParams extends WebflowBaseParams { + fieldData: Record +} + +export interface WebflowCreateItemOutput { + item: any + metadata: { + itemId: string + } +} + +export interface WebflowCreateItemResponse { + success: boolean + output: WebflowCreateItemOutput +} + +export interface WebflowUpdateItemParams extends WebflowBaseParams { + itemId: string + fieldData: Record +} + +export interface WebflowUpdateItemOutput { + item: any + metadata: { + itemId: string + } +} + +export interface WebflowUpdateItemResponse { + success: boolean + output: WebflowUpdateItemOutput +} + +export interface WebflowDeleteItemParams extends WebflowBaseParams { + itemId: string +} + +export interface WebflowDeleteItemOutput { + success: boolean + metadata: { + deleted: boolean + } +} + +export interface WebflowDeleteItemResponse { + success: boolean + output: WebflowDeleteItemOutput +} + +export type WebflowResponse = + | WebflowListItemsResponse + | WebflowGetItemResponse + | WebflowCreateItemResponse + | WebflowUpdateItemResponse + | WebflowDeleteItemResponse diff --git a/apps/sim/tools/webflow/update_item.ts b/apps/sim/tools/webflow/update_item.ts new file mode 100644 index 0000000000..2c04d315bc --- /dev/null +++ b/apps/sim/tools/webflow/update_item.ts @@ -0,0 +1,80 @@ +import type { ToolConfig } from '@/tools/types' +import type { WebflowUpdateItemParams, WebflowUpdateItemResponse } from '@/tools/webflow/types' + +export const webflowUpdateItemTool: ToolConfig = + { + id: 'webflow_update_item', + name: 'Webflow Update Item', + description: 'Update an existing item in a Webflow CMS collection', + version: '1.0.0', + + oauth: { + required: true, + provider: 'webflow', + }, + + params: { + accessToken: { + type: 'string', + required: true, + visibility: 'hidden', + description: 'OAuth access token', + }, + collectionId: { + type: 'string', + required: true, + visibility: 'user-only', + description: 'ID of the collection', + }, + itemId: { + type: 'string', + required: true, + visibility: 'user-or-llm', + description: 'ID of the item to update', + }, + fieldData: { + type: 'json', + required: true, + visibility: 'user-or-llm', + description: + 'Field data to update as a JSON object. Only include fields you want to change.', + }, + }, + + request: { + url: (params) => + `https://api.webflow.com/v2/collections/${params.collectionId}/items/${params.itemId}`, + method: 'PATCH', + headers: (params) => ({ + Authorization: `Bearer ${params.accessToken}`, + 'Content-Type': 'application/json', + }), + body: (params) => ({ + fieldData: params.fieldData, + }), + }, + + transformResponse: async (response) => { + const data = await response.json() + return { + success: true, + output: { + item: data, + metadata: { + itemId: data.id || 'unknown', + }, + }, + } + }, + + outputs: { + item: { + type: 'json', + description: 'The updated item object', + }, + metadata: { + type: 'json', + description: 'Metadata about the updated item', + }, + }, + } diff --git a/apps/sim/triggers/index.ts b/apps/sim/triggers/index.ts index 3c2c9cd0cb..42abc447af 100644 --- a/apps/sim/triggers/index.ts +++ b/apps/sim/triggers/index.ts @@ -14,6 +14,12 @@ import { slackWebhookTrigger } from './slack' import { stripeWebhookTrigger } from './stripe/webhook' import { telegramWebhookTrigger } from './telegram' import type { TriggerConfig, TriggerRegistry } from './types' +import { + webflowCollectionItemChangedTrigger, + webflowCollectionItemCreatedTrigger, + webflowCollectionItemDeletedTrigger, + webflowFormSubmissionTrigger, +} from './webflow' import { whatsappWebhookTrigger } from './whatsapp' // Central registry of all available triggers @@ -30,6 +36,10 @@ export const TRIGGER_REGISTRY: TriggerRegistry = { telegram_webhook: telegramWebhookTrigger, whatsapp_webhook: whatsappWebhookTrigger, google_forms_webhook: googleFormsWebhookTrigger, + webflow_collection_item_created: webflowCollectionItemCreatedTrigger, + webflow_collection_item_changed: webflowCollectionItemChangedTrigger, + webflow_collection_item_deleted: webflowCollectionItemDeletedTrigger, + webflow_form_submission: webflowFormSubmissionTrigger, } // Utility functions for working with triggers diff --git a/apps/sim/triggers/webflow/collection_item_changed.ts b/apps/sim/triggers/webflow/collection_item_changed.ts new file mode 100644 index 0000000000..ccb5d797c5 --- /dev/null +++ b/apps/sim/triggers/webflow/collection_item_changed.ts @@ -0,0 +1,96 @@ +import { WebflowIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const webflowCollectionItemChangedTrigger: TriggerConfig = { + id: 'webflow_collection_item_changed', + name: 'Collection Item Changed', + provider: 'webflow', + description: + 'Trigger workflow when an item is updated in a Webflow CMS collection (requires Webflow credentials)', + version: '1.0.0', + icon: WebflowIcon, + + requiresCredentials: true, + credentialProvider: 'webflow', + + configFields: { + siteId: { + type: 'select', + label: 'Site', + placeholder: 'Select a site', + description: 'The Webflow site to monitor', + required: true, + options: [], + }, + collectionId: { + type: 'select', + label: 'Collection', + placeholder: 'Select a collection (optional)', + description: 'Optionally filter to monitor only a specific collection', + required: false, + options: [], + }, + }, + + outputs: { + siteId: { + type: 'string', + description: 'The site ID where the event occurred', + }, + workspaceId: { + type: 'string', + description: 'The workspace ID where the event occurred', + }, + collectionId: { + type: 'string', + description: 'The collection ID where the item was changed', + }, + payload: { + id: { type: 'string', description: 'The ID of the changed item' }, + cmsLocaleId: { type: 'string', description: 'CMS locale ID' }, + lastPublished: { type: 'string', description: 'Last published timestamp' }, + lastUpdated: { type: 'string', description: 'Last updated timestamp' }, + createdOn: { type: 'string', description: 'Timestamp when the item was created' }, + isArchived: { type: 'boolean', description: 'Whether the item is archived' }, + isDraft: { type: 'boolean', description: 'Whether the item is a draft' }, + fieldData: { type: 'object', description: 'The updated field data of the item' }, + }, + }, + + instructions: [ + 'Connect your Webflow account using the "Select Webflow credential" button above.', + 'Enter your Webflow Site ID (found in the site URL or site settings).', + 'Optionally enter a Collection ID to monitor only specific collections.', + 'If no Collection ID is provided, the trigger will fire for items changed in any collection on the site.', + 'The webhook will trigger whenever an existing item is updated in the specified collection(s).', + 'Make sure your Webflow account has appropriate permissions for the specified site.', + ], + + samplePayload: { + siteId: '68f9666057aa8abaa9b0b668', + workspaceId: '68f96081e7018465432953b5', + collectionId: '68f9666257aa8abaa9b0b6d6', + payload: { + id: '68fa8445de250e147cd95cfd', + cmsLocaleId: '68f9666257aa8abaa9b0b6c9', + lastPublished: '2024-01-15T14:45:00.000Z', + lastUpdated: '2024-01-15T14:45:00.000Z', + createdOn: '2024-01-15T10:30:00.000Z', + isArchived: false, + isDraft: false, + fieldData: { + name: 'Updated Blog Post', + slug: 'updated-blog-post', + 'post-summary': 'This blog post has been updated', + featured: true, + }, + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/webflow/collection_item_created.ts b/apps/sim/triggers/webflow/collection_item_created.ts new file mode 100644 index 0000000000..2da7e4d71b --- /dev/null +++ b/apps/sim/triggers/webflow/collection_item_created.ts @@ -0,0 +1,97 @@ +import { WebflowIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const webflowCollectionItemCreatedTrigger: TriggerConfig = { + id: 'webflow_collection_item_created', + name: 'Collection Item Created', + provider: 'webflow', + description: + 'Trigger workflow when a new item is created in a Webflow CMS collection (requires Webflow credentials)', + version: '1.0.0', + icon: WebflowIcon, + + // Webflow requires OAuth credentials to create webhooks + requiresCredentials: true, + credentialProvider: 'webflow', + + configFields: { + siteId: { + type: 'select', + label: 'Site', + placeholder: 'Select a site', + description: 'The Webflow site to monitor', + required: true, + options: [], // Will be populated dynamically from API + }, + collectionId: { + type: 'select', + label: 'Collection', + placeholder: 'Select a collection (optional)', + description: 'Optionally filter to monitor only a specific collection', + required: false, + options: [], // Will be populated dynamically based on selected site + }, + }, + + outputs: { + siteId: { + type: 'string', + description: 'The site ID where the event occurred', + }, + workspaceId: { + type: 'string', + description: 'The workspace ID where the event occurred', + }, + collectionId: { + type: 'string', + description: 'The collection ID where the item was created', + }, + payload: { + id: { type: 'string', description: 'The ID of the created item' }, + cmsLocaleId: { type: 'string', description: 'CMS locale ID' }, + lastPublished: { type: 'string', description: 'Last published timestamp' }, + lastUpdated: { type: 'string', description: 'Last updated timestamp' }, + createdOn: { type: 'string', description: 'Timestamp when the item was created' }, + isArchived: { type: 'boolean', description: 'Whether the item is archived' }, + isDraft: { type: 'boolean', description: 'Whether the item is a draft' }, + fieldData: { type: 'object', description: 'The field data of the item' }, + }, + }, + + instructions: [ + 'Connect your Webflow account using the "Select Webflow credential" button above.', + 'Enter your Webflow Site ID (found in the site URL or site settings).', + 'Optionally enter a Collection ID to monitor only specific collections.', + 'If no Collection ID is provided, the trigger will fire for items created in any collection on the site.', + 'The webhook will trigger whenever a new item is created in the specified collection(s).', + 'Make sure your Webflow account has appropriate permissions for the specified site.', + ], + + samplePayload: { + siteId: '68f9666057aa8abaa9b0b668', + workspaceId: '68f96081e7018465432953b5', + collectionId: '68f9666257aa8abaa9b0b6d6', + payload: { + id: '68fa8445de250e147cd95cfd', + cmsLocaleId: '68f9666257aa8abaa9b0b6c9', + lastPublished: '2024-01-15T10:30:00.000Z', + lastUpdated: '2024-01-15T10:30:00.000Z', + createdOn: '2024-01-15T10:30:00.000Z', + isArchived: false, + isDraft: false, + fieldData: { + name: 'Sample Blog Post', + slug: 'sample-blog-post', + 'post-summary': 'This is a sample blog post created in the collection', + featured: false, + }, + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/webflow/collection_item_deleted.ts b/apps/sim/triggers/webflow/collection_item_deleted.ts new file mode 100644 index 0000000000..c49d2c26ba --- /dev/null +++ b/apps/sim/triggers/webflow/collection_item_deleted.ts @@ -0,0 +1,80 @@ +import { WebflowIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const webflowCollectionItemDeletedTrigger: TriggerConfig = { + id: 'webflow_collection_item_deleted', + name: 'Collection Item Deleted', + provider: 'webflow', + description: + 'Trigger workflow when an item is deleted from a Webflow CMS collection (requires Webflow credentials)', + version: '1.0.0', + icon: WebflowIcon, + + requiresCredentials: true, + credentialProvider: 'webflow', + + configFields: { + siteId: { + type: 'select', + label: 'Site', + placeholder: 'Select a site', + description: 'The Webflow site to monitor', + required: true, + options: [], + }, + collectionId: { + type: 'select', + label: 'Collection', + placeholder: 'Select a collection (optional)', + description: 'Optionally filter to monitor only a specific collection', + required: false, + options: [], + }, + }, + + outputs: { + siteId: { + type: 'string', + description: 'The site ID where the event occurred', + }, + workspaceId: { + type: 'string', + description: 'The workspace ID where the event occurred', + }, + collectionId: { + type: 'string', + description: 'The collection ID where the item was deleted', + }, + payload: { + id: { type: 'string', description: 'The ID of the deleted item' }, + deletedOn: { type: 'string', description: 'Timestamp when the item was deleted' }, + }, + }, + + instructions: [ + 'Connect your Webflow account using the "Select Webflow credential" button above.', + 'Enter your Webflow Site ID (found in the site URL or site settings).', + 'Optionally enter a Collection ID to monitor only specific collections.', + 'If no Collection ID is provided, the trigger will fire for items deleted in any collection on the site.', + 'The webhook will trigger whenever an item is deleted from the specified collection(s).', + 'Note: Once an item is deleted, only minimal information (ID, collection, site) is available.', + 'Make sure your Webflow account has appropriate permissions for the specified site.', + ], + + samplePayload: { + siteId: '68f9666057aa8abaa9b0b668', + workspaceId: '68f96081e7018465432953b5', + collectionId: '68f9666257aa8abaa9b0b6d6', + payload: { + id: '68fa8445de250e147cd95cfd', + deletedOn: '2024-01-15T16:20:00.000Z', + }, + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/webflow/form_submission.ts b/apps/sim/triggers/webflow/form_submission.ts new file mode 100644 index 0000000000..2f3767bc30 --- /dev/null +++ b/apps/sim/triggers/webflow/form_submission.ts @@ -0,0 +1,107 @@ +import { WebflowIcon } from '@/components/icons' +import type { TriggerConfig } from '../types' + +export const webflowFormSubmissionTrigger: TriggerConfig = { + id: 'webflow_form_submission', + name: 'Form Submission', + provider: 'webflow', + description: + 'Trigger workflow when a form is submitted on a Webflow site (requires Webflow credentials)', + version: '1.0.0', + icon: WebflowIcon, + + requiresCredentials: true, + credentialProvider: 'webflow', + + configFields: { + siteId: { + type: 'select', + label: 'Site', + placeholder: 'Select a site', + description: 'The Webflow site to monitor', + required: true, + options: [], + }, + formId: { + type: 'string', + label: 'Form ID', + placeholder: 'form-123abc (optional)', + description: 'The ID of the specific form to monitor (optional - leave empty for all forms)', + required: false, + }, + }, + + outputs: { + siteId: { + type: 'string', + description: 'The site ID where the form was submitted', + }, + workspaceId: { + type: 'string', + description: 'The workspace ID where the event occurred', + }, + name: { + type: 'string', + description: 'The name of the form', + }, + id: { + type: 'string', + description: 'The unique ID of the form submission', + }, + submittedAt: { + type: 'string', + description: 'Timestamp when the form was submitted', + }, + data: { + type: 'object', + description: 'The form submission field data (keys are field names)', + }, + schema: { + type: 'object', + description: 'Form schema information', + }, + formElementId: { + type: 'string', + description: 'The form element ID', + }, + }, + + instructions: [ + 'Connect your Webflow account using the "Select Webflow credential" button above.', + 'Enter your Webflow Site ID (found in the site URL or site settings).', + 'Optionally enter a Form ID to monitor only a specific form.', + 'If no Form ID is provided, the trigger will fire for any form submission on the site.', + 'The webhook will trigger whenever a form is submitted on the specified site.', + 'Form data will be included in the payload with all submitted field values.', + 'Make sure your Webflow account has appropriate permissions for the specified site.', + ], + + samplePayload: { + siteId: '68f9666057aa8abaa9b0b668', + workspaceId: '68f96081e7018465432953b5', + name: 'Contact Form', + id: '68fa8445de250e147cd95cfd', + submittedAt: '2024-01-15T12:00:00.000Z', + data: { + name: 'John Doe', + email: 'john@example.com', + message: 'I would like more information about your services.', + 'consent-checkbox': 'true', + }, + schema: { + fields: [ + { name: 'name', type: 'text' }, + { name: 'email', type: 'email' }, + { name: 'message', type: 'textarea' }, + ], + }, + formElementId: '68f9666257aa8abaa9b0b6e2', + }, + + webhook: { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + }, +} diff --git a/apps/sim/triggers/webflow/index.ts b/apps/sim/triggers/webflow/index.ts new file mode 100644 index 0000000000..5489cc029b --- /dev/null +++ b/apps/sim/triggers/webflow/index.ts @@ -0,0 +1,4 @@ +export { webflowCollectionItemChangedTrigger } from './collection_item_changed' +export { webflowCollectionItemCreatedTrigger } from './collection_item_created' +export { webflowCollectionItemDeletedTrigger } from './collection_item_deleted' +export { webflowFormSubmissionTrigger } from './form_submission'