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
co-authored by Claude Opus 4.5
parent 9848268d34
commit 3aa7199503
16292 changed files with 1284892 additions and 4708 deletions
@@ -0,0 +1,108 @@
import { createAction } from '@activepieces/pieces-framework';
import { foreplayCoApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { findAds as findAdsProperties } from '../properties';
import { findAdsSchema } from '../schemas';
import { foreplayCoAuth } from '../..';
export const findAds = createAction({
auth: foreplayCoAuth,
name: 'findAds',
displayName: 'Find Ads',
description:
'Search and filter ads by text, dates, platforms, and categories.',
props: findAdsProperties(),
async run({ auth, propsValue }) {
// Validate props using Zod schema
const validation = findAdsSchema.safeParse(propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
const values = propsValue;
// Build query parameters properly handling arrays for API
const queryParams = new URLSearchParams();
// Add optional parameters if provided
if (values['query']) {
queryParams.append('query', String(values['query']));
}
if (values['start_date']) {
queryParams.append('start_date', String(values['start_date']));
}
if (values['end_date']) {
queryParams.append('end_date', String(values['end_date']));
}
if (values['order']) {
queryParams.append('order', String(values['order']));
}
if (values['live']) {
queryParams.append('live', String(values['live']));
}
// Handle array parameters - repeat parameter name for each value
if (values['display_format'] && values['display_format'].length > 0) {
values['display_format'].forEach((format: unknown) => {
queryParams.append('display_format', String(format));
});
}
if (
values['publisher_platform'] &&
values['publisher_platform'].length > 0
) {
values['publisher_platform'].forEach((platform: unknown) => {
queryParams.append('publisher_platform', String(platform));
});
}
if (values['niches'] && values['niches'].length > 0) {
values['niches'].forEach((niche: unknown) => {
queryParams.append('niches', String(niche));
});
}
if (values['market_target'] && values['market_target'].length > 0) {
values['market_target'].forEach((target: unknown) => {
queryParams.append('market_target', String(target));
});
}
if (values['languages'] && values['languages'].length > 0) {
values['languages'].forEach((language: unknown) => {
queryParams.append('languages', String(language));
});
}
if (values['cursor']) {
queryParams.append('cursor', String(values['cursor']));
}
if (values['limit']) {
queryParams.append('limit', String(values['limit']));
}
// Build the full URL with query parameters manually to handle arrays properly
const queryString = queryParams.toString();
const fullUrl = queryString
? `/api/discovery/ads?${queryString}`
: '/api/discovery/ads';
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: fullUrl,
});
const responseBody = response.body;
// Check if the response is successful
if (responseBody.metadata && responseBody.metadata.success === true) {
// Return just the ads data for clean automation workflows
return responseBody.data;
} else {
// Handle error responses by throwing an error
const errorMessage =
responseBody.error ||
responseBody.metadata?.message ||
'Failed to find ads';
throw new Error(`Foreplay.co API Error: ${errorMessage}`);
}
},
});
@@ -0,0 +1,54 @@
import { createAction } from '@activepieces/pieces-framework';
import { foreplayCoApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { findBoards as findBoardsProperties } from '../properties';
import { findBoardsSchema } from '../schemas';
import { foreplayCoAuth } from '../..';
export const findBoards = createAction({
name: 'findBoards',
displayName: 'Find Boards',
description: 'Get all boards for the authenticated user with pagination.',
props: findBoardsProperties(),
auth: foreplayCoAuth,
async run({ auth, propsValue }) {
// Validate props using Zod schema
const validation = findBoardsSchema.safeParse(propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
const values = propsValue;
const queryParams: Record<string, string> = {};
// Add optional parameters if provided
if (values['offset'] !== undefined) {
queryParams['offset'] = String(values['offset']);
}
if (values['limit'] !== undefined) {
queryParams['limit'] = String(values['limit']);
}
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: '/api/boards',
queryParams,
});
const responseBody = response.body;
// Check if the response is successful
if (responseBody.metadata && responseBody.metadata.success === true) {
// Return just the boards data for clean automation workflows
return responseBody.data;
} else {
// Handle error responses by throwing an error
const errorMessage =
responseBody.error ||
responseBody.metadata?.message ||
'Failed to retrieve boards';
throw new Error(`Foreplay.co API Error: ${errorMessage}`);
}
},
});
@@ -0,0 +1,53 @@
import { createAction } from '@activepieces/pieces-framework';
import { foreplayCoApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { findBrands as findBrandsProperties } from '../properties';
import { findBrandsSchema } from '../schemas';
import { foreplayCoAuth } from '../..';
export const findBrands = createAction({
name: 'findBrands',
displayName: 'Find Brands',
description: 'Search for brands by name with fuzzy matching.',
props: findBrandsProperties(),
auth: foreplayCoAuth,
async run({ auth, propsValue }) {
// Validate props using Zod schema
const validation = findBrandsSchema.safeParse(propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
const values = propsValue;
const queryParams: Record<string, string> = {
query: String(values['query']),
};
// Add optional limit parameter if provided
if (values['limit']) {
queryParams['limit'] = String(values['limit']);
}
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: '/api/discovery/brands',
queryParams,
});
const responseBody = response.body;
// Check if the response is successful
if (responseBody.metadata && responseBody.metadata.success === true) {
// Return just the brands data for clean automation workflows
return responseBody.data;
} else {
// Handle error responses by throwing an error
const errorMessage =
responseBody.error ||
responseBody.metadata?.message ||
'Failed to find brands';
throw new Error(`Foreplay.co API Error: ${errorMessage}`);
}
},
});
@@ -0,0 +1,44 @@
import { createAction } from '@activepieces/pieces-framework';
import { foreplayCoApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { getAdById as getAdByIdProperties } from '../properties';
import { getAdByIdSchema } from '../schemas';
import { foreplayCoAuth } from '../..';
export const getAdById = createAction({
name: 'getAdById',
displayName: 'Get Ad by ID',
description: 'Get detailed information about a specific ad by its ID.',
props: getAdByIdProperties(),
auth: foreplayCoAuth,
async run({ auth, propsValue }) {
// Validate props using Zod schema
const validation = getAdByIdSchema.safeParse(propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
const adId = propsValue.ad_id;
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: `/api/ad/${adId}`,
});
const responseBody = response.body;
// Check if the response is successful
if (responseBody.metadata && responseBody.metadata.success === true) {
// Return just the ad data for clean automation workflows
return responseBody.data;
} else {
// Handle error responses by throwing an error
const errorMessage =
responseBody.error ||
responseBody.metadata?.message ||
'Failed to retrieve ad';
throw new Error(`Foreplay.co API Error: ${errorMessage}`);
}
},
});
@@ -0,0 +1,103 @@
import { createAction } from '@activepieces/pieces-framework';
import { foreplayCoApiCall } from '../common';
import { HttpMethod } from '@activepieces/pieces-common';
import { getAdsByPage as getAdsByPageProperties } from '../properties';
import { getAdsByPageSchema } from '../schemas';
import { foreplayCoAuth } from '../..';
export const getAdsByPage = createAction({
name: 'getAdsByPage',
displayName: 'Get Ads by Page',
description:
'Get all ads for a Facebook Page ID with filtering and pagination.',
props: getAdsByPageProperties(),
auth: foreplayCoAuth,
async run({ auth, propsValue }) {
// Validate props using Zod schema
const validation = getAdsByPageSchema.safeParse(propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
const values = propsValue;
// Build query parameters properly handling arrays for API
const queryParams = new URLSearchParams();
queryParams.append('page_id', String(values.page_id));
// Add optional parameters if provided
if (values.start_date) {
queryParams.append('start_date', String(values.start_date));
}
if (values.end_date) {
queryParams.append('end_date', String(values.end_date));
}
if (values.order) {
queryParams.append('order', String(values.order));
}
if (values.live) {
queryParams.append('live', String(values.live));
}
// Handle array parameters - repeat parameter name for each value
if (values.display_format && values.display_format.length > 0) {
values.display_format.forEach((format: unknown) => {
queryParams.append('display_format', String(format));
});
}
if (values.publisher_platform && values.publisher_platform.length > 0) {
values.publisher_platform.forEach((platform: unknown) => {
queryParams.append('publisher_platform', String(platform));
});
}
if (values.niches && values.niches.length > 0) {
values.niches.forEach((niche: unknown) => {
queryParams.append('niches', String(niche));
});
}
if (values.market_target && values.market_target.length > 0) {
values.market_target.forEach((target: unknown) => {
queryParams.append('market_target', String(target));
});
}
if (values.languages && values.languages.length > 0) {
values.languages.forEach((language: unknown) => {
queryParams.append('languages', String(language));
});
}
if (values.cursor) {
queryParams.append('cursor', String(values.cursor));
}
if (values.limit) {
queryParams.append('limit', String(values.limit));
}
// Build the full URL with query parameters manually to handle arrays properly
const queryString = queryParams.toString();
const fullUrl = queryString
? `/api/brand/getAdsByPageId?${queryString}`
: '/api/brand/getAdsByPageId';
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: fullUrl,
});
const responseBody = response.body;
// Check if the response is successful
if (responseBody.metadata && responseBody.metadata.success === true) {
// Return just the ads data for clean automation workflows
return responseBody.data;
} else {
// Handle error responses by throwing an error
const errorMessage =
responseBody.error ||
responseBody.metadata?.message ||
'Failed to retrieve ads for page';
throw new Error(`Foreplay.co API Error: ${errorMessage}`);
}
},
});
@@ -0,0 +1,5 @@
export { getAdById } from './get-ad-by-id';
export { getAdsByPage } from './get-ads-by-page';
export { findBrands } from './find-brands';
export { findAds } from './find-ads';
export { findBoards } from './find-boards';
@@ -0,0 +1,32 @@
import { httpClient, HttpMethod, AuthenticationType } from "@activepieces/pieces-common";
import { AppConnectionValueForAuthProperty } from "@activepieces/pieces-framework";
import { foreplayCoAuth } from "..";
export interface ForeplayCoApiCallProps {
apiKey: AppConnectionValueForAuthProperty<typeof foreplayCoAuth>;
method: HttpMethod;
resourceUri: string;
queryParams?: Record<string, string>;
body?: any;
}
export async function foreplayCoApiCall({
apiKey,
method,
resourceUri,
queryParams,
body,
}: ForeplayCoApiCallProps) {
const baseUrl = "https://public.api.foreplay.co";
return httpClient.sendRequest({
method,
url: `${baseUrl}${resourceUri}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: apiKey.secret_text,
},
queryParams,
body,
});
}
@@ -0,0 +1,585 @@
import { Property } from '@activepieces/pieces-framework';
import { foreplayCoApiCall } from './common';
import { HttpMethod } from '@activepieces/pieces-common';
import { foreplayCoAuth } from '..';
// Common dropdown options (keeping existing functionality)
const orderOptions = () => ({
options: [
{ label: 'Newest', value: 'newest' },
{ label: 'Oldest', value: 'oldest' },
{ label: 'Longest Running', value: 'longest_running' },
{ label: 'Most Relevant', value: 'most_relevant' },
],
});
const liveStatusOptions = () => ({
options: [
{ label: 'Active Only', value: 'true' },
{ label: 'Inactive Only', value: 'false' },
],
});
const displayFormatOptions = () => ({
options: [
{ label: 'Video', value: 'video' },
{ label: 'Carousel', value: 'carousel' },
{ label: 'Image', value: 'image' },
{ label: 'DCO', value: 'dco' },
{ label: 'DPA', value: 'dpa' },
{ label: 'Multi Images', value: 'multi_images' },
{ label: 'Multi Videos', value: 'multi_videos' },
{ label: 'Multi Medias', value: 'multi_medias' },
{ label: 'Event', value: 'event' },
{ label: 'Text', value: 'text' },
],
});
const publisherPlatformOptions = () => ({
options: [
{ label: 'Facebook', value: 'facebook' },
{ label: 'Instagram', value: 'instagram' },
{ label: 'Audience Network', value: 'audience_network' },
{ label: 'Messenger', value: 'messenger' },
{ label: 'TikTok', value: 'tiktok' },
{ label: 'YouTube', value: 'youtube' },
{ label: 'LinkedIn', value: 'linkedin' },
{ label: 'Threads', value: 'threads' },
],
});
const nicheOptions = () => ({
options: [
{ label: 'Accessories', value: 'accessories' },
{ label: 'App/Software', value: 'app/software' },
{ label: 'Beauty', value: 'beauty' },
{ label: 'Business/Professional', value: 'business/professional' },
{ label: 'Education', value: 'education' },
{ label: 'Entertainment', value: 'entertainment' },
{ label: 'Fashion', value: 'fashion' },
{ label: 'Finance', value: 'finance' },
{ label: 'Food', value: 'food' },
{ label: 'Health', value: 'health' },
{ label: 'Home', value: 'home' },
{ label: 'Pets', value: 'pets' },
{ label: 'Sports', value: 'sports' },
{ label: 'Technology', value: 'technology' },
{ label: 'Travel', value: 'travel' },
{ label: 'Automotive', value: 'automotive' },
{ label: 'Other', value: 'other' },
],
});
const marketTargetOptions = () => ({
options: [
{ label: 'B2B (Business-to-Business)', value: 'b2b' },
{ label: 'B2C (Business-to-Consumer)', value: 'b2c' },
],
});
const languageOptions = () => ({
options: [
{ label: 'English', value: 'english' },
{ label: 'French', value: 'french' },
{ label: 'German', value: 'german' },
{ label: 'Italian', value: 'italian' },
{ label: 'Dutch/Flemish', value: 'dutch, flemish' },
{ label: 'Spanish', value: 'spanish' },
{ label: 'Portuguese', value: 'portuguese' },
{ label: 'Romanian', value: 'romanian' },
{ label: 'Russian', value: 'russian' },
{ label: 'Chinese', value: 'chinese' },
{ label: 'Japanese', value: 'japanese' },
{ label: 'Korean', value: 'korean' },
{ label: 'Arabic', value: 'arabic' },
{ label: 'Hindi', value: 'hindi' },
],
});
// Action Properties
export const findAds = () => ({
query: Property.ShortText({
displayName: 'Search Query',
description:
'Search text for ad name or description. Leave empty to search all ads with filters only.',
required: false,
}),
start_date: Property.DateTime({
displayName: 'Start Date',
description:
'Start date (inclusive). Format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS',
required: false,
}),
end_date: Property.DateTime({
displayName: 'End Date',
description:
'End date (inclusive). Format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS',
required: false,
}),
order: Property.StaticDropdown({
displayName: 'Order',
description:
'Order of results: newest (default), oldest, longest_running, or most_relevant',
required: false,
options: orderOptions(),
}),
live: Property.StaticDropdown({
displayName: 'Live Status',
description:
'Filter ads by live status. true means currently active ads, false means inactive ads.',
required: false,
options: liveStatusOptions(),
}),
display_format: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Display Format',
description: 'Filter by one or more display formats',
required: false,
refreshers: [],
options: async () => displayFormatOptions(),
}),
publisher_platform: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Publisher Platform',
description: 'Filter by one or more publisher platforms',
required: false,
refreshers: [],
options: async () => publisherPlatformOptions(),
}),
niches: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Niches',
description: 'Filter by one or more niches',
required: false,
refreshers: [],
options: async () => nicheOptions(),
}),
market_target: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Market Target',
description: 'Filter by market target',
required: false,
refreshers: [],
options: async () => marketTargetOptions(),
}),
languages: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Languages',
description: 'Filter by languages. Accepts various language formats.',
required: false,
refreshers: [],
options: async () => languageOptions(),
}),
cursor: Property.ShortText({
displayName: 'Cursor',
description:
'Cursor for pagination. Use the cursor value from the previous response.',
required: false,
}),
limit: Property.Number({
displayName: 'Limit',
description:
'Pagination limit (default 10, max 250). Controls the number of ads returned per request.',
required: false,
defaultValue: 10,
}),
});
export const getAdById = () => ({
ad_id: Property.ShortText({
displayName: 'Ad ID',
description:
'The unique identifier of the ad (e.g., "ad_1234567890"). You can find this ID from other Foreplay actions or the platform.',
required: true,
}),
});
export const getAdsByPage = () => ({
page_id: Property.ShortText({
displayName: 'Page ID',
description:
'The numeric Facebook page ID (e.g., "123456789"). You can find this in your Facebook page settings or from other Foreplay actions.',
required: true,
}),
start_date: Property.DateTime({
displayName: 'Start Date',
description:
'Start date (inclusive). Format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS',
required: false,
}),
end_date: Property.DateTime({
displayName: 'End Date',
description:
'End date (inclusive). Format: YYYY-MM-DD or YYYY-MM-DDTHH:MM:SS',
required: false,
}),
order: Property.StaticDropdown({
displayName: 'Order',
description:
'Order of results: newest (default), oldest, longest_running, or most_relevant',
required: false,
options: orderOptions(),
}),
live: Property.StaticDropdown({
displayName: 'Live Status',
description:
'Filter ads by live status. true means currently active ads, false means inactive ads.',
required: false,
options: liveStatusOptions(),
}),
display_format: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Display Format',
description: 'Filter by one or more display formats',
required: false,
refreshers: [],
options: async () => displayFormatOptions(),
}),
publisher_platform: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Publisher Platform',
description: 'Filter by one or more publisher platforms',
required: false,
refreshers: [],
options: async () => publisherPlatformOptions(),
}),
niches: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Niches',
description: 'Filter by one or more niches',
required: false,
refreshers: [],
options: async () => nicheOptions(),
}),
market_target: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Market Target',
description: 'Filter by market target',
required: false,
refreshers: [],
options: async () => marketTargetOptions(),
}),
languages: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Languages',
description: 'Filter by languages. Accepts various language formats.',
required: false,
refreshers: [],
options: async () => languageOptions(),
}),
cursor: Property.ShortText({
displayName: 'Cursor',
description:
'Cursor for pagination. Use the cursor value from the previous response.',
required: false,
}),
limit: Property.Number({
displayName: 'Limit',
description:
'Pagination limit (default 10, max 250). Controls the number of ads returned per request.',
required: false,
defaultValue: 10,
}),
});
export const findBrands = () => ({
query: Property.ShortText({
displayName: 'Brand Name',
description:
'Brand name to search for (e.g., "Nike", "Apple"). Supports fuzzy matching for partial names.',
required: true,
}),
limit: Property.Number({
displayName: 'Limit',
description: 'Number of brands to return (max 10).',
required: false,
defaultValue: 10,
}),
});
export const findBoards = () => ({
offset: Property.Number({
displayName: 'Offset',
description: 'The offset for pagination (default 0).',
required: false,
defaultValue: 0,
}),
limit: Property.Number({
displayName: 'Limit',
description: 'The limit for pagination (default 10, max 10).',
required: false,
defaultValue: 10,
}),
});
// Trigger Properties
export const newAdInBoard = () => ({
board_id: Property.Dropdown({
auth: foreplayCoAuth,
displayName: 'Board',
description: 'Select the board to monitor for new ads.',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
try {
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: '/api/boards',
});
const responseBody = response.body;
if (responseBody.metadata && responseBody.metadata.success === true) {
const boards = responseBody.data || [];
return {
options: boards.map((board: any) => ({
label: board.name || board.title || `Board ${board.id}`,
value: board.id,
})),
};
} else {
return {
disabled: true,
options: [],
placeholder: 'Failed to load boards',
};
}
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading boards',
};
}
},
}),
polling_interval: Property.Number({
displayName: 'Polling Interval (minutes)',
description: 'How often to check for new ads (in minutes).',
required: false,
defaultValue: 5,
}),
live: Property.StaticDropdown({
displayName: 'Live Status',
description:
'Filter ads by live status. true means currently active ads, false means inactive ads.',
required: false,
options: liveStatusOptions(),
}),
display_format: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Display Format',
description: 'Filter by one or more display formats',
required: false,
refreshers: [],
options: async () => displayFormatOptions(),
}),
publisher_platform: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Publisher Platform',
description: 'Filter by one or more publisher platforms',
required: false,
refreshers: [],
options: async () => publisherPlatformOptions(),
}),
niches: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Niches',
description: 'Filter by one or more niches',
required: false,
refreshers: [],
options: async () => nicheOptions(),
}),
market_target: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Market Target',
description: 'Filter by market target',
required: false,
refreshers: [],
options: async () => marketTargetOptions(),
}),
languages: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Languages',
description: 'Filter by languages. Accepts various language formats.',
required: false,
refreshers: [],
options: async () => languageOptions(),
}),
});
export const newAdInSpyder = () => ({
brand_id: Property.Dropdown({
auth: foreplayCoAuth,
displayName: 'Brand',
description: 'Select the brand to monitor for new ads.',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first',
};
}
try {
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: '/api/spyder/brands',
});
const responseBody = response.body;
if (responseBody.metadata && responseBody.metadata.success === true) {
const brands = responseBody.data || [];
return {
options: brands.map((brand: any) => ({
label: brand.name || brand.id,
value: brand.id,
})),
};
} else {
return {
disabled: true,
options: [],
placeholder: 'Failed to load brands',
};
}
} catch (error) {
return {
disabled: true,
options: [],
placeholder: 'Error loading brands',
};
}
},
}),
polling_interval: Property.Number({
displayName: 'Polling Interval (minutes)',
description: 'How often to check for new ads (in minutes).',
required: false,
defaultValue: 5,
}),
live: Property.StaticDropdown({
displayName: 'Live Status',
description:
'Filter ads by live status. true means currently active ads, false means inactive ads.',
required: false,
options: liveStatusOptions(),
}),
display_format: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Display Format',
description: 'Filter by one or more display formats',
required: false,
refreshers: [],
options: async () => displayFormatOptions(),
}),
publisher_platform: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Publisher Platform',
description: 'Filter by one or more publisher platforms',
required: false,
refreshers: [],
options: async () => publisherPlatformOptions(),
}),
niches: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Niches',
description: 'Filter by one or more niches',
required: false,
refreshers: [],
options: async () => nicheOptions(),
}),
market_target: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Market Target',
description: 'Filter by market target',
required: false,
refreshers: [],
options: async () => marketTargetOptions(),
}),
languages: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Languages',
description: 'Filter by languages. Accepts various language formats.',
required: false,
refreshers: [],
options: async () => languageOptions(),
}),
});
export const newSwipefileAd = () => ({
polling_interval: Property.Number({
displayName: 'Polling Interval (minutes)',
description: 'How often to check for new ads (in minutes).',
required: false,
defaultValue: 5,
}),
start_date: Property.DateTime({
displayName: 'Start Date',
description: 'Filter ads published after this date.',
required: false,
}),
end_date: Property.DateTime({
displayName: 'End Date',
description: 'Filter ads published before this date.',
required: false,
}),
live: Property.StaticDropdown({
displayName: 'Live Status',
description: 'Filter by ad status (active/inactive).',
required: false,
options: liveStatusOptions(),
}),
display_format: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Display Format',
description: 'Filter by ad format (video, image, carousel, etc.).',
required: false,
refreshers: [],
options: async () => displayFormatOptions(),
}),
publisher_platform: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Publisher Platform',
description: 'Filter by platform (Facebook, Instagram, etc.).',
required: false,
refreshers: [],
options: async () => publisherPlatformOptions(),
}),
niches: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Niches',
description: 'Filter by industry/category.',
required: false,
refreshers: [],
options: async () => nicheOptions(),
}),
market_target: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Market Target',
description: 'Filter by target audience (B2B, B2C).',
required: false,
refreshers: [],
options: async () => marketTargetOptions(),
}),
languages: Property.MultiSelectDropdown({
auth: foreplayCoAuth,
displayName: 'Languages',
description: 'Filter by ad language.',
required: false,
refreshers: [],
options: async () => languageOptions(),
}),
});
@@ -0,0 +1,149 @@
import z from 'zod';
// Common validation schemas for dropdown options
const orderOptions = z.enum([
'newest',
'oldest',
'longest_running',
'most_relevant',
]);
const liveStatusOptions = z.enum(['true', 'false']);
const displayFormatOptions = z.enum([
'video',
'carousel',
'image',
'dco',
'dpa',
'multi_images',
'multi_videos',
'multi_medias',
'event',
'text',
]);
const publisherPlatformOptions = z.enum([
'facebook',
'instagram',
'audience_network',
'messenger',
'tiktok',
'youtube',
'linkedin',
'threads',
]);
const nicheOptions = z.enum([
'accessories',
'app/software',
'beauty',
'business/professional',
'education',
'entertainment',
'fashion',
'finance',
'food',
'health',
'home',
'pets',
'sports',
'technology',
'travel',
'automotive',
'other',
]);
const marketTargetOptions = z.enum(['b2b', 'b2c']);
const languageOptions = z.enum([
'english',
'french',
'german',
'italian',
'dutch, flemish',
'spanish',
'portuguese',
'romanian',
'russian',
'chinese',
'japanese',
'korean',
'arabic',
'hindi',
]);
const brandOrderOptions = z.enum(['most_ranked', 'least_ranked']);
// Action Schemas (Zod objects for validation)
export const findAdsSchema = z.object({
query: z.string().optional(),
start_date: z.string().optional(),
end_date: z.string().optional(),
order: orderOptions.optional(),
live: liveStatusOptions.optional(),
display_format: z.array(displayFormatOptions).optional(),
publisher_platform: z.array(publisherPlatformOptions).optional(),
niches: z.array(nicheOptions).optional(),
market_target: z.array(marketTargetOptions).optional(),
languages: z.array(languageOptions).optional(),
cursor: z.string().optional(),
limit: z.number().min(1).max(250).optional(),
});
export const getAdByIdSchema = z.object({
ad_id: z.string().min(1, 'Ad ID is required'),
});
export const getAdsByPageSchema = z.object({
page_id: z.string().min(1, 'Page ID is required'),
start_date: z.string().optional(),
end_date: z.string().optional(),
order: orderOptions.optional(),
live: liveStatusOptions.optional(),
display_format: z.array(displayFormatOptions).optional(),
publisher_platform: z.array(publisherPlatformOptions).optional(),
niches: z.array(nicheOptions).optional(),
market_target: z.array(marketTargetOptions).optional(),
languages: z.array(languageOptions).optional(),
cursor: z.string().optional(),
limit: z.number().min(1).max(250).optional(),
});
export const findBrandsSchema = z.object({
query: z.string().min(1, 'Brand name is required'),
limit: z.number().min(1).max(10).optional(),
});
export const findBoardsSchema = z.object({
offset: z.number().min(0).optional(),
limit: z.number().min(1).max(10).optional(),
});
// Trigger Schemas
export const newAdInBoardSchema = z.object({
board_id: z.string().min(1, 'Board ID is required'),
polling_interval: z.number().min(1).max(1440).optional(),
live: liveStatusOptions.optional(),
display_format: z.array(displayFormatOptions).optional(),
publisher_platform: z.array(publisherPlatformOptions).optional(),
niches: z.array(nicheOptions).optional(),
market_target: z.array(marketTargetOptions).optional(),
languages: z.array(languageOptions).optional(),
});
export const newAdInSpyderSchema = z.object({
brand_id: z.string().min(1, 'Brand ID is required'),
polling_interval: z.number().min(1).max(1440).optional(),
live: liveStatusOptions.optional(),
display_format: z.array(displayFormatOptions).optional(),
publisher_platform: z.array(publisherPlatformOptions).optional(),
niches: z.array(nicheOptions).optional(),
market_target: z.array(marketTargetOptions).optional(),
languages: z.array(languageOptions).optional(),
});
export const newSwipefileAdSchema = z.object({
polling_interval: z.number().min(1).max(1440).optional(),
start_date: z.string().optional(),
end_date: z.string().optional(),
live: liveStatusOptions.optional(),
display_format: z.array(displayFormatOptions).optional(),
publisher_platform: z.array(publisherPlatformOptions).optional(),
niches: z.array(nicheOptions).optional(),
market_target: z.array(marketTargetOptions).optional(),
languages: z.array(languageOptions).optional(),
});
@@ -0,0 +1,3 @@
export { newAdInSpyder } from './new-ad-in-spyder';
export { newAdInBoard } from './new-ad-in-board';
export { newSwipefileAd } from './new-swipefile-ad';
@@ -0,0 +1,178 @@
import { createTrigger, TriggerStrategy, Property, AppConnectionValueForAuthProperty } from "@activepieces/pieces-framework";
import { foreplayCoApiCall } from "../common";
import { HttpMethod, Polling, DedupeStrategy, pollingHelper } from "@activepieces/pieces-common";
import { newAdInBoard as newAdInBoardProperties } from "../properties";
import { newAdInBoardSchema } from "../schemas";
import { foreplayCoAuth } from "../..";
const getBoardsDropdown = async (auth: AppConnectionValueForAuthProperty<typeof foreplayCoAuth>) => {
try {
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: '/api/boards',
});
const responseBody = response.body;
if (responseBody.metadata && responseBody.metadata.success === true && responseBody.data) {
return {
options: responseBody.data.map((board: any) => ({
label: board.name || board.title || `Board ${board.id}`,
value: board.id
}))
};
}
return { options: [] };
} catch (error) {
console.error('Error fetching boards for dropdown:', error);
return { options: [] };
}
};
const polling: Polling<AppConnectionValueForAuthProperty<typeof foreplayCoAuth>, Record<string, any>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
const { board_id } = propsValue;
console.log(`[New Ad in Board Polling] Fetching ads for board: ${board_id}, lastFetch: ${new Date(lastFetchEpochMS || 0).toISOString()}`);
const queryParams: Record<string, string> = {
board_id: board_id,
limit: String(250), // Max limit to get more ads
order: 'newest'
};
// Add optional filters if provided
if (propsValue['live'] !== undefined) {
queryParams['live'] = String(propsValue['live'] === 'true');
}
if (propsValue['display_format'] && propsValue['display_format'].length > 0) {
(queryParams as any).display_format = propsValue['display_format'];
}
if (propsValue['publisher_platform'] && propsValue['publisher_platform'].length > 0) {
(queryParams as any).publisher_platform = propsValue['publisher_platform'];
}
if (propsValue['niches'] && propsValue['niches'].length > 0) {
(queryParams as any).niches = propsValue['niches'];
}
if (propsValue['market_target'] && propsValue['market_target'].length > 0) {
(queryParams as any).market_target = propsValue['market_target'];
}
if (propsValue['languages'] && propsValue['languages'].length > 0) {
(queryParams as any).languages = propsValue['languages'];
}
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: '/api/board/ads',
queryParams,
});
const responseBody = response.body;
if (!responseBody.metadata || !responseBody.metadata.success) {
console.log(`[New Ad in Board Polling] API call failed:`, responseBody);
return [];
}
const ads = responseBody.data || [];
console.log(`[New Ad in Board Polling] Found ${ads.length} ads for board ${board_id}`);
return ads.map((ad: any) => ({
epochMilliSeconds: new Date(ad.created_at).getTime(),
data: ad,
}));
}
};
export const newAdInBoard = createTrigger({
name: 'newAdInBoard',
displayName: 'New Ad in Board',
auth: foreplayCoAuth,
description: 'Triggers when a new ad is added to the selected board.',
type: TriggerStrategy.POLLING,
sampleData: {
id: "ad_789",
board_id: "board_456",
brand_id: "brand_456",
title: "New Campaign Ad",
description: "Latest marketing campaign",
live: true,
display_format: "video",
publisher_platform: ["facebook"],
niches: ["fashion"],
market_target: "b2c",
languages: ["en"],
created_at: "2024-01-15T10:30:00Z",
updated_at: "2024-01-15T10:30:00Z"
},
props: newAdInBoardProperties(),
async test(context) {
// Validate props using Zod schema
const validation = newAdInBoardSchema.safeParse(context.propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
store: context.store,
propsValue: context.propsValue,
auth: context.auth,
});
},
async run(context) {
// Validate props using Zod schema
const validation = newAdInBoardSchema.safeParse(context.propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
const result = await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
// Transform the result to match our expected format
return result.map((item: any) => ({
id: item.data.id,
board_id: context.propsValue.board_id,
brand_id: item.data.brand_id,
title: item.data.title || item.data.name,
description: item.data.description,
live: item.data.live,
display_format: item.data.display_format,
publisher_platform: item.data.publisher_platform,
niches: item.data.niches,
market_target: item.data.market_target,
languages: item.data.languages,
created_at: item.data.created_at,
updated_at: item.data.updated_at,
metadata: { success: true, message: 'New ad detected' }
}));
}
});
@@ -0,0 +1,154 @@
import { AppConnectionValueForAuthProperty, createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { foreplayCoApiCall } from '../common';
import {
HttpMethod,
Polling,
DedupeStrategy,
pollingHelper,
} from '@activepieces/pieces-common';
import { newAdInSpyder as newAdInSpyderProperties } from '../properties';
import { newAdInSpyderSchema } from '../schemas';
import { foreplayCoAuth } from '../..';
const polling: Polling<AppConnectionValueForAuthProperty<typeof foreplayCoAuth>, Record<string, any>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue }) => {
const { brand_id } = propsValue;
// Build query parameters with user's filter preferences
const queryParams = new URLSearchParams();
queryParams.append('brand_id', brand_id);
queryParams.append('limit', String(50)); // Reasonable default for polling
queryParams.append('order', 'newest');
// Add optional filters if provided
if (propsValue['live']) {
queryParams.append('live', String(propsValue['live']));
}
if (
propsValue['display_format'] &&
propsValue['display_format'].length > 0
) {
propsValue['display_format'].forEach((format: unknown) => {
queryParams.append('display_format', String(format));
});
}
if (
propsValue['publisher_platform'] &&
propsValue['publisher_platform'].length > 0
) {
propsValue['publisher_platform'].forEach((platform: unknown) => {
queryParams.append('publisher_platform', String(platform));
});
}
if (propsValue['niches'] && propsValue['niches'].length > 0) {
propsValue['niches'].forEach((niche: unknown) => {
queryParams.append('niches', String(niche));
});
}
if (propsValue['market_target'] && propsValue['market_target'].length > 0) {
propsValue['market_target'].forEach((target: unknown) => {
queryParams.append('market_target', String(target));
});
}
if (propsValue['languages'] && propsValue['languages'].length > 0) {
propsValue['languages'].forEach((language: unknown) => {
queryParams.append('languages', String(language));
});
}
const queryString = queryParams.toString();
const fullUrl = `/api/spyder/brand/ads?${queryString}`;
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: fullUrl,
});
const responseBody = response.body;
if (!responseBody.metadata || !responseBody.metadata.success) {
return [];
}
const ads = responseBody.data || [];
return ads.map((ad: any) => ({
epochMilliSeconds: new Date(ad.created_at).getTime(),
data: ad,
}));
},
};
export const newAdInSpyder = createTrigger({
name: 'newAdInSpyder',
displayName: 'New Ad in Spyder',
description: 'Triggers when new ads are added for a brand in Spyder.',
type: TriggerStrategy.POLLING,
sampleData: {
id: 'ad_123456789',
brand_id: 'brand_987654321',
title: 'New Summer Sale Ad',
description: 'A great summer sale ad.',
live: true,
display_format: 'video',
publisher_platform: ['facebook'],
niches: ['fashion'],
market_target: 'b2c',
languages: ['en'],
created_at: '2024-01-15T10:30:00Z',
updated_at: '2024-01-15T10:30:00Z',
},
props: newAdInSpyderProperties(),
auth: foreplayCoAuth,
async test(context) {
// Validate props using Zod schema
const validation = newAdInSpyderSchema.safeParse(context.propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
store: context.store,
propsValue: context.propsValue,
auth: context.auth,
});
},
async run(context) {
// Validate props using Zod schema
const validation = newAdInSpyderSchema.safeParse(context.propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
const result = await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
// Return clean ad data for automation workflows
return result.map((item: any) => item.data);
},
});
@@ -0,0 +1,163 @@
import { createTrigger, TriggerStrategy, Property, AppConnectionValueForAuthProperty } from "@activepieces/pieces-framework";
import { foreplayCoApiCall } from "../common";
import { HttpMethod, Polling, DedupeStrategy, pollingHelper } from "@activepieces/pieces-common";
import { newSwipefileAd as newSwipefileAdProperties } from "../properties";
import { newSwipefileAdSchema } from "../schemas";
import { foreplayCoAuth } from "../..";
const polling: Polling<AppConnectionValueForAuthProperty<typeof foreplayCoAuth>, Record<string, any>> = {
strategy: DedupeStrategy.TIMEBASED,
items: async ({ auth, propsValue, lastFetchEpochMS }) => {
console.log(`[New Swipefile Ad Polling] Fetching swipefile ads, lastFetch: ${new Date(lastFetchEpochMS || 0).toISOString()}`);
const queryParams: Record<string, string> = {
limit: String(250), // Max limit to get more ads
order: 'newest'
};
// Add optional filters if provided
if (propsValue['start_date']) {
queryParams['start_date'] = propsValue['start_date'];
}
if (propsValue['end_date']) {
queryParams['end_date'] = propsValue['end_date'];
}
if (propsValue['live'] !== undefined) {
queryParams['live'] = String(propsValue['live'] === 'true');
}
if (propsValue['display_format'] && propsValue['display_format'].length > 0) {
(queryParams as any).display_format = propsValue['display_format'];
}
if (propsValue['publisher_platform'] && propsValue['publisher_platform'].length > 0) {
(queryParams as any).publisher_platform = propsValue['publisher_platform'];
}
if (propsValue['niches'] && propsValue['niches'].length > 0) {
(queryParams as any).niches = propsValue['niches'];
}
if (propsValue['market_target'] && propsValue['market_target'].length > 0) {
(queryParams as any).market_target = propsValue['market_target'];
}
if (propsValue['languages'] && propsValue['languages'].length > 0) {
(queryParams as any).languages = propsValue['languages'];
}
const response = await foreplayCoApiCall({
apiKey: auth,
method: HttpMethod.GET,
resourceUri: '/api/swipefile/ads',
queryParams,
});
const responseBody = response.body;
if (!responseBody.metadata || !responseBody.metadata.success) {
console.log(`[New Swipefile Ad Polling] API call failed:`, responseBody);
return [];
}
const ads = responseBody.data || [];
console.log(`[New Swipefile Ad Polling] Found ${ads.length} swipefile ads`);
return ads.map((ad: any) => ({
epochMilliSeconds: new Date(ad.created_at).getTime(),
data: ad,
}));
}
};
export const newSwipefileAd = createTrigger({
name: 'newSwipefileAd',
displayName: 'New Swipefile Ad',
description: 'Triggers when a new ad is added to your swipefile collection.',
type: TriggerStrategy.POLLING,
sampleData: {
id: "ad_123456789",
brand_id: "brand_987654321",
brand_name: "Nike",
title: "Just Do It - New Collection",
description: "Discover our latest athletic wear collection",
live: true,
display_format: "video",
publisher_platform: ["facebook"],
niches: ["sports", "fashion"],
market_target: "b2c",
languages: ["en"],
created_at: "2024-01-15T10:30:00Z",
updated_at: "2024-01-15T10:30:00Z",
media_urls: [
"https://example.com/video1.mp4",
"https://example.com/image1.jpg"
],
ad_library_id: "123456789",
ad_library_url: "https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=US&view_all_page_id=123456789"
},
props: newSwipefileAdProperties(),
auth: foreplayCoAuth,
async test(context) {
// Validate props using Zod schema
const validation = newSwipefileAdSchema.safeParse(context.propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
return await pollingHelper.test(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
},
async onEnable(context) {
await pollingHelper.onEnable(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
});
},
async onDisable(context) {
await pollingHelper.onDisable(polling, {
store: context.store,
propsValue: context.propsValue,
auth: context.auth,
});
},
async run(context) {
// Validate props using Zod schema
const validation = newSwipefileAdSchema.safeParse(context.propsValue);
if (!validation.success) {
throw new Error(`Validation failed: ${validation.error.message}`);
}
const result = await pollingHelper.poll(polling, {
auth: context.auth,
store: context.store,
propsValue: context.propsValue,
files: context.files,
});
// Transform the result to match our expected format
return result.map((item: any) => ({
id: item.data.id,
brand_id: item.data.brand_id,
brand_name: item.data.brand_name,
title: item.data.title,
description: item.data.description,
live: item.data.live,
display_format: item.data.display_format,
publisher_platform: item.data.publisher_platform,
niches: item.data.niches,
market_target: item.data.market_target,
languages: item.data.languages,
created_at: item.data.created_at,
updated_at: item.data.updated_at,
media_urls: item.data.media_urls,
ad_library_id: item.data.ad_library_id,
ad_library_url: item.data.ad_library_url,
metadata: { success: true, message: 'New ad detected' }
}));
}
});