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,34 @@
import { createAction } from '@activepieces/pieces-framework';
import { webscrapingAiAuth, webscrapingAiCommon } from '../common';
export const askAQuestionAboutTheWebPage = createAction({
auth: webscrapingAiAuth,
name: 'askAQuestionAboutTheWebPage',
displayName: 'Ask a Question About the Web Page',
description: 'Gets an answer to a question about a given webpage.',
props: webscrapingAiCommon.askQuestionProperties,
async run({ auth: apiKey, propsValue }) {
const { device, format, question, ...rest } = propsValue;
const allowedCountries = [
'us', 'gb', 'de', 'it', 'fr', 'ca', 'es', 'ru', 'jp', 'kr', 'in'
];
const params: any = {
apiKey:apiKey.secret_text,
question,
...rest,
format: (format === 'json' || format === 'text') ? format : undefined,
proxy: (rest.proxy === 'datacenter' || rest.proxy === 'residential') ? rest.proxy : undefined,
country: (rest.country && allowedCountries.includes(rest.country))
? rest.country as typeof allowedCountries[number]
: undefined,
headers: rest.headers && Array.isArray(rest.headers)
? Object.fromEntries(rest.headers.map((h: any) => [(h as any).name, (h as any).value]))
: undefined,
device: device as 'desktop' | 'mobile' | 'tablet' | undefined,
};
return await webscrapingAiCommon.askQuestion(params);
},
});

View File

@@ -0,0 +1,36 @@
import { createAction } from '@activepieces/pieces-framework';
import { webscrapingAiAuth, webscrapingAiCommon } from '../common';
export const extractStructuredData = createAction({
auth: webscrapingAiAuth,
name: 'extractStructuredData',
displayName: 'Extract structured data',
description:
'Returns structured data fields extracted from the webpage using an LLM model.',
props: webscrapingAiCommon.getPageStructuredDataProperties,
async run({ auth: apiKey, propsValue }) {
const { fields, headers, ...rest } = propsValue;
const allowedCountries = [
'us', 'gb', 'de', 'it', 'fr', 'ca', 'es', 'ru', 'jp', 'kr', 'in'
];
const params: any = {
apiKey:apiKey.secret_text,
...rest,
proxy: (rest.proxy === 'datacenter' || rest.proxy === 'residential') ? rest.proxy : undefined,
country: (rest.country && allowedCountries.includes(rest.country))
? rest.country as typeof allowedCountries[number]
: undefined,
headers: headers && Array.isArray(headers)
? Object.fromEntries(headers.map((h: any) => [(h as any).name, (h as any).value]))
: undefined,
fields: fields && typeof fields === 'object'
? Object.fromEntries(Object.entries(fields).map(([k, v]) => [k, String(v)]))
: {},
device: rest.device as 'desktop' | 'mobile' | 'tablet' | undefined,
};
return await webscrapingAiCommon.getPageStructuredData(params);
},
});

View File

@@ -0,0 +1,13 @@
import { createAction } from '@activepieces/pieces-framework';
import { webscrapingAiAuth, webscrapingAiCommon } from '../common';
export const getAccountInformation = createAction({
auth: webscrapingAiAuth,
name: 'getAccountInformation',
displayName: 'Get Account Info',
description: 'Get account usage information including remaining API credits and concurrent requests.',
props: {},
async run({ auth: apiKey }) {
return await webscrapingAiCommon.getAccountInformation({ apiKey:apiKey.secret_text });
},
});

View File

@@ -0,0 +1,36 @@
import { createAction } from '@activepieces/pieces-framework';
import { webscrapingAiAuth, webscrapingAiCommon } from '../common';
export const getPageHtml = createAction({
auth: webscrapingAiAuth,
name: 'getPageHtml',
displayName: 'Get Page HTML',
description: 'Retrieves the raw HTML markup of a web page.',
props: webscrapingAiCommon.getPageHtmlProperties,
async run({ auth: apiKey, propsValue }) {
const { format, headers, proxy, device, errorOn404, errorOnRedirect, returnScriptResult, ...rest } = propsValue;
const allowedCountries = [
'us', 'gb', 'de', 'it', 'fr', 'ca', 'es', 'ru', 'jp', 'kr', 'in'
];
const params: any = {
apiKey:apiKey.secret_text,
...rest,
format: (format === 'json' || format === 'text') ? format : undefined,
proxy: (proxy === 'datacenter' || proxy === 'residential') ? proxy : undefined,
country: (rest.country && allowedCountries.includes(rest.country))
? rest.country as typeof allowedCountries[number]
: undefined,
headers: headers && Array.isArray(headers)
? Object.fromEntries(headers.map((h: any) => [(h as any).name, (h as any).value]))
: undefined,
device: device as 'desktop' | 'mobile' | 'tablet' | undefined,
errorOn404,
errorOnRedirect,
returnScriptResult,
};
return await webscrapingAiCommon.getPageHtml(params);
},
});

View File

@@ -0,0 +1,37 @@
import { createAction } from '@activepieces/pieces-framework';
import { webscrapingAiAuth, webscrapingAiCommon } from '../common';
export const scrapeWebsiteText = createAction({
auth: webscrapingAiAuth,
name: 'scrapeWebsiteText',
displayName: 'Scrape Website Text',
description:
'Returns the visible text content of a webpage specified by the URL.',
props: webscrapingAiCommon.getPageTextProperties,
async run({ auth: apiKey, propsValue }) {
const { textFormat, headers, returnLinks, ...rest } = propsValue;
const allowedCountries = [
'us', 'gb', 'de', 'it', 'fr', 'ca', 'es', 'ru', 'jp', 'kr', 'in'
];
const params: any = {
apiKey:apiKey.secret_text,
...rest,
textFormat: (textFormat === 'json' || textFormat === 'plain' || textFormat === 'xml')
? textFormat
: undefined,
returnLinks: (textFormat === 'json') ? returnLinks : undefined,
proxy: (rest.proxy === 'datacenter' || rest.proxy === 'residential') ? rest.proxy : undefined,
country: (rest.country && allowedCountries.includes(rest.country))
? rest.country as typeof allowedCountries[number]
: undefined,
headers: headers && Array.isArray(headers)
? Object.fromEntries(headers.map((h: any) => [(h as any).name, (h as any).value]))
: undefined,
device: rest.device as 'desktop' | 'mobile' | 'tablet' | undefined,
};
return await webscrapingAiCommon.getPageText(params);
},
});

View File

@@ -0,0 +1,488 @@
import {
httpClient,
HttpMethod,
QueryParams,
} from '@activepieces/pieces-common';
import { PieceAuth, Property } from '@activepieces/pieces-framework';
import { pickBy } from '@activepieces/shared';
const baseRequestProperties = {
url: Property.ShortText({
displayName: 'URL',
description: 'URL of the target page.',
required: true,
}),
headers: Property.Array({
displayName: 'Custom Headers',
description: 'Add custom HTTP headers (optional)',
required: false,
properties: {
name: Property.ShortText({
displayName: 'Header Name',
description: 'Header name (e.g., User-Agent, Authorization)',
required: true,
}),
value: Property.ShortText({
displayName: 'Header Value',
description: 'Header value',
required: true,
}),
},
}),
timeout: Property.Number({
displayName: 'Timeout',
description: 'Maximum page load time in milliseconds (default: 10000, max: 30000)',
required: false,
defaultValue: 10000,
}),
js: Property.Checkbox({
displayName: 'Enable JavaScript',
description: 'Execute JavaScript for dynamic content (recommended)',
defaultValue: true,
required: false,
}),
jsTimeout: Property.Number({
displayName: 'JavaScript Timeout',
description: 'Maximum JavaScript execution time in milliseconds (default: 2000)',
required: false,
defaultValue: 2000,
}),
waitFor: Property.ShortText({
displayName: 'Wait For',
description: 'CSS selector to wait for dynamic content (e.g., ".content-loaded")',
required: false,
}),
proxy: Property.StaticDropdown({
displayName: 'Proxy Type',
description: 'Use residential proxies for sites that block datacenter IPs (more expensive)',
required: false,
defaultValue: 'datacenter',
options: {
options: [
{ label: '🏢 Datacenter (Fast)', value: 'datacenter' },
{ label: '🏠 Residential (Stealth)', value: 'residential' },
],
},
}),
country: Property.StaticDropdown({
displayName: 'Proxy Country',
description: 'Geographic location of the proxy server',
required: false,
defaultValue: 'us',
options: {
options: [
{ label: 'United States', value: 'us' },
{ label: 'Canada', value: 'ca' },
{ label: 'United Kingdom', value: 'gb' },
{ label: 'Germany', value: 'de' },
{ label: 'France', value: 'fr' },
{ label: 'Italy', value: 'it' },
{ label: 'Spain', value: 'es' },
{ label: 'Russia', value: 'ru' },
{ label: 'Japan', value: 'jp' },
{ label: 'South Korea', value: 'kr' },
{ label: 'India', value: 'in' },
],
},
}),
customProxy: Property.ShortText({
displayName: 'Custom Proxy',
description: 'Your proxy URL in format: http://user:password@host:port',
required: false,
}),
jsScript: Property.LongText({
displayName: 'JavaScript Code',
description: 'Custom JavaScript to execute (e.g., document.querySelector("button").click())',
required: false,
}),
};
export const webscrapingAiAuth = PieceAuth.SecretText({
displayName: 'API Key',
required: true,
});
export const webscrapingAiCommon = {
baseUrl: 'https://api.webscraping.ai',
endpoints: {
askQuestion: '/ai/question',
getPageHtml: '/html',
getPageText: '/text',
getStructuredData: '/ai/fields',
getAccountInfo: '/account',
},
askQuestionProperties: {
question: Property.ShortText({
displayName: 'Question',
description:
'Question or instructions to ask the LLM model about the target page.',
required: true,
}),
...baseRequestProperties,
device: Property.StaticDropdown({
displayName: 'Device Type',
description: 'Emulate specific device for responsive design testing',
required: false,
options: {
options: [
{ label: 'Desktop', value: 'desktop' },
{ label: 'Mobile', value: 'mobile' },
{ label: 'Tablet', value: 'tablet' },
],
},
}),
errorOn404: Property.Checkbox({
displayName: 'Error on 404',
description: 'Fail the action if the page returns a 404 error',
required: false,
}),
errorOnRedirect: Property.Checkbox({
displayName: 'Error on Redirect',
description: 'Fail the action if the page redirects to another URL',
required: false,
}),
format: Property.StaticDropdown({
displayName: 'Response Format',
description: 'Response format: Text (simple) or JSON (structured)',
required: false,
defaultValue: 'text',
options: {
options: [
{ label: 'Text', value: 'text' },
{ label: 'JSON', value: 'json' },
],
},
}),
},
getPageHtmlProperties: {
...baseRequestProperties,
returnScriptResult: Property.Checkbox({
displayName: 'Return JavaScript Result',
description:
'Return result of the custom JavaScript code (js_script parameter) \
execution on the target page (false by default, page HTML will be returned).',
required: false,
}),
device: Property.StaticDropdown({
displayName: 'Device Type',
description: 'Emulate specific device for responsive design testing',
required: false,
options: {
options: [
{ label: 'Desktop', value: 'desktop' },
{ label: 'Mobile', value: 'mobile' },
{ label: 'Tablet', value: 'tablet' },
],
},
}),
errorOn404: Property.Checkbox({
displayName: 'Error on 404',
description: 'Fail the action if the page returns a 404 error',
required: false,
}),
errorOnRedirect: Property.Checkbox({
displayName: 'Error on Redirect',
description: 'Fail the action if the page redirects to another URL',
required: false,
}),
format: Property.StaticDropdown({
displayName: 'Response Format',
description: 'Response format: Text (simple) or JSON (structured)',
required: false,
defaultValue: 'text',
options: {
options: [
{ label: 'Text', value: 'text' },
{ label: 'JSON', value: 'json' },
],
},
}),
},
getPageTextProperties: {
...baseRequestProperties,
textFormat: Property.StaticDropdown({
displayName: 'Text Format',
description: 'Response format: Plain text, JSON (with title/description/content), or XML',
required: false,
defaultValue: 'plain',
options: {
options: [
{ label: 'Plain Text', value: 'plain' },
{ label: 'JSON', value: 'json' },
{ label: 'XML', value: 'xml' },
],
},
}),
returnLinks: Property.Checkbox({
displayName: 'Return Links',
description: 'Include links in response (only works with JSON format)',
required: false,
}),
device: Property.StaticDropdown({
displayName: 'Device Type',
description: 'Emulate specific device for responsive design testing',
required: false,
options: {
options: [
{ label: 'Desktop', value: 'desktop' },
{ label: 'Mobile', value: 'mobile' },
{ label: 'Tablet', value: 'tablet' },
],
},
}),
errorOn404: Property.Checkbox({
displayName: 'Error on 404',
description: 'Fail the action if the page returns a 404 error',
required: false,
}),
errorOnRedirect: Property.Checkbox({
displayName: 'Error on Redirect',
description: 'Fail the action if the page redirects to another URL',
required: false,
}),
},
getPageStructuredDataProperties: {
fields: Property.Object({
displayName: 'Fields to Extract',
description: 'Define fields to extract (e.g., {"title": "Product title", "price": "Product price"})',
required: true,
}),
...baseRequestProperties,
device: Property.StaticDropdown({
displayName: 'Device Type',
description: 'Emulate specific device for responsive design testing',
required: false,
options: {
options: [
{ label: 'Desktop', value: 'desktop' },
{ label: 'Mobile', value: 'mobile' },
{ label: 'Tablet', value: 'tablet' },
],
},
}),
errorOn404: Property.Checkbox({
displayName: 'Error on 404',
description: 'Fail the action if the page returns a 404 error',
required: false,
}),
errorOnRedirect: Property.Checkbox({
displayName: 'Error on Redirect',
description: 'Fail the action if the page redirects to another URL',
required: false,
}),
},
askQuestion: async (params: askQuestionParams) => {
const rawParams: Record<string, string | number | boolean | undefined> = {
api_key: params.apiKey,
url: params.url,
question: params.question,
timeout: params.timeout,
js: params.js,
js_timeout: params.jsTimeout,
wait_for: params.waitFor,
proxy: params.proxy,
country: params.country,
custom_proxy: params.customProxy,
device: params.device,
error_on_404: params.errorOn404,
error_on_redirect: params.errorOnRedirect,
js_script: params.jsScript,
format: params.format,
headers: params.headers && Array.isArray(params.headers)
? JSON.stringify(Object.fromEntries(params.headers.map(h => [h.name, h.value])))
: undefined,
};
const filtered = pickBy(
rawParams,
(value) => value !== undefined
) as Record<string, string | number | boolean>;
const queryParams: QueryParams = Object.fromEntries(
Object.entries(filtered).map(([k, v]) => [k, String(v)])
) as QueryParams;
return await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${webscrapingAiCommon.baseUrl}${webscrapingAiCommon.endpoints.askQuestion}`,
queryParams,
});
},
getPageHtml: async (params: getPageHtmlParams) => {
const rawParams: Record<string, string | number | boolean | undefined> = {
api_key: params.apiKey,
url: params.url,
headers: params.headers && Array.isArray(params.headers)
? JSON.stringify(Object.fromEntries(params.headers.map(h => [h.name, h.value])))
: undefined,
timeout: params.timeout,
js: params.js,
js_timeout: params.jsTimeout,
wait_for: params.waitFor,
proxy: params.proxy,
country: params.country,
custom_proxy: params.customProxy,
device: params.device,
error_on_404: params.errorOn404,
error_on_redirect: params.errorOnRedirect,
js_script: params.jsScript,
return_script_result: params.returnScriptResult,
format: params.format,
};
const filtered = pickBy(
rawParams,
(value) => value !== undefined
) as Record<string, string | number | boolean>;
const queryParams: QueryParams = Object.fromEntries(
Object.entries(filtered).map(([k, v]) => [k, String(v)])
) as QueryParams;
return await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${webscrapingAiCommon.baseUrl}${webscrapingAiCommon.endpoints.getPageHtml}`,
queryParams,
});
},
getPageText: async (params: getPageTextParams) => {
const rawParams: Record<string, string | number | boolean | undefined> = {
api_key: params.apiKey,
text_format: params.textFormat,
return_links: params.returnLinks,
url: params.url,
headers: params.headers && Array.isArray(params.headers)
? JSON.stringify(Object.fromEntries(params.headers.map(h => [h.name, h.value])))
: undefined,
timeout: params.timeout,
js: params.js,
js_timeout: params.jsTimeout,
wait_for: params.waitFor,
proxy: params.proxy,
country: params.country,
custom_proxy: params.customProxy,
device: params.device,
error_on_404: params.errorOn404,
error_on_redirect: params.errorOnRedirect,
js_script: params.jsScript,
};
const filtered = pickBy(
rawParams,
(value) => value !== undefined
) as Record<string, string | number | boolean>;
const queryParams: QueryParams = Object.fromEntries(
Object.entries(filtered).map(([k, v]) => [k, String(v)])
) as QueryParams;
return await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${webscrapingAiCommon.baseUrl}${webscrapingAiCommon.endpoints.getPageText}`,
queryParams,
});
},
getPageStructuredData: async (params: getPageStructuredDataParams) => {
const rawParams: Record<string, string | number | boolean | undefined> = {
api_key: params.apiKey,
url: params.url,
headers: params.headers && Array.isArray(params.headers)
? JSON.stringify(Object.fromEntries(params.headers.map(h => [h.name, h.value])))
: undefined,
timeout: params.timeout,
js: params.js,
js_timeout: params.jsTimeout,
wait_for: params.waitFor,
proxy: params.proxy,
country: params.country,
custom_proxy: params.customProxy,
device: params.device,
error_on_404: params.errorOn404,
error_on_redirect: params.errorOnRedirect,
js_script: params.jsScript,
};
const filtered = pickBy(
rawParams,
(value) => value !== undefined
) as Record<string, string | number | boolean>;
const expandedFields: Record<string, string> = Object.fromEntries(
Object.entries(params.fields || {}).map(([key, value]) => [
`fields[${key}]`,
value,
])
);
const queryParams: QueryParams = {
...Object.fromEntries(
Object.entries(filtered).map(([k, v]) => [k, String(v)])
),
...expandedFields,
} as QueryParams;
return await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${webscrapingAiCommon.baseUrl}${webscrapingAiCommon.endpoints.getStructuredData}`,
queryParams,
});
},
getAccountInformation: async ({ apiKey }: AuthenticationRequired) => {
return await httpClient.sendRequest({
method: HttpMethod.GET,
url: `${webscrapingAiCommon.baseUrl}${webscrapingAiCommon.endpoints.getAccountInfo}`,
queryParams: { api_key: apiKey },
});
},
};
type AuthenticationRequired = {
apiKey: string;
};
interface baseRequestParams extends AuthenticationRequired {
url: string;
headers?: Array<{name: string, value: string}>;
timeout?: number;
js?: boolean;
jsTimeout?: number;
waitFor?: string;
proxy?: 'datacenter' | 'residential';
country?:
| 'us'
| 'gb'
| 'de'
| 'it'
| 'fr'
| 'ca'
| 'es'
| 'ru'
| 'jp'
| 'kr'
| 'in';
customProxy?: string;
jsScript?: string;
}
interface askQuestionParams extends baseRequestParams {
question: string;
device?: 'desktop' | 'mobile' | 'tablet';
errorOn404?: boolean;
errorOnRedirect?: boolean;
format?: 'json' | 'text';
}
interface getPageHtmlParams extends baseRequestParams {
returnScriptResult?: boolean;
device?: 'desktop' | 'mobile' | 'tablet';
errorOn404?: boolean;
errorOnRedirect?: boolean;
format?: 'json' | 'text';
}
interface getPageTextParams extends baseRequestParams {
textFormat?: 'plain' | 'xml' | 'json';
returnLinks?: boolean;
device?: 'desktop' | 'mobile' | 'tablet';
errorOn404?: boolean;
errorOnRedirect?: boolean;
}
interface getPageStructuredDataParams extends baseRequestParams {
fields: Record<string, string>;
device?: 'desktop' | 'mobile' | 'tablet';
errorOn404?: boolean;
errorOnRedirect?: boolean;
}