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,68 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { runwayAuth } from '../common';
import RunwayML from '@runwayml/sdk';
import { z } from 'zod';
export const cancelOrDeleteTask = createAction({
auth: runwayAuth,
name: 'cancel_or_delete_task',
displayName: 'Cancel or Delete Task',
description: 'Cancel or delete a task. Running/pending tasks are cancelled, completed tasks are deleted.',
props: {
taskId: Property.ShortText({
displayName: 'Task ID',
description: 'The ID of the task to cancel or delete (UUID format)',
required: true
}),
},
async run({ auth, propsValue }) {
await propsValidation.validateZod(propsValue, {
taskId: z.string().uuid('Task ID must be a valid UUID format'),
});
const apiKey = auth.secret_text;
const client = new RunwayML({ apiKey });
// First get task details to determine what action will be taken
let task;
try {
task = await client.tasks.retrieve(propsValue.taskId);
} catch (error: any) {
if (error.status === 404) {
throw new Error(`Task not found: ${propsValue.taskId}. The task may have already been deleted.`);
}
throw new Error(`Failed to retrieve task: ${error.message || 'Unknown error'}`);
}
const wasRunning = ['RUNNING', 'PENDING', 'THROTTLED'].includes(task.status);
const action = wasRunning ? 'cancelled' : 'deleted';
try {
await client.tasks.delete(propsValue.taskId);
return {
success: true,
taskId: task.id,
action: action,
message: `Task ${action} successfully`,
previousStatus: task.status,
wasRunning: wasRunning
};
} catch (error: any) {
if (error.status === 404) {
return {
success: true,
taskId: propsValue.taskId,
action: 'already_deleted',
message: 'Task was already deleted',
previousStatus: 'UNKNOWN',
wasRunning: false
};
}
throw new Error(`Failed to ${action.slice(0, -1)} task: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,141 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { runwayAuth } from '../common';
import RunwayML from '@runwayml/sdk';
import { z } from 'zod';
export const generateImageFromText = createAction({
auth: runwayAuth,
name: 'generate_image_from_text',
displayName: 'Generate Image From Text',
description: 'Generates an image using a text prompt via Runway\'s AI models',
props: {
model: Property.StaticDropdown({
displayName: 'Model',
description: 'AI model to use for image generation',
required: true,
options: {
options: [
{ label: 'Gen4 Image (High Quality)', value: 'gen4_image' },
{ label: 'Gen4 Image Turbo (Fast)', value: 'gen4_image_turbo' },
],
},
}),
promptText: Property.LongText({
displayName: 'Prompt',
description: 'Describe what you want to see in the image (max 1000 characters)',
required: true,
}),
ratio: Property.StaticDropdown({
displayName: 'Image Ratio',
description: 'Resolution of the output image',
required: true,
options: {
options: [
{ label: 'HD Landscape (1920x1080)', value: '1920:1080' },
{ label: 'HD Portrait (1080x1920)', value: '1080:1920' },
{ label: 'Square (1024x1024)', value: '1024:1024' },
{ label: 'Wide (1360x768)', value: '1360:768' },
{ label: 'Square HD (1080x1080)', value: '1080:1080' },
{ label: 'Standard Wide (1168x880)', value: '1168:880' },
{ label: 'Standard Landscape (1440x1080)', value: '1440:1080' },
{ label: 'Standard Portrait (1080x1440)', value: '1080:1440' },
{ label: 'Ultra Wide (1808x768)', value: '1808:768' },
{ label: 'Cinema Wide (2112x912)', value: '2112:912' },
{ label: 'HD 720p (1280x720)', value: '1280:720' },
{ label: 'HD 720p Portrait (720x1280)', value: '720:1280' },
{ label: 'Square 720p (720x720)', value: '720:720' },
{ label: 'Standard (960x720)', value: '960:720' },
{ label: 'Standard Portrait (720x960)', value: '720:960' },
{ label: 'Wide 720p (1680x720)', value: '1680:720' },
],
},
}),
seed: Property.Number({
displayName: 'Seed (Optional)',
description: 'Random seed for reproducible results (0-4294967295)',
required: false,
}),
referenceImages: Property.Array({
displayName: 'Reference Images (Optional)',
description: 'Up to 3 reference images (required for gen4_image_turbo)',
required: false,
properties: {
uri: Property.ShortText({
displayName: 'Image URL',
description: 'HTTPS URL or data URI of the reference image',
required: true
}),
tag: Property.ShortText({
displayName: 'Tag',
description: 'Name to reference this image in your prompt using @tag (3-16 characters, alphanumeric + underscore)',
required: false
}),
},
}),
publicFigureThreshold: Property.StaticDropdown({
displayName: 'Public Figure Detection',
description: 'How strict content moderation should be for recognizable public figures',
required: false,
defaultValue: 'auto',
options: {
options: [
{ label: 'Auto (Recommended)', value: 'auto' },
{ label: 'Low (Less Strict)', value: 'low' },
],
},
}),
},
async run({ auth, propsValue }) {
// Zod validation
await propsValidation.validateZod(propsValue, {
promptText: z.string().min(1, 'Prompt text cannot be empty').max(1000, 'Prompt text must be 1000 characters or fewer'),
seed: z.number().min(0, 'Seed must be at least 0').max(4294967295, 'Seed must be at most 4294967295').optional(),
referenceImages: z.array(z.object({
uri: z.string().url('Reference image URI must be a valid URL'),
tag: z.string().min(3, 'Tag must be at least 3 characters').max(16, 'Tag must be at most 16 characters').regex(/^[a-zA-Z][a-zA-Z0-9_]*$/, 'Tag must start with a letter and contain only letters, numbers, and underscores').optional()
})).max(3, 'Maximum of 3 reference images allowed').optional(),
});
// Special validation for gen4_image_turbo
if (propsValue.model === 'gen4_image_turbo' && (!propsValue.referenceImages || propsValue.referenceImages.length === 0)) {
throw new Error('gen4_image_turbo requires at least one reference image');
}
const apiKey = auth.secret_text;
const client = new RunwayML({ apiKey });
const requestBody: any = {
model: propsValue.model,
promptText: propsValue.promptText,
ratio: propsValue.ratio,
};
if (propsValue.seed !== undefined) {
requestBody.seed = propsValue.seed;
}
if (propsValue.referenceImages && propsValue.referenceImages.length > 0) {
requestBody.referenceImages = propsValue.referenceImages;
}
if (propsValue.publicFigureThreshold) {
requestBody.contentModeration = {
publicFigureThreshold: propsValue.publicFigureThreshold
};
}
try {
const task = await client.textToImage.create(requestBody);
return {
success: true,
taskId: task.id,
};
} catch (error: any) {
throw new Error(`Failed to generate image: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,306 @@
import { createAction, Property, DynamicPropsValue } from '@activepieces/pieces-framework';
import { propsValidation } from '@activepieces/pieces-common';
import { runwayAuth } from '../common';
import RunwayML from '@runwayml/sdk';
import { z } from 'zod';
export const generateVideoFromImage = createAction({
auth: runwayAuth,
name: 'generate_video_from_image',
displayName: 'Generate Video From Image',
description: 'Generates a video based on image(s) and text prompt using Runway\'s AI models',
props: {
model: Property.StaticDropdown({
displayName: 'Model',
description: 'AI model to use for video generation',
required: true,
options: {
options: [
{ label: 'Gen4 Turbo (Fast, High Quality)', value: 'gen4_turbo' },
{ label: 'Gen3a Turbo (Balanced)', value: 'gen3a_turbo' },
{ label: 'Veo3 (Google\'s Latest)', value: 'veo3' },
],
},
}),
promptImageFile: Property.File({
displayName: 'Prompt Image File',
description: 'Upload an image file to use as the video\'s starting frame',
required: false
}),
promptImageUrl: Property.ShortText({
displayName: 'Prompt Image URL',
description: 'HTTPS URL of an image to use as the video\'s starting frame',
required: false
}),
imagePosition: Property.StaticDropdown({
displayName: 'Image Position',
description: 'Position of the image in the video (last frame only supported by gen3a_turbo)',
required: false,
defaultValue: 'first',
options: {
options: [
{ label: 'First Frame', value: 'first' },
{ label: 'Last Frame (gen3a_turbo only)', value: 'last' },
],
},
}),
promptText: Property.LongText({
displayName: 'Prompt Text (Optional)',
description: 'Describe what should happen in the video (max 1000 characters)',
required: false
}),
ratio: Property.DynamicProperties({
auth: runwayAuth,
displayName: 'Video Resolution',
description: 'Available resolutions depend on the selected model',
required: true,
refreshers: ['model'],
props: async ({ model }) => {
const ratioOptions: DynamicPropsValue = {};
if ((model as unknown as string) === 'gen4_turbo') {
ratioOptions['ratio'] = Property.StaticDropdown({
displayName: 'Resolution',
required: true,
options: {
options: [
{ label: 'HD Landscape (1280x720)', value: '1280:720' },
{ label: 'HD Portrait (720x1280)', value: '720:1280' },
{ label: 'Wide (1104x832)', value: '1104:832' },
{ label: 'Tall (832x1104)', value: '832:1104' },
{ label: 'Square (960x960)', value: '960:960' },
{ label: 'Ultra Wide (1584x672)', value: '1584:672' },
],
},
});
} else if ((model as unknown as string) === 'gen3a_turbo') {
ratioOptions['ratio'] = Property.StaticDropdown({
displayName: 'Resolution',
required: true,
options: {
options: [
{ label: 'Landscape (1280x768)', value: '1280:768' },
{ label: 'Portrait (768x1280)', value: '768:1280' },
],
},
});
} else if ((model as unknown as string) === 'veo3') {
ratioOptions['ratio'] = Property.StaticDropdown({
displayName: 'Resolution',
required: true,
options: {
options: [
{ label: 'HD Landscape (1280x720)', value: '1280:720' },
{ label: 'HD Portrait (720x1280)', value: '720:1280' },
],
},
});
} else {
ratioOptions['ratio'] = Property.StaticDropdown({
displayName: 'Resolution',
required: true,
options: {
options: [
{ label: 'Select a model first', value: '' },
],
},
});
}
return ratioOptions;
},
}),
duration: Property.DynamicProperties({
auth: runwayAuth,
displayName: 'Video Duration',
description: 'Available durations depend on the selected model',
required: true,
refreshers: ['model'],
props: async ({ model }) => {
const durationOptions: DynamicPropsValue = {};
if ((model as unknown as string) === 'veo3') {
durationOptions['duration'] = Property.StaticDropdown({
displayName: 'Duration (seconds)',
required: true,
defaultValue: 8,
options: {
options: [
{ label: '8 seconds (required for veo3)', value: 8 },
],
},
});
} else if ((model as unknown as string) === 'gen4_turbo' || (model as unknown as string) === 'gen3a_turbo') {
durationOptions['duration'] = Property.StaticDropdown({
displayName: 'Duration (seconds)',
required: true,
options: {
options: [
{ label: '5 seconds', value: 5 },
{ label: '10 seconds', value: 10 },
],
},
});
} else {
durationOptions['duration'] = Property.StaticDropdown({
displayName: 'Duration (seconds)',
required: true,
options: {
options: [
{ label: 'Select a model first', value: '' },
],
},
});
}
return durationOptions;
},
}),
seed: Property.Number({
displayName: 'Seed (Optional)',
description: 'Random seed for reproducible results (0-4294967295)',
required: false
}),
contentModeration: Property.DynamicProperties({
auth: runwayAuth,
displayName: 'Content Moderation',
description: 'Content moderation settings (not available for veo3)',
required: false,
refreshers: ['model'],
props: async ({ model }) => {
const moderationOptions: DynamicPropsValue = {};
if ((model as unknown as string) === 'veo3') {
moderationOptions['info'] = Property.MarkDown({
value: '**Note:** Content moderation is not supported by the veo3 model.',
});
} else {
moderationOptions['publicFigureThreshold'] = Property.StaticDropdown({
displayName: 'Public Figure Detection',
description: 'How strict content moderation should be for recognizable public figures',
required: false,
defaultValue: 'auto',
options: {
options: [
{ label: 'Auto (Recommended)', value: 'auto' },
{ label: 'Low (Less Strict)', value: 'low' },
],
},
});
}
return moderationOptions;
},
}),
},
async run({ auth, propsValue, files }) {
// Zod validation
await propsValidation.validateZod(propsValue, {
promptText: z.string().max(1000, 'Prompt text must be 1000 characters or fewer').optional(),
seed: z.number().min(0, 'Seed must be at least 0').max(4294967295, 'Seed must be at most 4294967295').optional(),
promptImageUrl: z.string().url('Prompt image URL must be a valid HTTPS URL').optional(),
});
// Input validation
const hasFile = !!propsValue.promptImageFile;
const hasUrl = !!propsValue.promptImageUrl;
if ((hasFile && hasUrl) || (!hasFile && !hasUrl)) {
throw new Error('You must provide either a Prompt Image File or a Prompt Image URL, but not both.');
}
// Model-specific validations
const model = propsValue.model;
const duration = (propsValue.duration as any)?.['duration'] || propsValue.duration;
const ratio = (propsValue.ratio as any)?.['ratio'] || propsValue.ratio;
const imagePosition = propsValue.imagePosition || 'first';
// Validate duration per model
if (model === 'veo3' && duration !== 8) {
throw new Error('veo3 model requires a duration of exactly 8 seconds');
}
if ((model === 'gen4_turbo' || model === 'gen3a_turbo') && duration !== 5 && duration !== 10) {
throw new Error(`${model} model requires a duration of either 5 or 10 seconds`);
}
// Validate ratio per model
const validRatios = {
gen4_turbo: ['1280:720', '720:1280', '1104:832', '832:1104', '960:960', '1584:672'],
gen3a_turbo: ['1280:768', '768:1280'],
veo3: ['1280:720', '720:1280']
};
if (!validRatios[model as keyof typeof validRatios]?.includes(ratio)) {
throw new Error(`Invalid resolution ${ratio} for model ${model}`);
}
// Validate image position per model
if (imagePosition === 'last' && model !== 'gen3a_turbo') {
throw new Error('Last frame positioning is only supported by gen3a_turbo model');
}
// Prepare image URL
let imageUrl: string;
if (hasFile) {
const f = propsValue.promptImageFile as any;
const ext = f?.extension || 'jpeg';
imageUrl = `data:image/${ext};base64,${f?.base64}`;
} else {
imageUrl = propsValue.promptImageUrl as string;
}
const apiKey = auth.secret_text;
const client = new RunwayML({ apiKey });
// Build request body according to SDK specification
const requestBody: any = {
model: model,
ratio: ratio,
};
// Handle different image input formats based on position
if (imagePosition === 'first' || model !== 'gen3a_turbo') {
// Simple string format for first frame or non-gen3a_turbo models
requestBody.promptImage = imageUrl;
} else {
// Array format for last frame positioning (gen3a_turbo only)
requestBody.promptImage = [{
uri: imageUrl,
position: imagePosition
}];
}
// Add optional parameters
if (duration) {
requestBody.duration = duration;
}
if (propsValue.promptText) {
requestBody.promptText = propsValue.promptText;
}
if (propsValue.seed !== undefined) {
requestBody.seed = propsValue.seed;
}
// Add content moderation only for supported models
if (model !== 'veo3' && (propsValue.contentModeration as any)?.['publicFigureThreshold']) {
requestBody.contentModeration = {
publicFigureThreshold: (propsValue.contentModeration as any)['publicFigureThreshold']
};
}
try {
const task = await client.imageToVideo.create(requestBody);
return {
success: true,
taskId: task.id,
};
} catch (error: any) {
throw new Error(`Failed to generate video: ${error.message || 'Unknown error'}`);
}
},
});

View File

@@ -0,0 +1,166 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { AuthenticationType, httpClient, HttpMethod, propsValidation } from '@activepieces/pieces-common';
import { runwayAuth } from '../common';
import RunwayML from '@runwayml/sdk';
import { z } from 'zod';
// Helper function to get file extension from URL or Content-Type
const getFileExtensionFromUrl = (url: string, contentType?: string): string => {
// Try to get extension from URL first
const urlExt = url.split('.').pop()?.split('?')[0]?.toLowerCase();
if (urlExt && ['mp4', 'mov', 'avi', 'gif', 'webm', 'jpg', 'jpeg', 'png', 'webp'].includes(urlExt)) {
return urlExt;
}
// Fallback to content type
if (contentType) {
if (contentType.includes('video/mp4')) return 'mp4';
if (contentType.includes('video/quicktime')) return 'mov';
if (contentType.includes('video/webm')) return 'webm';
if (contentType.includes('image/gif')) return 'gif';
if (contentType.includes('image/jpeg')) return 'jpg';
if (contentType.includes('image/png')) return 'png';
if (contentType.includes('image/webp')) return 'webp';
}
// Default fallback
return 'mp4';
};
const getStatusDescription = (status: string): string => {
switch (status) {
case 'PENDING': return 'Task is queued and waiting to start';
case 'THROTTLED': return 'Task is waiting for other jobs to complete';
case 'RUNNING': return 'Task is currently being processed';
case 'SUCCEEDED': return 'Task completed successfully';
case 'FAILED': return 'Task failed to complete';
case 'CANCELLED': return 'Task was cancelled or aborted';
default: return 'Unknown status';
}
};
export const getTaskDetails = createAction({
auth: runwayAuth,
name: 'get_task_details',
displayName: 'Get Task Details',
description: 'Retrieve details of an existing Runway task by its ID',
props: {
taskId: Property.ShortText({
displayName: 'Task ID',
description: 'The unique ID of the task to retrieve (UUID format)',
required: true
}),
downloadOutput: Property.Checkbox({
displayName: 'Download Output Files',
description: 'Download and return the generated files as attachments (if task succeeded)',
required: false,
defaultValue: false
}),
},
async run({ auth, propsValue, files }) {
// Zod validation
await propsValidation.validateZod(propsValue, {
taskId: z.string().uuid('Task ID must be a valid UUID format'),
});
const apiKey = auth.secret_text;
const client = new RunwayML({ apiKey });
let task;
try {
task = await client.tasks.retrieve(propsValue.taskId);
} catch (error: any) {
if (error.status === 404) {
throw new Error(`Task not found: ${propsValue.taskId}. Please verify the task ID is correct.`);
}
throw new Error(`Failed to retrieve task: ${error.message || 'Unknown error'}`);
}
const outputs = task.output || [];
const artifacts: Array<any> = [];
// Download output files if requested and available
if (propsValue.downloadOutput && outputs.length > 0) {
if (task.status !== 'SUCCEEDED') {
console.warn(`Task status is ${task.status}, but download was requested. Only succeeded tasks have downloadable outputs.`);
} else {
for (const [index, url] of outputs.entries()) {
try {
// First, make a HEAD request to get content type
const headResponse = await httpClient.sendRequest({
method: HttpMethod.HEAD,
url,
authentication: { type: AuthenticationType.BEARER_TOKEN, token: apiKey },
timeout: 30000,
});
const contentType = headResponse.headers?.['content-type'] as string;
const extension = getFileExtensionFromUrl(url, contentType);
// Now download the actual file
const response = await httpClient.sendRequest<ArrayBuffer>({
method: HttpMethod.GET,
url,
responseType: 'arraybuffer',
authentication: { type: AuthenticationType.BEARER_TOKEN, token: apiKey },
timeout: 300000, // 5 minutes for large video files
});
const file = await files.write({
fileName: `runway-output-${task.id}-${index + 1}.${extension}`,
data: Buffer.from(response.body as any),
});
artifacts.push({
fileName: `runway-output-${task.id}-${index + 1}.${extension}`,
fileUrl: file,
contentType: contentType,
originalUrl: url,
index: index + 1
});
} catch (downloadError: any) {
console.error(`Failed to download output ${index + 1}:`, downloadError.message);
// Continue with other files even if one fails
}
}
}
}
// Calculate completion percentage for display
let completionPercentage = 0;
if (task.status === 'SUCCEEDED') {
completionPercentage = 100;
} else if (task.status === 'RUNNING' && typeof task.progress === 'number') {
completionPercentage = Math.round(task.progress * 100);
}
return {
success: true,
taskId: task.id,
status: task.status,
statusDescription: getStatusDescription(task.status),
createdAt: task.createdAt,
completionPercentage,
progress: task.progress,
outputUrls: outputs,
downloadedFiles: artifacts,
hasOutputs: outputs.length > 0,
isComplete: task.status === 'SUCCEEDED',
isFailed: task.status === 'FAILED',
isRunning: task.status === 'RUNNING',
failureCode: task.failureCode,
failureMessage: task.failure,
// Summary for easy consumption
summary: {
id: task.id,
status: task.status,
created: task.createdAt,
progress: completionPercentage,
outputCount: outputs.length,
downloadedCount: artifacts.length
}
};
},
});

View File

@@ -0,0 +1,9 @@
import { PieceAuth } from '@activepieces/pieces-framework';
export const runwayAuth = PieceAuth.SecretText({
displayName: 'API Key',
description: 'Your Runway API key. Get it from your Runway account settings.',
required: true,
});

View File

@@ -0,0 +1,12 @@
export * from './auth';
import { Property } from '@activepieces/pieces-framework';
export const runwayModelProperty = Property.ShortText({ displayName: 'Model', required: true });
export const runwayTaskIdProperty = Property.ShortText({
displayName: 'Task ID',
description: 'The UUID of the task to retrieve (copy from the task creation response)',
required: true
});