@224industries/webflow-ai-sdk 0.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 224 Industries / Ben Sabic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # Webflow - AI SDK Tools
2
+
3
+ ![224 Industries OSS](https://img.shields.io/badge/224_Industries-OSS-111212?style=for-the-badge&labelColor=6AFFDC)
4
+ ![MIT License](https://img.shields.io/badge/License-MIT-111212?style=for-the-badge&labelColor=6AFFDC)
5
+ [![Webflow Premium Partner](https://img.shields.io/badge/Premium_Partner-146EF5?style=for-the-badge&logo=webflow&logoColor=white)](https://webflow.com/@224-industries)
6
+ ![Vercel AI SDK](https://img.shields.io/badge/Vercel-AI%20SDK-000000?style=for-the-badge&logo=vercel&logoColor=white)
7
+
8
+ A collection of [AI SDK](https://ai-sdk.dev) tools that give your AI agents the ability to manage [Webflow](https://webflow.com) sites, pages, forms, and custom code.
9
+
10
+ ## Installation
11
+
12
+ ```bash
13
+ npm install @224industries/webflow-ai-sdk
14
+ ```
15
+
16
+ ## Setup
17
+
18
+ Set the following environment variables:
19
+
20
+ ```bash
21
+ WEBFLOW_API_KEY="your_webflow_api_key"
22
+ WEBFLOW_SITE_ID="your_default_site_id"
23
+ ```
24
+ Get your API key from the [Webflow Dashboard](https://webflow.com/dashboard).
25
+
26
+ ## Usage
27
+
28
+ ```ts
29
+ import { generateText, stepCountIs } from "ai";
30
+ import { listSites, listPages, publishSite } from "@224industries/webflow-ai-sdk";
31
+
32
+ const { text } = await generateText({
33
+ model: 'openai/gpt-5.2',
34
+ tools: { listSites, listPages, publishSite },
35
+ prompt: "List all my sites and their pages",
36
+ stopWhen: stepCountIs(5),
37
+ });
38
+ ```
39
+
40
+ ## Available Tools
41
+
42
+ | Tool | Description |
43
+ |------|-------------|
44
+ | `listSites` | List all Webflow sites accessible with the current API token |
45
+ | `publishSite` | Publish a site to custom domains or the Webflow subdomain |
46
+ | `listPages` | List all pages for a site with pagination |
47
+ | `listForms` | List all forms for a site with field definitions |
48
+ | `listFormSubmissions` | Retrieve submitted form data, optionally filtered by form |
49
+ | `listCustomCode` | List all custom code scripts applied to a site and its pages |
50
+ | `addCustomCode` | Register and apply an inline script to a site or page |
51
+
52
+ ## AI SDK Library
53
+
54
+ Find other AI SDK agents and tools in the [AI SDK Library](https://aisdklibrary.com).
55
+
56
+ ## Resources
57
+
58
+ - [Vercel AI SDK documentation](https://ai-sdk.dev/docs/introduction)
59
+ - [Webflow API documentation](https://developers.webflow.com)
60
+
61
+ ## Contributing
62
+
63
+ Contributions are welcome! Please read our [Contributing Guide](.github/CONTRIBUTING.md) for more information.
64
+
65
+ ## License
66
+
67
+ [MIT License](LICENSE)
68
+
69
+ ## Creator
70
+
71
+ [Ben Sabic](https://bensabic.dev) (Fractional CTO) at [224 Industries](https://224industries.com.au)
@@ -0,0 +1,145 @@
1
+ import * as ai from 'ai';
2
+
3
+ declare const listSites: ai.Tool<Record<string, never>, {
4
+ sites: {
5
+ id: string;
6
+ displayName: string;
7
+ shortName: string;
8
+ lastPublished?: string | undefined;
9
+ lastUpdated?: string | undefined;
10
+ previewUrl?: string | undefined;
11
+ timeZone?: string | undefined;
12
+ customDomains?: {
13
+ id: string;
14
+ url: string;
15
+ }[] | undefined;
16
+ }[];
17
+ count: number;
18
+ error?: string | undefined;
19
+ }>;
20
+ declare const publishSite: ai.Tool<{
21
+ siteId?: string | undefined;
22
+ customDomains?: string[] | undefined;
23
+ publishToWebflowSubdomain?: boolean | undefined;
24
+ }, {
25
+ success: boolean;
26
+ publishedDomains?: {
27
+ id: string;
28
+ url: string;
29
+ }[] | undefined;
30
+ error?: string | undefined;
31
+ }>;
32
+ declare const listPages: ai.Tool<{
33
+ siteId?: string | undefined;
34
+ limit?: number | undefined;
35
+ offset?: number | undefined;
36
+ }, {
37
+ pages: {
38
+ id: string;
39
+ title: string;
40
+ slug: string;
41
+ archived?: boolean | undefined;
42
+ draft?: boolean | undefined;
43
+ createdOn?: string | undefined;
44
+ lastUpdated?: string | undefined;
45
+ publishedPath?: string | undefined;
46
+ seo?: {
47
+ title?: string | undefined;
48
+ description?: string | undefined;
49
+ } | undefined;
50
+ }[];
51
+ count: number;
52
+ pagination?: {
53
+ limit: number;
54
+ offset: number;
55
+ total: number;
56
+ } | undefined;
57
+ error?: string | undefined;
58
+ }>;
59
+ declare const listForms: ai.Tool<{
60
+ siteId?: string | undefined;
61
+ limit?: number | undefined;
62
+ offset?: number | undefined;
63
+ }, {
64
+ forms: {
65
+ id: string;
66
+ displayName: string;
67
+ pageId?: string | undefined;
68
+ pageName?: string | undefined;
69
+ formElementId?: string | undefined;
70
+ fields?: Record<string, {
71
+ displayName?: string | undefined;
72
+ type?: string | undefined;
73
+ }> | undefined;
74
+ createdOn?: string | undefined;
75
+ lastUpdated?: string | undefined;
76
+ }[];
77
+ count: number;
78
+ pagination?: {
79
+ limit: number;
80
+ offset: number;
81
+ total: number;
82
+ } | undefined;
83
+ error?: string | undefined;
84
+ }>;
85
+ declare const listFormSubmissions: ai.Tool<{
86
+ siteId?: string | undefined;
87
+ elementId?: string | undefined;
88
+ limit?: number | undefined;
89
+ offset?: number | undefined;
90
+ }, {
91
+ formSubmissions: {
92
+ id: string;
93
+ displayName?: string | undefined;
94
+ dateSubmitted?: string | undefined;
95
+ formResponse?: Record<string, unknown> | undefined;
96
+ }[];
97
+ count: number;
98
+ pagination?: {
99
+ limit: number;
100
+ offset: number;
101
+ total: number;
102
+ } | undefined;
103
+ error?: string | undefined;
104
+ }>;
105
+ declare const listCustomCode: ai.Tool<{
106
+ siteId?: string | undefined;
107
+ limit?: number | undefined;
108
+ offset?: number | undefined;
109
+ }, {
110
+ blocks: {
111
+ siteId: string;
112
+ scripts: {
113
+ id: string;
114
+ location: string;
115
+ version: string;
116
+ }[];
117
+ pageId?: string | undefined;
118
+ type?: string | undefined;
119
+ createdOn?: string | undefined;
120
+ lastUpdated?: string | undefined;
121
+ }[];
122
+ count: number;
123
+ pagination?: {
124
+ limit: number;
125
+ offset: number;
126
+ total: number;
127
+ } | undefined;
128
+ error?: string | undefined;
129
+ }>;
130
+ declare const addCustomCode: ai.Tool<{
131
+ target: "site" | "page";
132
+ sourceCode: string;
133
+ displayName: string;
134
+ version: string;
135
+ location: "header" | "footer";
136
+ siteId?: string | undefined;
137
+ pageId?: string | undefined;
138
+ }, {
139
+ success: boolean;
140
+ scriptId: string;
141
+ appliedTo?: string | undefined;
142
+ error?: string | undefined;
143
+ }>;
144
+
145
+ export { addCustomCode, listCustomCode, listFormSubmissions, listForms, listPages, listSites, publishSite };
package/dist/index.js ADDED
@@ -0,0 +1,662 @@
1
+ // src/index.ts
2
+ import { tool } from "ai";
3
+ import { z } from "zod";
4
+ var BASE_URL = "https://api.webflow.com/v2";
5
+ var getApiKey = () => {
6
+ const apiKey = process.env.WEBFLOW_API_KEY;
7
+ if (!apiKey) {
8
+ throw new Error("WEBFLOW_API_KEY environment variable is required");
9
+ }
10
+ return apiKey;
11
+ };
12
+ var callApi = async (path, options = {}) => {
13
+ const { method = "GET", body, params } = options;
14
+ const url = new URL(`${BASE_URL}${path}`);
15
+ if (params) {
16
+ for (const [key, value] of Object.entries(params)) {
17
+ if (value !== void 0) {
18
+ url.searchParams.set(key, String(value));
19
+ }
20
+ }
21
+ }
22
+ const headers = {
23
+ Authorization: `Bearer ${getApiKey()}`
24
+ };
25
+ if (body) {
26
+ headers["Content-Type"] = "application/json";
27
+ }
28
+ const response = await fetch(url.toString(), {
29
+ method,
30
+ headers,
31
+ body: body ? JSON.stringify(body) : void 0
32
+ });
33
+ if (!response.ok) {
34
+ const text = await response.text();
35
+ throw new Error(`Webflow API error ${response.status}: ${text}`);
36
+ }
37
+ return response.json();
38
+ };
39
+ var getDefaultSiteId = () => process.env.WEBFLOW_SITE_ID ?? "";
40
+ var resolveSiteId = (siteId) => {
41
+ const resolved = siteId || getDefaultSiteId();
42
+ if (!resolved) {
43
+ throw new Error(
44
+ "A site ID is required. Either pass a siteId or set the WEBFLOW_SITE_ID environment variable."
45
+ );
46
+ }
47
+ return resolved;
48
+ };
49
+ var getStringField = (obj, snakeCase, camelCase) => {
50
+ if (obj[snakeCase]) {
51
+ return String(obj[snakeCase]);
52
+ }
53
+ if (obj[camelCase]) {
54
+ return String(obj[camelCase]);
55
+ }
56
+ return void 0;
57
+ };
58
+ var parseFormFields = (rawFields) => {
59
+ if (!rawFields) {
60
+ return void 0;
61
+ }
62
+ const fields = {};
63
+ for (const [key, value] of Object.entries(rawFields)) {
64
+ fields[key] = {
65
+ displayName: value.displayName ? String(value.displayName) : void 0,
66
+ type: value.type ? String(value.type) : void 0
67
+ };
68
+ }
69
+ return Object.keys(fields).length > 0 ? fields : void 0;
70
+ };
71
+ var SiteInfoSchema = z.object({
72
+ id: z.string().describe("Unique identifier for the Site"),
73
+ displayName: z.string().describe("Name given to the Site"),
74
+ shortName: z.string().describe("Slugified version of the name"),
75
+ lastPublished: z.string().optional().describe("ISO timestamp when the site was last published"),
76
+ lastUpdated: z.string().optional().describe("ISO timestamp when the site was last updated"),
77
+ previewUrl: z.string().optional().describe("URL of the site preview image"),
78
+ timeZone: z.string().optional().describe("Site timezone"),
79
+ customDomains: z.array(
80
+ z.object({
81
+ id: z.string().describe("Domain ID"),
82
+ url: z.string().describe("Registered domain name")
83
+ })
84
+ ).optional().describe("Custom domains attached to the site")
85
+ });
86
+ var ListSitesResultSchema = z.object({
87
+ sites: z.array(SiteInfoSchema).describe("Array of site metadata"),
88
+ count: z.number().describe("Number of sites returned"),
89
+ error: z.string().optional().describe("Error message if failed")
90
+ });
91
+ var PublishSiteResultSchema = z.object({
92
+ success: z.boolean().describe("Whether the publish was queued successfully"),
93
+ publishedDomains: z.array(
94
+ z.object({
95
+ id: z.string().describe("Domain ID"),
96
+ url: z.string().describe("Domain URL")
97
+ })
98
+ ).optional().describe("Domains that were published to"),
99
+ error: z.string().optional().describe("Error message if failed")
100
+ });
101
+ var PageInfoSchema = z.object({
102
+ id: z.string().describe("Unique identifier for the Page"),
103
+ title: z.string().describe("Title of the Page"),
104
+ slug: z.string().describe("URL slug of the Page"),
105
+ archived: z.boolean().optional().describe("Whether the Page is archived"),
106
+ draft: z.boolean().optional().describe("Whether the Page is a draft"),
107
+ createdOn: z.string().optional().describe("ISO timestamp when the page was created"),
108
+ lastUpdated: z.string().optional().describe("ISO timestamp when the page was last updated"),
109
+ publishedPath: z.string().optional().describe("Relative path of the published page URL"),
110
+ seo: z.object({
111
+ title: z.string().optional().describe("SEO title"),
112
+ description: z.string().optional().describe("SEO description")
113
+ }).optional().describe("SEO metadata for the page")
114
+ });
115
+ var ListPagesResultSchema = z.object({
116
+ pages: z.array(PageInfoSchema).describe("Array of page metadata"),
117
+ count: z.number().describe("Number of pages returned"),
118
+ pagination: z.object({
119
+ limit: z.number().describe("Limit used for pagination"),
120
+ offset: z.number().describe("Offset used for pagination"),
121
+ total: z.number().describe("Total number of records")
122
+ }).optional().describe("Pagination info"),
123
+ error: z.string().optional().describe("Error message if failed")
124
+ });
125
+ var FormInfoSchema = z.object({
126
+ id: z.string().describe("Unique ID for the Form"),
127
+ displayName: z.string().describe("Form name displayed on the site"),
128
+ pageId: z.string().optional().describe("ID of the Page the form is on"),
129
+ pageName: z.string().optional().describe("Name of the Page the form is on"),
130
+ formElementId: z.string().optional().describe(
131
+ "Unique element ID for the form, used to filter submissions across component instances"
132
+ ),
133
+ fields: z.record(
134
+ z.string(),
135
+ z.object({
136
+ displayName: z.string().optional().describe("Field display name"),
137
+ type: z.string().optional().describe("Field type")
138
+ })
139
+ ).optional().describe("Form field definitions"),
140
+ createdOn: z.string().optional().describe("ISO timestamp when the form was created"),
141
+ lastUpdated: z.string().optional().describe("ISO timestamp when the form was last updated")
142
+ });
143
+ var ListFormsResultSchema = z.object({
144
+ forms: z.array(FormInfoSchema).describe("Array of form metadata"),
145
+ count: z.number().describe("Number of forms returned"),
146
+ pagination: z.object({
147
+ limit: z.number().describe("Limit used for pagination"),
148
+ offset: z.number().describe("Offset used for pagination"),
149
+ total: z.number().describe("Total number of records")
150
+ }).optional().describe("Pagination info"),
151
+ error: z.string().optional().describe("Error message if failed")
152
+ });
153
+ var FormSubmissionSchema = z.object({
154
+ id: z.string().describe("Unique ID of the form submission"),
155
+ displayName: z.string().optional().describe("Form name"),
156
+ dateSubmitted: z.string().optional().describe("ISO timestamp when the form was submitted"),
157
+ formResponse: z.record(z.string(), z.unknown()).optional().describe("Key/value pairs of submitted form data")
158
+ });
159
+ var ListFormSubmissionsResultSchema = z.object({
160
+ formSubmissions: z.array(FormSubmissionSchema).describe("Array of form submissions"),
161
+ count: z.number().describe("Number of submissions returned"),
162
+ pagination: z.object({
163
+ limit: z.number().describe("Limit used for pagination"),
164
+ offset: z.number().describe("Offset used for pagination"),
165
+ total: z.number().describe("Total number of records")
166
+ }).optional().describe("Pagination info"),
167
+ error: z.string().optional().describe("Error message if failed")
168
+ });
169
+ var CustomCodeBlockSchema = z.object({
170
+ siteId: z.string().describe("Site ID where the code is applied"),
171
+ pageId: z.string().optional().describe("Page ID if applied at page level"),
172
+ type: z.string().optional().describe("Whether applied at site or page level"),
173
+ scripts: z.array(
174
+ z.object({
175
+ id: z.string().describe("Script ID"),
176
+ location: z.string().describe("header or footer"),
177
+ version: z.string().describe("SemVer version string")
178
+ })
179
+ ).describe("Scripts applied in this block"),
180
+ createdOn: z.string().optional().describe("ISO timestamp when created"),
181
+ lastUpdated: z.string().optional().describe("ISO timestamp when last updated")
182
+ });
183
+ var ListCustomCodeResultSchema = z.object({
184
+ blocks: z.array(CustomCodeBlockSchema).describe("Array of custom code blocks applied to the site and its pages"),
185
+ count: z.number().describe("Number of blocks returned"),
186
+ pagination: z.object({
187
+ limit: z.number().describe("Limit used for pagination"),
188
+ offset: z.number().describe("Offset used for pagination"),
189
+ total: z.number().describe("Total number of records")
190
+ }).optional().describe("Pagination info"),
191
+ error: z.string().optional().describe("Error message if failed")
192
+ });
193
+ var AddCustomCodeResultSchema = z.object({
194
+ success: z.boolean().describe("Whether the script was registered and applied"),
195
+ scriptId: z.string().describe("ID of the registered script"),
196
+ appliedTo: z.string().optional().describe("Whether the script was applied to a site or page"),
197
+ error: z.string().optional().describe("Error message if failed")
198
+ });
199
+ var siteIdDescription = getDefaultSiteId() ? ` If not provided, defaults to the configured site: ${getDefaultSiteId()}.` : "";
200
+ var listSites = tool({
201
+ description: "List all Webflow sites that the user currently has access to. Use this tool to discover available sites, find site IDs, check custom domains, or see when a site was last published.",
202
+ inputSchema: z.object({}),
203
+ inputExamples: [{ input: {} }],
204
+ outputSchema: ListSitesResultSchema,
205
+ strict: true,
206
+ execute: async () => {
207
+ try {
208
+ const response = await callApi("/sites");
209
+ const rawSites = response.sites ?? [];
210
+ const sites = rawSites.map((raw) => {
211
+ const domains = raw.customDomains;
212
+ return {
213
+ id: String(raw.id ?? ""),
214
+ displayName: String(raw.displayName ?? ""),
215
+ shortName: String(raw.shortName ?? ""),
216
+ lastPublished: getStringField(raw, "last_published", "lastPublished"),
217
+ lastUpdated: getStringField(raw, "last_updated", "lastUpdated"),
218
+ previewUrl: raw.previewUrl ? String(raw.previewUrl) : void 0,
219
+ timeZone: raw.timeZone ? String(raw.timeZone) : void 0,
220
+ designerUrl: `https://${raw.shortName}.design.webflow.com`,
221
+ settingsUrl: `https://webflow.com/dashboard/sites/${raw.shortName}/general`,
222
+ customDomains: domains?.map((d) => ({
223
+ id: String(d.id ?? ""),
224
+ url: String(d.url ?? "")
225
+ }))
226
+ };
227
+ });
228
+ return { sites, count: sites.length };
229
+ } catch (error) {
230
+ console.error("Error listing sites:", error);
231
+ return {
232
+ sites: [],
233
+ count: 0,
234
+ error: error instanceof Error ? error.message : "Failed to list sites"
235
+ };
236
+ }
237
+ }
238
+ });
239
+ var publishSite = tool({
240
+ description: "Publish a Webflow site to one or more domains. Use this tool when the user wants to deploy or publish their site. You must set publishToWebflowSubdomain to true, pass specific custom domain IDs from listSites, or both. Do NOT pass customDomains unless you have retrieved actual domain IDs from listSites \u2014 not all sites have custom domains. Rate limited to 1 publish per minute." + siteIdDescription,
241
+ inputSchema: z.object({
242
+ siteId: z.string().optional().describe(
243
+ `The ID of the site to publish.${siteIdDescription || " Use listSites to find available site IDs."}`
244
+ ),
245
+ customDomains: z.array(z.string()).optional().describe(
246
+ "Array of custom domain IDs to publish to. Only provide this if you have retrieved actual domain IDs from the listSites tool. Do NOT guess or fabricate domain IDs. Omit this field entirely if the site has no custom domains."
247
+ ),
248
+ publishToWebflowSubdomain: z.boolean().optional().describe(
249
+ "Whether to publish to the default Webflow subdomain (yoursite.webflow.io). Set to true if no custom domains are being used."
250
+ )
251
+ }),
252
+ inputExamples: [
253
+ {
254
+ input: {
255
+ siteId: "580e63e98c9a982ac9b8b741",
256
+ publishToWebflowSubdomain: true
257
+ }
258
+ },
259
+ {
260
+ input: {
261
+ siteId: "580e63e98c9a982ac9b8b741",
262
+ customDomains: ["660c6449dd97ebc7346ac629"]
263
+ }
264
+ }
265
+ ],
266
+ outputSchema: PublishSiteResultSchema,
267
+ strict: true,
268
+ needsApproval: true,
269
+ execute: async ({ siteId, customDomains, publishToWebflowSubdomain }) => {
270
+ try {
271
+ const resolvedSiteId = resolveSiteId(siteId);
272
+ const body = {
273
+ publishToWebflowSubdomain: publishToWebflowSubdomain ?? false
274
+ };
275
+ if (customDomains && customDomains.length > 0) {
276
+ body.customDomains = customDomains;
277
+ }
278
+ const response = await callApi(`/sites/${resolvedSiteId}/publish`, {
279
+ method: "POST",
280
+ body
281
+ });
282
+ const domains = response.customDomains;
283
+ return {
284
+ success: true,
285
+ publishedDomains: domains?.map((d) => ({
286
+ id: String(d.id ?? ""),
287
+ url: String(d.url ?? "")
288
+ }))
289
+ };
290
+ } catch (error) {
291
+ console.error("Error publishing site:", error);
292
+ return {
293
+ success: false,
294
+ error: error instanceof Error ? error.message : "Failed to publish site"
295
+ };
296
+ }
297
+ }
298
+ });
299
+ var listPages = tool({
300
+ description: "List all pages for a Webflow site. Use this tool to browse site pages, find page IDs for custom code injection, or check page SEO metadata. Supports pagination via limit and offset parameters." + siteIdDescription,
301
+ inputSchema: z.object({
302
+ siteId: z.string().optional().describe(
303
+ `The ID of the site.${siteIdDescription || " Use listSites to find available site IDs."}`
304
+ ),
305
+ limit: z.number().min(1).max(100).optional().describe("Maximum number of pages to return. Default 100, max 100."),
306
+ offset: z.number().optional().describe("Offset for pagination if results exceed the limit.")
307
+ }),
308
+ inputExamples: [
309
+ { input: { siteId: "580e63e98c9a982ac9b8b741" } },
310
+ { input: { siteId: "580e63e98c9a982ac9b8b741", limit: 10, offset: 0 } }
311
+ ],
312
+ outputSchema: ListPagesResultSchema,
313
+ strict: true,
314
+ execute: async ({ siteId, limit, offset }) => {
315
+ try {
316
+ const resolvedSiteId = resolveSiteId(siteId);
317
+ const response = await callApi(`/sites/${resolvedSiteId}/pages`, {
318
+ params: { limit, offset }
319
+ });
320
+ const rawPages = response.pages ?? [];
321
+ const pages = rawPages.map((page) => {
322
+ const seo = page.seo;
323
+ return {
324
+ id: String(page.id ?? ""),
325
+ title: String(page.title ?? ""),
326
+ slug: String(page.slug ?? ""),
327
+ archived: page.archived ? Boolean(page.archived) : void 0,
328
+ draft: page.draft ? Boolean(page.draft) : void 0,
329
+ createdOn: getStringField(page, "created_on", "createdOn"),
330
+ lastUpdated: getStringField(page, "last_updated", "lastUpdated"),
331
+ publishedPath: page.publishedPath ? String(page.publishedPath) : void 0,
332
+ seo: seo ? {
333
+ title: seo.title ? String(seo.title) : void 0,
334
+ description: seo.description ? String(seo.description) : void 0
335
+ } : void 0
336
+ };
337
+ });
338
+ const pagination = response.pagination;
339
+ return {
340
+ pages,
341
+ count: pages.length,
342
+ pagination: pagination ? {
343
+ limit: Number(pagination.limit ?? 0),
344
+ offset: Number(pagination.offset ?? 0),
345
+ total: Number(pagination.total ?? 0)
346
+ } : void 0
347
+ };
348
+ } catch (error) {
349
+ console.error("Error listing pages:", error);
350
+ return {
351
+ pages: [],
352
+ count: 0,
353
+ error: error instanceof Error ? error.message : "Failed to list pages"
354
+ };
355
+ }
356
+ }
357
+ });
358
+ var listForms = tool({
359
+ description: "List all forms for a Webflow site. Use this tool to browse available forms, find form IDs and element IDs, or inspect form field definitions. The formElementId can be used to filter submissions across component instances. Supports pagination via limit and offset parameters." + siteIdDescription,
360
+ inputSchema: z.object({
361
+ siteId: z.string().optional().describe(
362
+ `The ID of the site.${siteIdDescription || " Use listSites to find available site IDs."}`
363
+ ),
364
+ limit: z.number().min(1).max(100).optional().describe("Maximum number of forms to return. Default 100, max 100."),
365
+ offset: z.number().optional().describe("Offset for pagination if results exceed the limit.")
366
+ }),
367
+ inputExamples: [
368
+ { input: { siteId: "580e63e98c9a982ac9b8b741" } },
369
+ { input: { siteId: "580e63e98c9a982ac9b8b741", limit: 10 } }
370
+ ],
371
+ outputSchema: ListFormsResultSchema,
372
+ strict: true,
373
+ execute: async ({ siteId, limit, offset }) => {
374
+ try {
375
+ const resolvedSiteId = resolveSiteId(siteId);
376
+ const response = await callApi(`/sites/${resolvedSiteId}/forms`, {
377
+ params: { limit, offset }
378
+ });
379
+ const rawForms = response.forms ?? [];
380
+ const forms = rawForms.map((form) => ({
381
+ id: String(form.id ?? ""),
382
+ displayName: String(form.displayName ?? ""),
383
+ pageId: form.pageId ? String(form.pageId) : void 0,
384
+ pageName: form.pageName ? String(form.pageName) : void 0,
385
+ formElementId: getStringField(form, "form_element_id", "formElementId"),
386
+ fields: parseFormFields(
387
+ form.fields
388
+ ),
389
+ createdOn: getStringField(form, "created_on", "createdOn"),
390
+ lastUpdated: getStringField(form, "last_updated", "lastUpdated")
391
+ }));
392
+ const pagination = response.pagination;
393
+ return {
394
+ forms,
395
+ count: forms.length,
396
+ pagination: pagination ? {
397
+ limit: Number(pagination.limit ?? 0),
398
+ offset: Number(pagination.offset ?? 0),
399
+ total: Number(pagination.total ?? 0)
400
+ } : void 0
401
+ };
402
+ } catch (error) {
403
+ console.error("Error listing forms:", error);
404
+ return {
405
+ forms: [],
406
+ count: 0,
407
+ error: error instanceof Error ? error.message : "Failed to list forms"
408
+ };
409
+ }
410
+ }
411
+ });
412
+ var listFormSubmissions = tool({
413
+ description: "List form submissions for a Webflow site. Use this tool to retrieve submitted form data such as leads, contact requests, or signups. Optionally filter by elementId to get submissions for a specific form across all component instances. Get the elementId from the listForms tool (returned as formElementId). Supports pagination via limit and offset parameters." + siteIdDescription,
414
+ inputSchema: z.object({
415
+ siteId: z.string().optional().describe(
416
+ `The ID of the site.${siteIdDescription || " Use listSites to find available site IDs."}`
417
+ ),
418
+ elementId: z.string().optional().describe(
419
+ "Filter submissions to a specific form by its element ID. Get this from listForms (formElementId field)."
420
+ ),
421
+ limit: z.number().min(1).max(100).optional().describe(
422
+ "Maximum number of submissions to return. Default 100, max 100."
423
+ ),
424
+ offset: z.number().optional().describe("Offset for pagination if results exceed the limit.")
425
+ }),
426
+ inputExamples: [
427
+ { input: { siteId: "580e63e98c9a982ac9b8b741" } },
428
+ {
429
+ input: {
430
+ siteId: "580e63e98c9a982ac9b8b741",
431
+ elementId: "18259716-3e5a-646a-5f41-5dc4b9405aa0",
432
+ limit: 25
433
+ }
434
+ }
435
+ ],
436
+ outputSchema: ListFormSubmissionsResultSchema,
437
+ strict: true,
438
+ execute: async ({ siteId, elementId, limit, offset }) => {
439
+ try {
440
+ const resolvedSiteId = resolveSiteId(siteId);
441
+ const response = await callApi(
442
+ `/sites/${resolvedSiteId}/form_submissions`,
443
+ { params: { elementId, limit, offset } }
444
+ );
445
+ const rawSubmissions = response.formSubmissions ?? [];
446
+ const formSubmissions = rawSubmissions.map((submission) => ({
447
+ id: String(submission.id ?? ""),
448
+ displayName: submission.displayName ? String(submission.displayName) : void 0,
449
+ dateSubmitted: getStringField(
450
+ submission,
451
+ "date_submitted",
452
+ "dateSubmitted"
453
+ ),
454
+ formResponse: submission.formResponse ?? {}
455
+ }));
456
+ const pagination = response.pagination;
457
+ return {
458
+ formSubmissions,
459
+ count: formSubmissions.length,
460
+ pagination: pagination ? {
461
+ limit: Number(pagination.limit ?? 0),
462
+ offset: Number(pagination.offset ?? 0),
463
+ total: Number(pagination.total ?? 0)
464
+ } : void 0
465
+ };
466
+ } catch (error) {
467
+ console.error("Error listing form submissions:", error);
468
+ return {
469
+ formSubmissions: [],
470
+ count: 0,
471
+ error: error instanceof Error ? error.message : "Failed to list form submissions"
472
+ };
473
+ }
474
+ }
475
+ });
476
+ var listCustomCode = tool({
477
+ description: "List all custom code scripts applied to a Webflow site and its pages. Use this tool to audit what scripts are currently active, check script versions, or see where scripts are applied (site-level vs page-level). Supports pagination via limit and offset parameters." + siteIdDescription,
478
+ inputSchema: z.object({
479
+ siteId: z.string().optional().describe(
480
+ `The ID of the site.${siteIdDescription || " Use listSites to find available site IDs."}`
481
+ ),
482
+ limit: z.number().min(1).max(100).optional().describe(
483
+ "Maximum number of code blocks to return. Default 100, max 100."
484
+ ),
485
+ offset: z.number().optional().describe("Offset for pagination if results exceed the limit.")
486
+ }),
487
+ inputExamples: [
488
+ { input: { siteId: "580e63e98c9a982ac9b8b741" } },
489
+ { input: { siteId: "580e63e98c9a982ac9b8b741", limit: 10 } }
490
+ ],
491
+ outputSchema: ListCustomCodeResultSchema,
492
+ strict: true,
493
+ execute: async ({ siteId, limit, offset }) => {
494
+ try {
495
+ const resolvedSiteId = resolveSiteId(siteId);
496
+ const response = await callApi(
497
+ `/sites/${resolvedSiteId}/custom_code/blocks`,
498
+ { params: { limit, offset } }
499
+ );
500
+ const rawBlocks = response.blocks ?? [];
501
+ const blocks = rawBlocks.map((block) => {
502
+ const rawScripts = block.scripts ?? [];
503
+ return {
504
+ siteId: String(block.siteId ?? ""),
505
+ pageId: block.pageId ? String(block.pageId) : void 0,
506
+ type: block.type ? String(block.type) : void 0,
507
+ scripts: rawScripts.map((script) => ({
508
+ id: String(script.id ?? ""),
509
+ location: String(script.location ?? ""),
510
+ version: String(script.version ?? "")
511
+ })),
512
+ createdOn: getStringField(block, "created_on", "createdOn"),
513
+ lastUpdated: getStringField(block, "last_updated", "lastUpdated")
514
+ };
515
+ });
516
+ const pagination = response.pagination;
517
+ return {
518
+ blocks,
519
+ count: blocks.length,
520
+ pagination: pagination ? {
521
+ limit: Number(pagination.limit ?? 0),
522
+ offset: Number(pagination.offset ?? 0),
523
+ total: Number(pagination.total ?? 0)
524
+ } : void 0
525
+ };
526
+ } catch (error) {
527
+ console.error("Error listing custom code:", error);
528
+ return {
529
+ blocks: [],
530
+ count: 0,
531
+ error: error instanceof Error ? error.message : "Failed to list custom code"
532
+ };
533
+ }
534
+ }
535
+ });
536
+ var addCustomCode = tool({
537
+ description: "Register and apply an inline script to a Webflow site or a specific page. Use this tool when the user wants to add tracking scripts (e.g. Google Analytics, Meta Pixel), custom JavaScript, chat widgets, or any inline script. This tool handles both registering the script and applying it in a single step. Inline scripts are limited to 2000 characters. The site must be published after adding custom code for changes to take effect." + siteIdDescription,
538
+ inputSchema: z.object({
539
+ siteId: z.string().optional().describe(
540
+ `The ID of the site to register the script on.${siteIdDescription || " Use listSites to find available site IDs."}`
541
+ ),
542
+ target: z.enum(["site", "page"]).describe(
543
+ 'Where to apply the script. Use "site" for site-wide scripts or "page" for a specific page.'
544
+ ),
545
+ pageId: z.string().optional().describe(
546
+ 'The ID of the page to apply the script to. Required when target is "page". Use listPages to find page IDs.'
547
+ ),
548
+ sourceCode: z.string().describe("The JavaScript source code to add. Maximum 2000 characters."),
549
+ displayName: z.string().describe(
550
+ "A user-facing name for the script. Must be between 1 and 50 alphanumeric characters (e.g. 'Google Analytics', 'Chat Widget')."
551
+ ),
552
+ version: z.string().describe(
553
+ 'A Semantic Version string for the script (e.g. "1.0.0", "0.0.1").'
554
+ ),
555
+ location: z.enum(["header", "footer"]).describe(
556
+ 'Where to place the script on the page. Use "header" for scripts that need to load early (e.g. analytics) or "footer" for scripts that can load after content.'
557
+ )
558
+ }),
559
+ inputExamples: [
560
+ {
561
+ input: {
562
+ siteId: "580e63e98c9a982ac9b8b741",
563
+ target: "site",
564
+ sourceCode: "console.log('Hello from Webflow!');",
565
+ displayName: "Hello Script",
566
+ version: "1.0.0",
567
+ location: "footer"
568
+ }
569
+ },
570
+ {
571
+ input: {
572
+ siteId: "580e63e98c9a982ac9b8b741",
573
+ target: "page",
574
+ pageId: "63c720f9347c2139b248e552",
575
+ sourceCode: "!function(f,b,e,v,n,t,s){/* Meta Pixel */}(window,document,'script','https://connect.facebook.net/en_US/fbevents.js');",
576
+ displayName: "Meta Pixel",
577
+ version: "1.0.0",
578
+ location: "header"
579
+ }
580
+ }
581
+ ],
582
+ outputSchema: AddCustomCodeResultSchema,
583
+ strict: true,
584
+ needsApproval: true,
585
+ execute: async ({
586
+ siteId,
587
+ target,
588
+ pageId,
589
+ sourceCode,
590
+ displayName,
591
+ version,
592
+ location
593
+ }) => {
594
+ try {
595
+ if (target === "page" && !pageId) {
596
+ return {
597
+ success: false,
598
+ scriptId: "",
599
+ error: 'A pageId is required when target is "page". Use listPages to find page IDs.'
600
+ };
601
+ }
602
+ if (sourceCode.length > 2e3) {
603
+ return {
604
+ success: false,
605
+ scriptId: "",
606
+ error: `Script exceeds the 2000 character limit (${sourceCode.length} characters). Consider hosting the script externally.`
607
+ };
608
+ }
609
+ const resolvedSiteId = resolveSiteId(siteId);
610
+ const registered = await callApi(
611
+ `/sites/${resolvedSiteId}/registered_scripts/inline`,
612
+ {
613
+ method: "POST",
614
+ body: { sourceCode, version, displayName }
615
+ }
616
+ );
617
+ const scriptId = String(registered.id ?? "");
618
+ if (!scriptId) {
619
+ return {
620
+ success: false,
621
+ scriptId: "",
622
+ error: "Failed to register script: no script ID returned"
623
+ };
624
+ }
625
+ const scriptPayload = {
626
+ scripts: [{ id: scriptId, location, version }]
627
+ };
628
+ if (target === "page" && pageId) {
629
+ await callApi(`/pages/${pageId}/custom_code`, {
630
+ method: "PUT",
631
+ body: scriptPayload
632
+ });
633
+ } else {
634
+ await callApi(`/sites/${resolvedSiteId}/custom_code`, {
635
+ method: "PUT",
636
+ body: scriptPayload
637
+ });
638
+ }
639
+ return {
640
+ success: true,
641
+ scriptId,
642
+ appliedTo: target === "page" ? `page:${pageId}` : `site:${resolvedSiteId}`
643
+ };
644
+ } catch (error) {
645
+ console.error("Error adding custom code:", error);
646
+ return {
647
+ success: false,
648
+ scriptId: "",
649
+ error: error instanceof Error ? error.message : "Failed to add custom code"
650
+ };
651
+ }
652
+ }
653
+ });
654
+ export {
655
+ addCustomCode,
656
+ listCustomCode,
657
+ listFormSubmissions,
658
+ listForms,
659
+ listPages,
660
+ listSites,
661
+ publishSite
662
+ };
package/package.json ADDED
@@ -0,0 +1,65 @@
1
+ {
2
+ "name": "@224industries/webflow-ai-sdk",
3
+ "version": "0.0.0",
4
+ "description": "Webflow tools for the AI SDK",
5
+ "keywords": [
6
+ "ai",
7
+ "ai-sdk",
8
+ "webflow",
9
+ "cms",
10
+ "tools"
11
+ ],
12
+ "homepage": "https://github.com/224-Industries/webflow-ai-sdk",
13
+ "bugs": {
14
+ "url": "https://github.com/224-Industries/webflow-ai-sdk/issues"
15
+ },
16
+ "license": "MIT",
17
+ "author": {
18
+ "name": "Ben Sabic",
19
+ "url": "https://bensabic.dev"
20
+ },
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/224-Industries/webflow-ai-sdk.git"
24
+ },
25
+ "main": "./dist/index.js",
26
+ "module": "./dist/index.js",
27
+ "types": "./dist/index.d.ts",
28
+ "exports": {
29
+ ".": {
30
+ "import": "./dist/index.js",
31
+ "types": "./dist/index.d.ts"
32
+ }
33
+ },
34
+ "files": [
35
+ "dist",
36
+ "README.md"
37
+ ],
38
+ "type": "module",
39
+ "scripts": {
40
+ "build": "tsup src/index.ts --format esm --dts",
41
+ "test": "vitest run",
42
+ "prepublishOnly": "pnpm build",
43
+ "check": "ultracite check",
44
+ "fix": "ultracite fix",
45
+ "bump-deps": "npx npm-check-updates -u -p pnpm && pnpm install",
46
+ "type-check": "tsc --noEmit"
47
+ },
48
+ "devDependencies": {
49
+ "@biomejs/biome": "2.3.15",
50
+ "@types/node": "^25.2.3",
51
+ "tsup": "^8.5.1",
52
+ "tsx": "^4.21.0",
53
+ "typescript": "^5.9.3",
54
+ "ultracite": "7.1.5",
55
+ "vitest": "^4.0.18"
56
+ },
57
+ "peerDependencies": {
58
+ "ai": "^6.0.67",
59
+ "zod": "^4.3.6"
60
+ },
61
+ "engines": {
62
+ "node": ">=20"
63
+ },
64
+ "packageManager": "pnpm@10.29.3"
65
+ }