Add Activepieces integration for workflow automation

- Add Activepieces fork with SmoothSchedule custom piece
- Create integrations app with Activepieces service layer
- Add embed token endpoint for iframe integration
- Create Automations page with embedded workflow builder
- Add sidebar visibility fix for embed mode
- Add list inactive customers endpoint to Public API
- Include SmoothSchedule triggers: event created/updated/cancelled
- Include SmoothSchedule actions: create/update/cancel events, list resources/services/customers

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
poduck
2025-12-18 22:59:37 -05:00
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions

View File

@@ -0,0 +1,62 @@
import { createAction, DynamicPropsValue, Property } from '@activepieces/pieces-framework';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
import { WebflowApiClient } from '../common/client';
export const webflowCreateCollectionItemAction = createAction({
auth: webflowAuth,
name: 'create_collection_item',
displayName: 'Create Collection Item',
description: 'Creates new collection item.',
props: {
site_id: webflowProps.site_id,
collection_id: webflowProps.collection_id,
collection_fields: webflowProps.collection_fields,
is_archived: Property.Checkbox({
displayName: 'Is Archived',
description: 'Whether the item is archived or not',
required: false,
}),
is_draft: Property.Checkbox({
displayName: 'Is Draft',
description: 'Whether the item is a draft or not',
required: false,
}),
},
async run(context) {
const collectionId = context.propsValue.collection_id;
const isArchived = context.propsValue.is_archived;
const isDraft = context.propsValue.is_draft;
const collectionInputFields = context.propsValue.collection_fields;
const client = new WebflowApiClient(context.auth.access_token);
const { fields: CollectionFields } = await client.getCollection(collectionId);
const formattedCollectionFields: DynamicPropsValue = {};
for (const field of CollectionFields) {
const fieldValue = collectionInputFields[field.slug];
if (fieldValue !== undefined && fieldValue !== '') {
switch (field.type) {
case 'ImageRef':
case 'FileRef':
formattedCollectionFields[field.slug] = { url: fieldValue };
break;
case 'Set':
formattedCollectionFields[field.slug] = fieldValue.map((url: string) => ({ url: url }));
break;
case 'Number':
formattedCollectionFields[field.slug] = Number(fieldValue);
break;
default:
formattedCollectionFields[field.slug] = fieldValue;
}
}
}
return await client.createCollectionItem(collectionId, {
fields: { ...formattedCollectionFields, _archived: isArchived, _draft: isDraft },
});
},
});

View File

@@ -0,0 +1,26 @@
import { createAction } from '@activepieces/pieces-framework';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
import { WebflowApiClient } from '../common/client';
export const webflowDeleteCollectionItem = createAction({
auth: webflowAuth,
name: 'delete_collection_item',
description: 'Delete collection item',
displayName: 'Delete an item in a collection',
props: {
site_id: webflowProps.site_id,
collection_id: webflowProps.collection_id,
collection_item_id: webflowProps.collection_item_id,
},
async run(context) {
const collectionId = context.propsValue.collection_id;
const collectionItemId = context.propsValue.collection_item_id;
const client = new WebflowApiClient(context.auth.access_token);
return await client.deleteCollectionItem(collectionId, collectionItemId);
},
});

View File

@@ -0,0 +1,70 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import {
HttpRequest,
HttpMethod,
httpClient,
AuthenticationType,
} from '@activepieces/pieces-common';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
export const webflowFindCollectionItem = createAction({
auth: webflowAuth,
name: 'find_collection_item',
description: 'Find collection item in a collection by field',
displayName: 'Find a Collection Item by Field',
props: {
site_id: webflowProps.site_id,
collection_id: webflowProps.collection_id,
field_name: Property.ShortText({
displayName: 'Field Name',
description: 'The name of the field to search by',
required: true,
}),
field_value: Property.ShortText({
displayName: 'Field Value',
description: 'The value of the field to search for',
required: true,
}),
max_results: Property.Number({
displayName: 'Max Results',
description: 'The maximum number of results to return',
required: false,
}),
},
async run(configValue) {
const accessToken = configValue.auth['access_token'];
const collectionId = configValue.propsValue['collection_id'];
const fieldName = configValue.propsValue['field_name'];
const fieldValue = configValue.propsValue['field_value'];
const maxResults = configValue.propsValue['max_results'];
const request: HttpRequest = {
method: HttpMethod.GET,
url: `https://api.webflow.com/collections/${collectionId}/items`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
try {
const res = await httpClient.sendRequest(request);
if (res.status !== 200) {
throw new Error('Failed to fetch collection items');
}
const items = res.body.items;
const matches = items
.filter((item: any) => {
return item.fields[fieldName] === fieldValue;
})
.slice(0, maxResults);
return { success: true, result: matches };
} catch (err) {
return { success: false, message: err };
}
},
});

View File

@@ -0,0 +1,25 @@
import { createAction } from '@activepieces/pieces-framework';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
import { WebflowApiClient } from '../common/client';
export const webflowFindOrder = createAction({
auth: webflowAuth,
name: 'find_order',
description: 'Find order',
displayName: 'Find an order',
props: {
site_id: webflowProps.site_id,
order_id: webflowProps.order_id,
},
async run(context) {
const orderId = context.propsValue.order_id;
const siteId = context.propsValue.site_id;
const client = new WebflowApiClient(context.auth.access_token);
return await client.getOrder(siteId, orderId);
},
});

View File

@@ -0,0 +1,31 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
import { WebflowApiClient } from '../common/client';
export const webflowFulfillOrder = createAction({
auth: webflowAuth,
name: 'fulfill_order',
description: 'Fulfill order',
displayName: 'Fulfill an order',
props: {
site_id: webflowProps.site_id,
order_id: webflowProps.order_id,
send_order_fulfilled_email: Property.Checkbox({
displayName: 'Send Order Fulfilled Email',
description: 'Send an email to the customer that their order has been fulfilled',
required: false,
}),
},
async run(context) {
const orderId = context.propsValue.order_id;
const siteId = context.propsValue.site_id;
const sendOrderFulfilledEmail = context.propsValue.send_order_fulfilled_email;
const client = new WebflowApiClient(context.auth.access_token);
return await client.fulfillOrder(siteId, orderId, { sendOrderFulfilledEmail });
},
});

View File

@@ -0,0 +1,26 @@
import { createAction } from '@activepieces/pieces-framework';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
import { WebflowApiClient } from '../common/client';
export const webflowGetCollectionItem = createAction({
auth: webflowAuth,
name: 'get_collection_item',
description: 'Get collection item in a collection by ID',
displayName: 'Get a Collection Item by ID',
props: {
site_id: webflowProps.site_id,
collection_id: webflowProps.collection_id,
collection_item_id: webflowProps.collection_item_id,
},
async run(context) {
const collectionId = context.propsValue.collection_id;
const collectionItemId = context.propsValue.collection_item_id;
const client = new WebflowApiClient(context.auth.access_token);
return await client.getCollectionItem(collectionId, collectionItemId);
},
});

View File

@@ -0,0 +1,26 @@
import { createAction } from '@activepieces/pieces-framework';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
import { WebflowApiClient } from '../common/client';
export const webflowPublishCollectionItem = createAction({
auth: webflowAuth,
name: 'publish_collection_item',
description: 'Publish collection item',
displayName: 'Publish a Collection Item',
props: {
site_id: webflowProps.site_id,
collection_id: webflowProps.collection_id,
collection_item_id: webflowProps.collection_item_id,
},
async run(context) {
const collectionId = context.propsValue.collection_id;
const collectionItemId = context.propsValue.collection_item_id;
const client = new WebflowApiClient(context.auth.access_token);
return await client.publishCollectionItem(collectionId, collectionItemId);
},
});

View File

@@ -0,0 +1,47 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
import { WebflowApiClient } from '../common/client';
export const webflowRefundOrder = createAction({
auth: webflowAuth,
name: 'refund_order',
description: 'Refund order',
displayName: 'Refund an order',
props: {
site_id: webflowProps.site_id,
order_id: webflowProps.order_id,
// reason: Property.StaticDropdown({
// displayName: 'Reason',
// description: 'The reason for the refund',
// required: false,
// options: {
// disabled: false,
// options: [
// {
// label: 'Duplicate',
// value: 'duplicate',
// },
// {
// label: 'Fraudulent',
// value: 'fraudulent',
// },
// {
// label: 'Requested',
// value: 'requested',
// },
// ],
// },
// }),
},
async run(context) {
const orderId = context.propsValue.order_id;
const siteId = context.propsValue.site_id;
const client = new WebflowApiClient(context.auth.access_token);
return await client.refundOrder(siteId, orderId);
},
});

View File

@@ -0,0 +1,25 @@
import { createAction } from '@activepieces/pieces-framework';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
import { WebflowApiClient } from '../common/client';
export const webflowUnfulfillOrder = createAction({
auth: webflowAuth,
name: 'unfulfill_order',
description: 'Unfulfill order',
displayName: 'Unfulfill an order',
props: {
site_id: webflowProps.site_id,
order_id: webflowProps.order_id,
},
async run(context) {
const orderId = context.propsValue.order_id;
const siteId = context.propsValue.site_id;
const client = new WebflowApiClient(context.auth.access_token);
return await client.unfulfillOrder(siteId, orderId);
},
});

View File

@@ -0,0 +1,74 @@
import { createAction, DynamicPropsValue, Property } from '@activepieces/pieces-framework';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
import { WebflowApiClient } from '../common/client';
export const webflowUpdateCollectionItem = createAction({
auth: webflowAuth,
name: 'update_collection_item',
description: 'Update collection item',
displayName: 'Update an item in a collection',
props: {
site_id: webflowProps.site_id,
collection_id: webflowProps.collection_id,
collection_item_id: webflowProps.collection_item_id,
collection_fields: webflowProps.collection_fields,
is_archived: Property.Checkbox({
displayName: 'Is Archived',
description: 'Whether the item is archived or not',
required: false,
}),
is_draft: Property.Checkbox({
displayName: 'Is Draft',
description: 'Whether the item is a draft or not',
required: false,
}),
},
async run(context) {
const collectionId = context.propsValue.collection_id;
const collectionItemId = context.propsValue.collection_item_id;
const isArchived = context.propsValue.is_archived;
const isDraft = context.propsValue.is_draft;
const collectionInputFields = context.propsValue.collection_fields;
const client = new WebflowApiClient(context.auth.access_token);
const { fields: CollectionFields } = await client.getCollection(collectionId);
const formattedCollectionFields: DynamicPropsValue = {};
for (const field of CollectionFields) {
const fieldValue = collectionInputFields[field.slug];
if (fieldValue !== undefined && fieldValue !== '') {
switch (field.type) {
case 'ImageRef':
case 'FileRef':
formattedCollectionFields[field.slug] = { url: fieldValue };
break;
case 'Set':
if (fieldValue.length > 0) {
formattedCollectionFields[field.slug] = fieldValue.map((url: string) => ({
url: url,
}));
}
break;
case 'ItemRefSet':
if (fieldValue.length > 0) {
formattedCollectionFields[field.slug] = fieldValue;
}
break;
case 'Number':
formattedCollectionFields[field.slug] = Number(fieldValue);
break;
default:
formattedCollectionFields[field.slug] = fieldValue;
}
}
}
return await client.updateCollectionItem(collectionId, collectionItemId, {
fields: { ...formattedCollectionFields, _archived: isArchived, _draft: isDraft },
});
},
});

View File

@@ -0,0 +1,148 @@
import {
AuthenticationType,
httpClient,
HttpMethod,
HttpRequest,
QueryParams,
} from '@activepieces/pieces-common';
export class WebflowApiClient {
constructor(private accessToken: string) {}
async makeRequest(
method: HttpMethod,
resourceUri: string,
query?: Record<string, string | number | string[] | undefined>,
body: any | undefined = undefined,
): Promise<any> {
const apiUrl = 'https://api.webflow.com';
const params: QueryParams = {};
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined) {
params[key] = String(value);
}
}
}
const request: HttpRequest = {
method: method,
url: apiUrl + resourceUri,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: this.accessToken,
},
queryParams: params,
body: body,
};
const response = await httpClient.sendRequest(request);
return response.body;
}
async listSites() {
return await this.makeRequest(HttpMethod.GET, '/sites');
}
async listCollections(siteId: string) {
return await this.makeRequest(HttpMethod.GET, `/sites/${siteId}/collections`);
}
async getCollection(collectionId: string) {
return await this.makeRequest(HttpMethod.GET, `/collections/${collectionId}`);
}
async createCollectionItem(collectionId: string, request: Record<string, any>) {
return await this.makeRequest(
HttpMethod.POST,
`/collections/${collectionId}/items`,
undefined,
request,
);
}
async updateCollectionItem(collectionId: string, itemId: string, request: Record<string, any>) {
return await this.makeRequest(
HttpMethod.PUT,
`/collections/${collectionId}/items/${itemId}`,
undefined,
request,
);
}
async getCollectionItem(collectionId: string, itemId: string) {
return await this.makeRequest(HttpMethod.GET, `/collections/${collectionId}/items/${itemId}`);
}
async deleteCollectionItem(collectionId: string, itemId: string) {
return await this.makeRequest(
HttpMethod.DELETE,
`/collections/${collectionId}/items/${itemId}`,
);
}
async publishCollectionItem(collectionId: string, itemId: string) {
return await this.makeRequest(
HttpMethod.POST,
`/collections/${collectionId}/items/publish`,
undefined,
{ itemIds: [itemId] },
);
}
async listCollectionItems(collectionId: string, page: number, limit: number) {
return await this.makeRequest(HttpMethod.GET, `/collections/${collectionId}/items`, {
offset: page,
limit,
});
}
async getOrder(siteId: string, orderId: string) {
return await this.makeRequest(HttpMethod.GET, `/sites/${siteId}/orders/${orderId}`);
}
async fulfillOrder(siteId: string, orderId: string, request: Record<string, any>) {
return await this.makeRequest(
HttpMethod.POST,
`/sites/${siteId}/orders/${orderId}/fulfill`,
undefined,
request,
);
}
async unfulfillOrder(siteId: string, orderId: string) {
return await this.makeRequest(
HttpMethod.POST,
`/sites/${siteId}/orders/${orderId}/unfulfill`,
undefined,
);
}
async refundOrder(siteId: string, orderId: string) {
return await this.makeRequest(HttpMethod.POST, `/sites/${siteId}/orders/${orderId}/refund`);
}
async listOrders(siteId: string, page: number, limit: number) {
return await this.makeRequest(HttpMethod.GET, `/sites/${siteId}/orders`, {
offset: page,
limit,
});
}
async createWebhook(siteId: string, triggerType: string, webhookUrl: string) {
return await this.makeRequest(
HttpMethod.POST,
`'/sites/${siteId}/webhooks`,
{},
{
triggerType,
url: webhookUrl,
},
);
}
async deleteWebhook(webhookId: string) {
return await this.makeRequest(HttpMethod.DELETE, `/webhooks/${webhookId}`);
}
}

View File

@@ -0,0 +1,52 @@
import { Property, OAuth2PropertyValue, DynamicPropsValue } from '@activepieces/pieces-framework';
import {
HttpRequest,
HttpMethod,
AuthenticationType,
httpClient,
} from '@activepieces/pieces-common';
export const webflowCommon = {
baseUrl: 'https://api.webflow.com/',
subscribeWebhook: async (
siteId: string,
tag: string,
webhookUrl: string,
accessToken: string,
) => {
const request: HttpRequest = {
method: HttpMethod.POST,
url: `https://api.webflow.com/sites/${siteId}/webhooks`,
headers: {
'Content-Type': 'application/json',
},
body: {
triggerType: tag,
url: webhookUrl,
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
queryParams: {},
};
const res = await httpClient.sendRequest(request);
return res;
},
unsubscribeWebhook: async (siteId: string, webhookId: string, accessToken: string) => {
const request: HttpRequest = {
method: HttpMethod.DELETE,
url: `https://api.webflow.com/sites/${siteId}/webhooks/${webhookId}`,
headers: {
'Content-Type': 'application/json',
},
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
};
return await httpClient.sendRequest(request);
},
};

View File

@@ -0,0 +1,237 @@
import {
DropdownOption,
DynamicPropsValue,
PiecePropValueSchema,
Property,
} from '@activepieces/pieces-framework';
import { WebflowApiClient } from './client';
import { webflowAuth } from '../..';
export const webflowProps = {
site_id: Property.Dropdown({
auth: webflowAuth,
displayName: 'Site',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect account first.',
};
}
const authValue = auth;
const client = new WebflowApiClient(authValue.access_token);
const sites = await client.listSites();
const options: DropdownOption<string>[] = [];
for (const site of sites) {
options.push({ label: site.name, value: site._id });
}
return {
disabled: false,
options,
};
},
}),
collection_id: Property.Dropdown({
auth: webflowAuth,
displayName: 'Collection',
required: true,
refreshers: ['site_id'],
options: async ({ auth, site_id }) => {
if (!auth || !site_id) {
return {
disabled: true,
options: [],
placeholder: 'Please connect account first.',
};
}
const authValue = auth as PiecePropValueSchema<typeof webflowAuth>;
const client = new WebflowApiClient(authValue.access_token);
const collections = await client.listCollections(site_id as string);
const options: DropdownOption<string>[] = [];
for (const collection of collections) {
options.push({ label: collection.name, value: collection._id });
}
return {
disabled: false,
options,
};
},
}),
collection_fields: Property.DynamicProperties({
auth: webflowAuth,
displayName: 'Collection Fields',
required: true,
refreshers: ['collection_id'],
props: async ({ auth, collection_id }) => {
if (!auth) return {};
if (!collection_id) return {};
const collectionFields: DynamicPropsValue = {};
const authValue = auth as PiecePropValueSchema<typeof webflowAuth>;
const client = new WebflowApiClient(authValue.access_token);
const { fields } = await client.getCollection(collection_id as unknown as string);
for (const field of fields) {
if (field.editable && field.slug !== '_archived' && field.slug !== '_draft') {
switch (field.type) {
case 'Option':
collectionFields[field.slug] = Property.StaticDropdown({
displayName: field.name,
required: field.required,
options: {
disabled: false,
options: field.validations.options.map((option: { name: string }) => {
return {
label: option.name,
value: option.name,
};
}),
},
});
break;
case 'RichText':
case 'Email':
case 'PlainText':
case 'Phone':
case 'Link':
case 'Video':
case 'Color':
case 'ItemRef':
case 'FileRef':
collectionFields[field.slug] = Property.ShortText({
displayName: field.name,
required: field.required,
});
break;
case 'ImageRef':
collectionFields[field.slug] = Property.ShortText({
displayName: field.name,
required: field.required,
description:
'Images must be hosted on a publicly accessible URL to be uploaded via the API.The maximum file size for images is 4MB.',
});
break;
case 'Set':
collectionFields[field.slug] = Property.Array({
displayName: field.name,
required: field.required,
description:
' Images must be hosted on a publicly accessible URL to be uploaded via the API.The maximum file size for images is 4MB.',
});
break;
case 'ItemRefSet':
collectionFields[field.slug] = Property.Array({
displayName: field.name,
required: field.required,
});
break;
case 'Number':
collectionFields[field.slug] = Property.Number({
displayName: field.name,
required: field.required,
});
break;
case 'Date':
collectionFields[field.slug] = Property.DateTime({
displayName: field.name,
required: field.required,
});
break;
case 'Bool':
collectionFields[field.slug] = Property.Checkbox({
displayName: field.name,
required: false,
});
break;
}
}
}
return collectionFields;
},
}),
collection_item_id: Property.Dropdown({
auth: webflowAuth,
displayName: 'Collection Item',
required: true,
refreshers: ['collection_id'],
options: async ({ auth, collection_id }) => {
if (!auth || !collection_id) {
return {
disabled: true,
options: [],
placeholder: 'Please connect account first.',
};
}
const authValue = auth as PiecePropValueSchema<typeof webflowAuth>;
const client = new WebflowApiClient(authValue.access_token);
const options: DropdownOption<string>[] = [];
let page = 0;
let response;
do {
response = await client.listCollectionItems(collection_id as string, page, 100);
page += 100;
for (const item of response.items) {
options.push({ label: item.name, value: item._id });
}
} while (response.items.length > 0);
return {
disabled: false,
options,
};
},
}),
order_id: Property.Dropdown({
auth: webflowAuth,
displayName: 'Order',
required: true,
refreshers: ['site_id'],
options: async ({ auth, site_id }) => {
if (!auth || !site_id) {
return {
disabled: true,
options: [],
placeholder: 'Please connect account first.',
};
}
const authValue = auth as PiecePropValueSchema<typeof webflowAuth>;
const client = new WebflowApiClient(authValue.access_token);
const options: DropdownOption<string>[] = [];
let page = 0;
let response;
do {
response = await client.listOrders(site_id as string, page, 100);
page += 100;
for (const order of response) {
options.push({ label: order.orderId, value: order.orderId });
}
} while (response.length > 0);
return {
disabled: false,
options,
};
},
}),
};

View File

@@ -0,0 +1,79 @@
import { webflowCommon } from '../common/common';
import { createTrigger, TriggerStrategy, Property } from '@activepieces/pieces-framework';
import { getAccessTokenOrThrow } from '@activepieces/pieces-common';
import { webflowAuth } from '../..';
import { webflowProps } from '../common/props';
const triggerNameInStore = 'webflow_created_form_submissions_trigger';
export const webflowNewSubmission = createTrigger({
auth: webflowAuth,
name: 'new_submission',
displayName: 'New Submission',
description: 'Triggers when Webflow Site receives a new submission',
props: {
site_id: webflowProps.site_id,
formName: Property.ShortText({
displayName: 'Form Name',
required: false,
description: 'Copy from the form settings, or from one of the responses',
}),
},
type: TriggerStrategy.WEBHOOK,
// TODO remove and force testing as the data can be custom.
sampleData: {
name: 'Sample Form',
site: '62749158efef318abc8d5a0f',
data: {
field_one: 'mock valued',
},
d: '2022-09-14T12:35:16.117Z',
_id: '6321ca84df3949bfc6752327',
},
async onEnable(context) {
const formSubmissionTag = 'form_submission';
const res = await webflowCommon.subscribeWebhook(
context.propsValue['site_id']!,
formSubmissionTag,
context.webhookUrl,
getAccessTokenOrThrow(context.auth),
);
await context.store?.put<WebhookInformation>(triggerNameInStore, {
webhookId: res.body._id,
});
},
async onDisable(context) {
const response = await context.store?.get<WebhookInformation>(triggerNameInStore);
if (response !== null && response !== undefined) {
await webflowCommon.unsubscribeWebhook(
context.propsValue['site_id']!,
response.webhookId,
getAccessTokenOrThrow(context.auth),
);
}
},
async run(context) {
const body = context.payload.body as PayloadBody;
const { formName } = context.propsValue;
//if formName provided, trigger only required formName if it's matched; else trigger all forms in selected webflow site.
if (formName) {
if (body.name == formName) {
return [body];
} else {
return [];
}
} else {
return [body];
}
},
});
interface WebhookInformation {
webhookId: string;
}
type PayloadBody = {
name: string;
};