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,164 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { heygenAuth } from '../common/auth';
import { heygenApiCall } from '../common/client';
import {
folderDropdown,
brandVoiceDropdown,
templateDropdown,
templateVariables,
} from '../common/props';
import { isNil } from '@activepieces/shared';
export const createVideoFromTemplateAction = createAction({
auth: heygenAuth,
name: 'create-video-from-template',
displayName: 'Create Video from Template',
description: 'Create a video using a selected template.',
props: {
templateId: templateDropdown,
title: Property.ShortText({
displayName: 'Video Title',
required: true,
description: 'Title of the generated video.',
}),
caption: Property.Checkbox({
displayName: 'Enable Captions',
required: false,
defaultValue: false,
}),
includeGif: Property.Checkbox({
displayName: 'Include GIF Preview',
required: false,
defaultValue: false,
}),
enableSharing: Property.Checkbox({
displayName: 'Enable Public Sharing',
required: false,
defaultValue: false,
}),
folderId: folderDropdown,
brandVoiceId: brandVoiceDropdown,
callbackUrl: Property.ShortText({
displayName: 'Callback URL',
required: false,
description: 'Webhook URL to notify when video rendering is complete.',
}),
dimensionWidth: Property.Number({
displayName: 'Video Width',
required: false,
defaultValue: 1280,
}),
dimensionHeight: Property.Number({
displayName: 'Video Height',
required: false,
defaultValue: 720,
}),
variables: templateVariables,
},
async run({ propsValue, auth }) {
const {
templateId,
title,
caption,
includeGif,
enableSharing,
folderId,
brandVoiceId,
callbackUrl,
dimensionWidth,
dimensionHeight,
} = propsValue;
const inputVariables = propsValue.variables ?? {};
const template = await heygenApiCall<{
data: {
variables: { [x: string]: { type: string; name: string; properties: Record<string, any> } };
};
}>({
apiKey: auth as unknown as string,
method: HttpMethod.GET,
resourceUri: `/template/${templateId}`,
apiVersion: 'v2',
});
const templateVariables = template.data.variables;
const formattedVariables: Record<string, any> = {};
for (const [key, value] of Object.entries(inputVariables)) {
if (isNil(value) || value === '') continue;
const variable = templateVariables[key];
if (!variable) continue;
const { type, name, properties } = variable;
const base = { name, type };
switch (type) {
case 'text':
formattedVariables[key] = {
...base,
properties: { content: value },
};
break;
case 'image':
case 'video':
case 'audio':
formattedVariables[key] = {
...base,
properties: { ...properties, url: value },
};
break;
case 'character':
formattedVariables[key] = {
...base,
properties: { ...properties, character_id: value },
};
break;
case 'voice':
formattedVariables[key] = {
...base,
properties: { ...properties, voice_id: value },
};
break;
default:
break;
}
}
const body: Record<string, any> = {
template_id: templateId,
title,
caption: caption === true,
include_gif: includeGif === true,
enable_sharing: enableSharing === true,
};
if (folderId) body['folder_id'] = folderId;
if (brandVoiceId) body['brand_voice_id'] = brandVoiceId;
if (callbackUrl) body['callback_url'] = callbackUrl;
if (dimensionWidth && dimensionHeight) {
body['dimension'] = {
width: dimensionWidth,
height: dimensionHeight,
};
}
if (Object.keys(formattedVariables || {}).length) {
body['variables'] = formattedVariables;
}
const response = await heygenApiCall({
apiKey: auth.secret_text,
method: HttpMethod.POST,
resourceUri: `/template/${templateId}/generate`,
body,
apiVersion: 'v2',
});
return response;
},
});

View File

@@ -0,0 +1,30 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { heygenAuth } from '../common/auth';
import { heygenApiCall } from '../common/client';
export const retrieveTranslatedVideoStatus = createAction({
auth: heygenAuth,
name: 'retrieve-translated-video-status',
displayName: 'Retrieve Translated Video Status',
description: 'Retrieves the status of a translated video.',
props: {
videoId: Property.ShortText({
displayName: 'Video ID',
required: true,
description: 'The ID of the translated video to check the status for.',
}),
},
async run({ propsValue, auth }) {
const { videoId } = propsValue;
const response = await heygenApiCall({
apiKey: auth.secret_text,
method: HttpMethod.GET,
resourceUri: `/video_translate/${videoId}`,
apiVersion: 'v2',
});
return response;
},
});

View File

@@ -0,0 +1,31 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { heygenApiCall } from '../common/client';
import { heygenAuth } from '../common/auth';
export const retrieveVideoStatusAction = createAction({
auth: heygenAuth,
name: 'retrieve_video_status',
displayName: 'Retrieve Video Status',
description: 'Retrieve the status and details of a video using its ID.',
props: {
videoId: Property.ShortText({
displayName: 'Video ID',
description: 'The ID of the video to retrieve the status for.',
required: true,
}),
},
async run({ propsValue, auth }) {
const { videoId } = propsValue;
const response = await heygenApiCall({
apiKey: auth.secret_text,
method: HttpMethod.GET,
resourceUri: `/video_status.get`,
query: { video_id: videoId },
apiVersion: 'v1',
});
return response;
},
});

View File

@@ -0,0 +1,31 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { heygenApiCall } from '../common/client';
import { heygenAuth } from '../common/auth';
export const retrieveSharableVideoUrlAction = createAction({
auth: heygenAuth,
name: 'retrieve_sharable_video_url',
displayName: 'Retrieve Sharable Video URL',
description: 'Generates a public URL for a video, allowing it to be shared and accessed publicly.',
props: {
videoId: Property.ShortText({
displayName: 'Video ID',
description: 'The ID of the video to generate a shareable URL for.',
required: true,
}),
},
async run({ propsValue, auth }) {
const { videoId } = propsValue;
const response = await heygenApiCall({
apiKey: auth.secret_text,
method: HttpMethod.POST,
resourceUri: '/video/share',
body: { video_id: videoId },
apiVersion: 'v1',
});
return response;
},
});

View File

@@ -0,0 +1,83 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { heygenApiCall } from '../common/client';
import { heygenAuth } from '../common/auth';
import { brandVoiceDropdown, supportedLanguagesDropdown } from '../common/props';
export const translateVideoAction = createAction({
auth: heygenAuth,
name: 'translate_video',
displayName: 'Translate Video',
description: 'Translate a video into 175+ languages with natural voice and lip-sync.',
props: {
videoUrl: Property.ShortText({
displayName: 'Video URL',
required: true,
description:
'URL of the video file to be translated. Supports direct URLs, Google Drive, and YouTube.',
}),
title: Property.ShortText({
displayName: 'Title',
required: false,
description: 'Optional title of the translated video.',
}),
outputLanguage: supportedLanguagesDropdown,
translateAudioOnly: Property.Checkbox({
displayName: 'Translate Audio Only',
required: false,
defaultValue: false,
description: 'Only translate the audio without modifying faces.',
}),
speakerNum: Property.Number({
displayName: 'Number of Speakers',
required: false,
description: 'Number of speakers in the video (if applicable).',
}),
brandVoiceId: brandVoiceDropdown,
callbackId: Property.ShortText({
displayName: 'Callback ID',
required: false,
description: 'Custom ID returned in webhook callback.',
}),
callbackUrl: Property.ShortText({
displayName: 'Callback URL',
required: false,
description: 'URL to notify when translation is complete.',
}),
},
async run({ propsValue, auth }) {
const {
videoUrl,
title,
outputLanguage,
translateAudioOnly,
speakerNum,
callbackId,
brandVoiceId,
callbackUrl,
} = propsValue;
const body: Record<string, unknown> = {
video_url: videoUrl,
output_language: outputLanguage,
};
if (title) body['title'] = title;
if (translateAudioOnly) body['translate_audio_only'] = translateAudioOnly;
if (speakerNum) body['speaker_num'] = speakerNum;
if (callbackId) body['callback_id'] = callbackId;
if (brandVoiceId) body['brand_voice_id'] = brandVoiceId;
if (callbackUrl) body['callback_url'] = callbackUrl;
const response = await heygenApiCall({
apiKey: auth.secret_text,
method: HttpMethod.POST,
resourceUri: '/video_translate',
body,
apiVersion: 'v2',
});
return response;
},
});

View File

@@ -0,0 +1,55 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { HttpMethod, httpClient } from '@activepieces/pieces-common';
import { heygenAuth } from '../common/auth';
export const uploadAssetAction = createAction({
auth: heygenAuth,
name: 'upload_asset',
displayName: 'Upload an Asset',
description:
'Upload media files (images, videos, or audio) to HeyGen. Supports JPEG, PNG, MP4, WEBM, and MPEG files.',
props: {
file: Property.File({
displayName: 'File',
description: 'The file to upload (JPEG, PNG, MP4, WEBM, or MPEG).',
required: true,
}),
},
async run(context) {
const { file } = context.propsValue;
const getContentType = (filename: string): string => {
const extension = filename.toLowerCase().split('.').pop();
switch (extension) {
case 'jpg':
case 'jpeg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'mp4':
return 'video/mp4';
case 'webm':
return 'video/webm';
case 'mpeg':
case 'mpg':
return 'audio/mpeg';
default:
throw new Error(`Unsupported file type: ${extension}`);
}
};
const contentType = getContentType(file.filename);
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: 'https://upload.heygen.com/v1/asset',
headers: {
'x-api-key': context.auth.secret_text,
'Content-Type': contentType,
},
body: file.data,
});
return response.body;
},
});

View File

@@ -0,0 +1,25 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { PieceAuth } from '@activepieces/pieces-framework';
import { heygenApiCall } from './client';
export const heygenAuth = PieceAuth.SecretText({
displayName: 'API Key',
description: `You can obtain your API key by navigating to your Space Settings in HeyGen App.`,
required: true,
validate: async ({ auth }) => {
try {
await heygenApiCall({
apiKey: auth as string,
method: HttpMethod.GET,
resourceUri: '/user/me',
apiVersion: 'v1',
});
return { valid: true };
} catch {
return {
valid: false,
error: 'Invalid API Key.',
};
}
},
});

View File

@@ -0,0 +1,53 @@
import {
httpClient,
HttpMessageBody,
HttpMethod,
HttpRequest,
QueryParams,
} from '@activepieces/pieces-common';
export type HeygenApiCallParams = {
apiKey: string;
method: HttpMethod;
resourceUri: string;
query?: Record<string, string | number | string[] | undefined>;
body?: unknown;
apiVersion: 'v1' | 'v2';
};
export const BASE_URL_V1 = 'https://api.heygen.com/v1';
export const BASE_URL_V2 = 'https://api.heygen.com/v2';
export async function heygenApiCall<T extends HttpMessageBody>({
apiKey,
method,
resourceUri,
query,
body,
apiVersion,
}: HeygenApiCallParams): Promise<T> {
const qs: QueryParams = {};
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined) {
qs[key] = String(value);
}
}
}
const url = (apiVersion === 'v1' ? BASE_URL_V1 : BASE_URL_V2) + resourceUri;
const request: HttpRequest = {
method,
url,
headers: {
'X-Api-Key': apiKey,
},
queryParams: qs,
body,
};
const response = await httpClient.sendRequest<T>(request);
return response.body;
}

View File

@@ -0,0 +1,265 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { DynamicPropsValue, Property } from '@activepieces/pieces-framework';
import { heygenApiCall } from './client';
import { isNil } from '@activepieces/shared';
import { heygenAuth } from './auth';
export const folderDropdown = Property.Dropdown({
displayName: 'Folder',
description: 'Select the folder to store the video.',
required: false,
refreshers: [],
auth: heygenAuth,
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first.',
};
}
const response = await heygenApiCall<{
data: { folders: { id: string; name: string }[] };
}>({
apiKey: auth.secret_text,
method: HttpMethod.GET,
resourceUri: '/folders',
apiVersion: 'v1',
});
return {
disabled: false,
options: response.data.folders.map((folder) => ({
label: folder.name,
value: folder.id,
})),
};
},
});
export const brandVoiceDropdown = Property.Dropdown({
auth: heygenAuth,
displayName: 'Brand Voice',
description: 'Select the Brand Voice to apply to the video.',
required: false,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first.',
};
}
const response = await heygenApiCall<{
data: { list: { id: string; name: string }[] };
}>({
apiKey: auth.secret_text,
method: HttpMethod.GET,
resourceUri: '/brand_voice/list',
apiVersion: 'v1',
});
return {
disabled: false,
options: response.data.list.map((voice) => ({
label: voice.name,
value: voice.id,
})),
};
},
});
export const templateDropdown = Property.Dropdown({
auth: heygenAuth,
displayName: 'Template',
description: 'Select the template to generate the video.',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first.',
};
}
const response = await heygenApiCall<{
data: { templates: { template_id: string; name: string; aspect_ratio: string }[] };
}>({
apiKey: auth.secret_text,
method: HttpMethod.GET,
resourceUri: '/templates',
apiVersion: 'v2',
});
return {
disabled: false,
options: response.data.templates.map((template) => ({
label: template.name,
value: template.template_id,
})),
};
},
});
export const supportedLanguagesDropdown = Property.Dropdown({
auth: heygenAuth,
displayName: 'Supported Language',
description: 'Select the language for video translation.',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your account first.',
};
}
const response = await heygenApiCall<{ data: { languages: string[] } }>({
apiKey: auth.secret_text,
method: HttpMethod.GET,
resourceUri: '/video_translate/target_languages',
apiVersion: 'v2',
});
return {
disabled: false,
options: response.data.languages.map((lang) => ({
label: lang,
value: lang,
})),
};
},
});
export const templateVariables = Property.DynamicProperties({
auth: heygenAuth,
displayName: 'Template Varriables',
refreshers: ['templateId'],
required: false,
props: async ({ auth, templateId }) => {
if (!auth || !templateId) return {};
const fields: DynamicPropsValue = {};
try {
const response = await heygenApiCall<{
data: { variables: { [x: string]: { type: string; name: string } } };
}>({
apiKey: auth.secret_text,
method: HttpMethod.GET,
resourceUri: `/template/${templateId}`,
apiVersion: 'v2',
});
const variables = response.data.variables;
if (!isNil(variables)) return {};
for (const [key, value] of Object.entries(response.data.variables)) {
const fieldKey = key;
const fieldType = value.type;
switch (fieldType) {
case 'text':
fields[fieldKey] = Property.ShortText({
displayName: fieldKey,
required: false,
description: 'Provide text value.',
});
break;
case 'image':
fields[fieldKey] = Property.ShortText({
displayName: fieldKey,
required: false,
description: 'Provide image URL.',
});
break;
case 'video':
fields[fieldKey] = Property.ShortText({
displayName: fieldKey,
required: false,
description: 'Provide video URL.',
});
break;
case 'audio':
fields[fieldKey] = Property.ShortText({
displayName: fieldKey,
required: false,
description: 'Provide audio URL.',
});
break;
case 'character': {
const characters = await heygenApiCall<{
avatars: { avatar_name: string; avatar_id: string }[];
talking_photos: {
talking_photo_id: string;
talking_photo_name: string;
}[];
}>({
apiKey: auth as unknown as string,
method: HttpMethod.GET,
resourceUri: `/avatars`,
apiVersion: 'v2',
});
const options = [
...characters.avatars.map((avatar) => ({
label: avatar.avatar_name,
value: avatar.avatar_id,
})),
...characters.talking_photos.map((photo) => ({
label: photo.talking_photo_name,
value: photo.talking_photo_id,
})),
];
fields[fieldKey] = Property.StaticDropdown({
displayName: fieldKey,
required: false,
description: 'Select one of avatar or talking photo.',
options: { disabled: false, options },
});
break;
}
case 'voice': {
const voices = await heygenApiCall<{
voices: { name: string; voice_id: string }[];
}>({
apiKey: auth as unknown as string,
method: HttpMethod.GET,
resourceUri: `/voices`,
apiVersion: 'v2',
});
fields[fieldKey] = Property.StaticDropdown({
displayName: fieldKey,
required: false,
options: {
disabled: false,
options: voices.voices.map((voice) => ({
label: voice.name,
value: voice.voice_id,
})),
},
});
break;
}
default:
break;
}
}
return fields;
} catch (error) {
console.error(`${error instanceof Error ? error.message : 'Unknown error'}`);
return {};
}
},
});

View File

@@ -0,0 +1,67 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { heygenApiCall } from '../common/client';
import { heygenAuth } from '../common/auth';
const TRIGGER_KEY = 'video_generation_completed_trigger';
export const videoGenerationCompletedTrigger = createTrigger({
auth: heygenAuth,
name: 'video_generation_completed',
displayName: 'New Avatar Video Event (Success)',
description: 'Triggers when a video is generated successfully.',
type: TriggerStrategy.WEBHOOK,
props: {},
sampleData: {
event_type: 'avatar_video.success',
event_data: {
video_id: '123',
url: 'https://www.example.com',
gif_download_url: '<gif_url>',
folder_id: '123',
callback_id: '123',
},
},
async onEnable(context) {
const webhook = (await heygenApiCall({
apiKey: context.auth.secret_text,
method: HttpMethod.POST,
resourceUri: '/webhook/endpoint.add',
apiVersion: 'v1',
body: {
url: context.webhookUrl,
events: ['avatar_video.success'],
},
})) as { data: { endpoint_id: string } };
await context.store.put<string>(TRIGGER_KEY, webhook.data.endpoint_id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>(TRIGGER_KEY);
if (webhookId) {
await heygenApiCall({
apiKey: context.auth.secret_text,
method: HttpMethod.DELETE,
resourceUri: '/webhook/endpoint.delete',
apiVersion: 'v1',
query: {
endpoint_id: webhookId,
},
});
}
},
async run(context) {
const payload = context.payload.body as {
event_type: string;
event_data: Record<string, any>;
};
if (payload.event_type !== 'avatar_video.success') return [];
return [payload.event_data];
},
});

View File

@@ -0,0 +1,66 @@
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { heygenApiCall } from '../common/client';
import { heygenAuth } from '../common/auth';
const TRIGGER_KEY = 'video_generation_failed_trigger';
export const videoGenerationFailedTrigger = createTrigger({
auth: heygenAuth,
name: 'video_generation_failed',
displayName: 'New Avatar Video Event (Fail)',
description: 'Triggers when a video generation process fails.',
type: TriggerStrategy.WEBHOOK,
props: {},
sampleData: {
event_type: 'avatar_video.fail',
event_data: {
video_id: 'abc',
msg: 'Failed',
callback_id: '123',
},
},
async onEnable(context) {
const webhook = (await heygenApiCall({
apiKey: context.auth.secret_text,
method: HttpMethod.POST,
resourceUri: '/webhook/endpoint.add',
apiVersion: 'v1',
body: {
url: context.webhookUrl,
events: ['avatar_video.fail'],
},
})) as { data: { endpoint_id: string } };
await context.store.put<string>(TRIGGER_KEY, webhook.data.endpoint_id);
},
async onDisable(context) {
const webhookId = await context.store.get<string>(TRIGGER_KEY);
if (webhookId) {
await heygenApiCall({
apiKey: context.auth.secret_text,
method: HttpMethod.DELETE,
resourceUri: '/webhook/endpoint.delete',
apiVersion: 'v1',
query: {
endpoint_id: webhookId,
},
});
}
},
async run(context) {
const payload = context.payload.body as {
event_type: string;
event_data: Record<string, any>;
};
if (payload.event_type !== 'avatar_video.fail') return [];
return [payload];
},
});