feat(tools): added webflow OAuth + tools (#1720)

* feat(tools): added webflow OAuth + tools

* remove itemId from delete item

* remove siteId

* added webhook triggers + oauth scopes + site/collection selector

* update sample payload for webflow triggers

* cleanup

* fix discord color
This commit is contained in:
Waleed
2025-10-23 13:08:32 -07:00
committed by GitHub
parent ec034f3fc7
commit a4e874b266
33 changed files with 1927 additions and 36 deletions
@@ -66,6 +66,7 @@
"typeform",
"vision",
"wealthbox",
"webflow",
"webhook",
"whatsapp",
"wikipedia",
+150
View File
@@ -0,0 +1,150 @@
---
title: Webflow
description: Manage Webflow CMS collections
---
import { BlockInfoCard } from "@/components/ui/block-info-card"
<BlockInfoCard
type="webflow"
color="#E0E0E0"
icon={true}
iconSvg={`<svg className="block-icon"
viewBox='0 0 1080 674'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
fill='#146EF5'
/>
</svg>`}
/>
{/* 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`
@@ -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 }
)
}
}
@@ -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 }
)
}
}
+153
View File
@@ -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<string, string> = {
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<string, any>) || {}
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
}
}
@@ -115,6 +115,10 @@ const SCOPE_DESCRIPTIONS: Record<string, string> = {
'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
@@ -35,6 +35,7 @@ interface TriggerConfigSectionProps {
onChange: (fieldId: string, value: any) => void
webhookUrl: string
dynamicOptions?: Record<string, Array<{ id: string; name: string }> | string[]>
loadingFields?: Record<string, boolean>
}
export function TriggerConfigSection({
@@ -44,6 +45,7 @@ export function TriggerConfigSection({
onChange,
webhookUrl,
dynamicOptions = {},
loadingFields = {},
}: TriggerConfigSectionProps) {
const [showSecrets, setShowSecrets] = useState<Record<string, boolean>>({})
const [copied, setCopied] = useState<string | null>(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 (
<div className='space-y-2'>
<Label htmlFor={fieldId}>
<Label htmlFor={fieldId} className='font-medium text-sm'>
{fieldDef.label}
{fieldDef.required && <span className='ml-1 text-red-500'>*</span>}
</Label>
<Select value={value} onValueChange={(value) => onChange(fieldId, value)}>
<SelectTrigger>
<SelectValue placeholder={fieldDef.placeholder} />
{fieldDef.description && (
<p className='text-muted-foreground text-sm'>{fieldDef.description}</p>
)}
<Select
value={value}
onValueChange={(value) => onChange(fieldId, value)}
disabled={isLoading}
>
<SelectTrigger id={fieldId} className='h-10'>
<SelectValue placeholder={isLoading ? 'Loading...' : fieldDef.placeholder} />
</SelectTrigger>
<SelectContent>
{fieldDef.options?.map((option: string) => (
<SelectItem key={option} value={option}>
{option}
{availableOptions.map((option: any) => (
<SelectItem key={option.id} value={option.id}>
{option.name}
</SelectItem>
))}
</SelectContent>
</Select>
{fieldDef.description && (
<p className='text-muted-foreground text-sm'>{fieldDef.description}</p>
)}
</div>
)
}
@@ -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 (
<div className='space-y-4 rounded-md border border-border bg-card p-4 shadow-sm'>
{webhookUrl && (
{shouldShowWebhookUrl && (
<div className='mb-4 space-y-1'>
<div className='flex items-center gap-2'>
<Label className='font-medium text-sm'>Webhook URL</Label>
@@ -96,6 +96,7 @@ export function TriggerModal({
const [dynamicOptions, setDynamicOptions] = useState<
Record<string, Array<{ id: string; name: string }>>
>({})
const [loadingFields, setLoadingFields] = useState<Record<string, boolean>>({})
const lastCredentialIdRef = useRef<string | null>(null)
const [testUrl, setTestUrl] = useState<string | null>(null)
const [testUrlExpiresAt, setTestUrlExpiresAt] = useState<string | null>(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 && (
<div className='mr-2 h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
<div className='h-4 w-4 animate-spin rounded-full border-[1.5px] border-current border-t-transparent' />
)}
{isSaving ? 'Saving...' : 'Save Changes'}
</Button>
@@ -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
@@ -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'
@@ -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 (
<div className='space-y-4'>
<ConfigSection title='Webflow Configuration'>
<ConfigField
id='webflow-site-id'
label='Site ID *'
description='The ID of the Webflow site to monitor (found in site settings or URL)'
>
{isLoadingToken ? (
<Skeleton className='h-10 w-full' />
) : (
<Input
id='webflow-site-id'
value={siteId}
onChange={(e) => setSiteId(e.target.value)}
placeholder='6c3592'
required
/>
)}
</ConfigField>
{isCollectionTrigger && setCollectionId && (
<ConfigField
id='webflow-collection-id'
label='Collection ID'
description='The ID of the collection to monitor (optional - leave empty to monitor all collections)'
>
{isLoadingToken ? (
<Skeleton className='h-10 w-full' />
) : (
<Input
id='webflow-collection-id'
value={collectionId || ''}
onChange={(e) => setCollectionId(e.target.value)}
placeholder='68f9666257aa8abaa9b0b6d6'
/>
)}
</ConfigField>
)}
{isFormTrigger && setFormId && (
<ConfigField
id='webflow-form-id'
label='Form ID'
description='The ID of the specific form to monitor (optional - leave empty to monitor all forms)'
>
{isLoadingToken ? (
<Skeleton className='h-10 w-full' />
) : (
<Input
id='webflow-form-id'
value={formId || ''}
onChange={(e) => setFormId(e.target.value)}
placeholder='form-contact'
/>
)}
</ConfigField>
)}
</ConfigSection>
{testResult && (
<WebhookTestResult
testResult={testResult}
copied={copied}
copyToClipboard={copyToClipboard}
/>
)}
<InstructionsSection tip='Webflow webhooks monitor changes in your CMS and trigger your workflow automatically.'>
<ol className='list-inside list-decimal space-y-1'>
<li>Connect your Webflow account using the credential selector above.</li>
<li>Enter your Webflow Site ID (found in the site URL or site settings).</li>
{isCollectionTrigger && (
<>
<li>
Optionally enter a Collection ID to monitor only specific collections (leave empty
to monitor all).
</li>
<li>
The webhook will trigger when items are created, changed, or deleted in the
specified collection(s).
</li>
</>
)}
{isFormTrigger && (
<>
<li>
Optionally enter a Form ID to monitor only a specific form (leave empty to monitor
all forms).
</li>
<li>The webhook will trigger whenever a form is submitted on your site.</li>
</>
)}
<li>
Sim will automatically register the webhook with Webflow when you save this
configuration.
</li>
<li>Make sure your Webflow account has appropriate permissions for the site.</li>
</ol>
</InstructionsSection>
</div>
)
}
+1 -1
View File
@@ -11,7 +11,7 @@ export const DiscordBlock: BlockConfig<DiscordResponse> = {
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: [
{
+190
View File
@@ -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<WebflowResponse> = {
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',
],
},
}
+2 -5
View File
@@ -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<string, BlockConfig> = {
typeform: TypeformBlock,
vision: VisionBlock,
wealthbox: WealthboxBlock,
webflow: WebflowBlock,
webhook: WebhookBlock,
whatsapp: WhatsAppBlock,
wikipedia: WikipediaBlock,
+20
View File
@@ -3775,3 +3775,23 @@ export function ZepIcon(props: SVGProps<SVGSVGElement>) {
</svg>
)
}
export function WebflowIcon(props: SVGProps<SVGSVGElement>) {
return (
<svg
{...props}
width='1080'
height='674'
viewBox='0 0 1080 674'
fill='none'
xmlns='http://www.w3.org/2000/svg'
>
<path
fillRule='evenodd'
clipRule='evenodd'
d='M1080 0L735.386 673.684H411.695L555.916 394.481H549.445C430.464 548.934 252.942 650.61 -0.000488281 673.684V398.344C-0.000488281 398.344 161.813 388.787 256.938 288.776H-0.000488281V0.0053214H288.771V237.515L295.252 237.489L413.254 0.0053214H631.644V236.009L638.126 235.999L760.555 0H1080Z'
fill='#146EF5'
/>
</svg>
)
}
+51
View File
@@ -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
+2
View File
@@ -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
+35
View File
@@ -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<string, OAuthProviderConfig> = {
},
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}`)
}
+51
View File
@@ -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
}
+12
View File
@@ -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<string, ToolConfig> = {
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,
+73
View File
@@ -0,0 +1,73 @@
import type { ToolConfig } from '@/tools/types'
import type { WebflowCreateItemParams, WebflowCreateItemResponse } from '@/tools/webflow/types'
export const webflowCreateItemTool: ToolConfig<WebflowCreateItemParams, WebflowCreateItemResponse> =
{
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',
},
},
}
+70
View File
@@ -0,0 +1,70 @@
import type { ToolConfig } from '@/tools/types'
import type { WebflowDeleteItemParams, WebflowDeleteItemResponse } from '@/tools/webflow/types'
export const webflowDeleteItemTool: ToolConfig<WebflowDeleteItemParams, WebflowDeleteItemResponse> =
{
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',
},
},
}
+68
View File
@@ -0,0 +1,68 @@
import type { ToolConfig } from '@/tools/types'
import type { WebflowGetItemParams, WebflowGetItemResponse } from '@/tools/webflow/types'
export const webflowGetItemTool: ToolConfig<WebflowGetItemParams, WebflowGetItemResponse> = {
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',
},
},
}
+13
View File
@@ -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,
}
+88
View File
@@ -0,0 +1,88 @@
import type { ToolConfig } from '@/tools/types'
import type { WebflowListItemsParams, WebflowListItemsResponse } from '@/tools/webflow/types'
export const webflowListItemsTool: ToolConfig<WebflowListItemsParams, WebflowListItemsResponse> = {
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',
},
},
}
+95
View File
@@ -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<string, any>
}
export interface WebflowCreateItemOutput {
item: any
metadata: {
itemId: string
}
}
export interface WebflowCreateItemResponse {
success: boolean
output: WebflowCreateItemOutput
}
export interface WebflowUpdateItemParams extends WebflowBaseParams {
itemId: string
fieldData: Record<string, any>
}
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
+80
View File
@@ -0,0 +1,80 @@
import type { ToolConfig } from '@/tools/types'
import type { WebflowUpdateItemParams, WebflowUpdateItemResponse } from '@/tools/webflow/types'
export const webflowUpdateItemTool: ToolConfig<WebflowUpdateItemParams, WebflowUpdateItemResponse> =
{
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',
},
},
}
+10
View File
@@ -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
@@ -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',
},
},
}
@@ -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',
},
},
}
@@ -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',
},
},
}
@@ -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',
},
},
}
+4
View File
@@ -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'