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,46 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { browseAiApiCall } from '../common/client';
import { browseAiAuth } from '../common/auth';
import { robotIdDropdown, taskIdDropdown } from '../common/props';
export const getTaskDetailsAction = createAction({
name: 'get-task-details',
auth: browseAiAuth,
displayName: 'Get Task Details',
description:
'Retrieves the details of a specific task executed by a Browse AI robot.',
props: {
robotId: robotIdDropdown,
taskId: taskIdDropdown,
},
async run(context) {
const { robotId, taskId } = context.propsValue;
try {
const response = await browseAiApiCall({
auth: { apiKey: context.auth.secret_text },
method: HttpMethod.GET,
resourceUri: `/robots/${robotId}/tasks/${taskId}`,
});
return response;
} catch (error: any) {
if (error.response?.status === 404) {
throw new Error(
'Task not found. Please verify the Robot ID and Task ID.'
);
}
if (error.response?.status === 401) {
throw new Error('Authentication failed. Please check your API key.');
}
throw new Error(
`Failed to fetch task details: ${
error.message || 'Unknown error occurred'
}`
);
}
},
});

View File

@@ -0,0 +1,35 @@
import { createAction } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { browseAiApiCall } from '../common/client';
import { browseAiAuth } from '../common/auth';
export const listRobotsAction = createAction({
name: 'list-robots',
auth: browseAiAuth,
displayName: 'List Robots',
description: 'Retrieves all robots available in your account.',
props: {},
async run(context) {
try {
const response = await browseAiApiCall({
auth: { apiKey: context.auth.secret_text },
method: HttpMethod.GET,
resourceUri: '/robots',
});
return response;
} catch (error: any) {
if (error.response?.status === 401) {
throw new Error('Authentication failed. Please check your API key.');
}
if (error.response?.status === 429) {
throw new Error('Rate limit exceeded. Please wait before retrying.');
}
throw new Error(
`Failed to fetch robots: ${error.message || 'Unknown error occurred'}`
);
}
},
});

View File

@@ -0,0 +1,59 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createAction, Property } from '@activepieces/pieces-framework';
import { browseAiApiCall } from '../common/client';
import { browseAiAuth } from '../common/auth';
import { robotIdDropdown, robotParameters } from '../common/props';
export const runRobotAction = createAction({
name: 'run-robot',
auth: browseAiAuth,
displayName: 'Run a Robot',
description:
'Runs a robot on-demand with custom input parameters.',
props: {
robotId: robotIdDropdown,
recordVideo: Property.Checkbox({
displayName: 'Record Video',
description:
'Try to record a video while running the task.',
required: false,
defaultValue: false,
}),
robotParams: robotParameters,
},
async run(context) {
const { robotId, recordVideo } = context.propsValue;
const inputParameters = context.propsValue.robotParams ?? {};
try {
const response = await browseAiApiCall({
method: HttpMethod.POST,
resourceUri: `/robots/${robotId}/tasks`,
auth: { apiKey: context.auth.secret_text },
body: {
recordVideo: recordVideo || false,
inputParameters,
},
});
return response;
} catch (error: any) {
if (error.response?.status === 401) {
throw new Error('Authentication failed. Please check your API key.');
}
if (error.response?.status === 404) {
throw new Error('Robot not found. Please verify the robot ID.');
}
if (error.response?.status === 429) {
throw new Error('Rate limit exceeded. Please wait before retrying.');
}
throw new Error(
`Failed to run robot: ${error.message || 'Unknown error occurred'}`
);
}
},
});

View File

@@ -0,0 +1,24 @@
import { PieceAuth } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { browseAiApiCall } from './client';
export const browseAiAuth = PieceAuth.SecretText({
displayName: 'API Key',
description: 'You can find your Browse AI API key on the dashboard under Settings → API Key.',
required: true,
validate: async ({ auth }) => {
try {
await browseAiApiCall({
method: HttpMethod.GET,
resourceUri: '/status',
auth: { apiKey: auth },
});
return { valid: true };
} catch {
return {
valid: false,
error: 'Invalid API Key. Please check your Browse AI credentials.',
};
}
},
});

View File

@@ -0,0 +1,85 @@
import {
httpClient,
HttpMethod,
HttpRequest,
HttpMessageBody,
QueryParams,
} from '@activepieces/pieces-common';
export type BrowseAiAuthProps = {
apiKey: string;
};
export type BrowseAiApiCallParams = {
method: HttpMethod;
resourceUri: string;
query?: Record<string, string | number | string[] | undefined>;
body?: unknown;
auth: BrowseAiAuthProps;
};
export async function browseAiApiCall<T extends HttpMessageBody>({
method,
resourceUri,
query,
body,
auth,
}: BrowseAiApiCallParams): Promise<T> {
const { apiKey } = auth;
if (!apiKey) {
throw new Error('Browse AI API key is required for authentication');
}
const queryParams: QueryParams = {};
if (query) {
for (const [key, value] of Object.entries(query)) {
if (value !== null && value !== undefined) {
queryParams[key] = String(value);
}
}
}
const baseUrl = `https://api.browse.ai/v2`;
const request: HttpRequest = {
method,
url: `${baseUrl}${resourceUri}`,
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
queryParams,
body,
};
try {
const response = await httpClient.sendRequest<T>(request);
return response.body;
} catch (error: any) {
const statusCode = error.response?.status;
const errorData = error.response?.data;
switch (statusCode) {
case 400:
throw new Error(`Bad Request: ${errorData?.message || 'Invalid parameters'}`);
case 401:
throw new Error('Unauthorized: Invalid API key. Please check your credentials.');
case 403:
throw new Error('Forbidden: You do not have permission to access this resource.');
case 404:
throw new Error('Not Found: The requested resource does not exist.');
case 429:
throw new Error('Rate Limit Exceeded: Please slow down your requests.');
case 500:
throw new Error('Internal Server Error: Something went wrong on Browse AIs side.');
default:
{
const message = errorData?.message || error.message || 'Unknown error';
throw new Error(`Browse AI API Error (${statusCode || 'Unknown'}): ${message}`);
}
}
}
}

View File

@@ -0,0 +1,213 @@
import { DynamicPropsValue, Property } from '@activepieces/pieces-framework';
import { HttpMethod } from '@activepieces/pieces-common';
import { browseAiApiCall } from './client';
import { browseAiAuth } from './auth';
interface BrowseAiRobot {
id: string;
name: string;
}
interface BrowseAiTask {
id: string;
status: string;
createdAt?: number;
}
interface BrowseAiTasksResponse {
statusCode: number;
messageCode: string;
result: {
robotTasks: {
totalCount: number;
pageNumber: number;
hasMore: boolean;
items: BrowseAiTask[];
};
};
}
interface BrowseAiRobotResponse {
robot: {
id: string;
name: string;
inputParameters: {
type: string;
name: string;
label: string;
required: boolean;
options?: { label: string; value: string }[];
}[];
};
}
export const robotIdDropdown = Property.Dropdown({
auth: browseAiAuth,
displayName: 'Robot',
description: 'Select a robot from your Browse AI account',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your Browse AI account first.',
};
}
try {
const response = await browseAiApiCall<{
robots: { items: BrowseAiRobot[] };
}>({
method: HttpMethod.GET,
resourceUri: '/robots',
auth: { apiKey: auth.secret_text },
});
const robots = response?.robots?.items ?? [];
if (robots.length === 0) {
return {
disabled: true,
options: [],
placeholder: 'No robots found in your account.',
};
}
return {
disabled: false,
options: robots.map((robot) => ({
label: robot.name,
value: robot.id,
})),
};
} catch (error: any) {
return {
disabled: true,
options: [],
placeholder:
'Failed to load robots. Please check your API key and try again.',
};
}
},
});
export const taskIdDropdown = Property.Dropdown({
auth: browseAiAuth,
displayName: 'Task',
description: 'Select a task associated with the selected robot',
required: true,
refreshers: ['robotId'],
options: async ({ auth, robotId }) => {
if (!auth) {
return {
disabled: true,
options: [],
placeholder: 'Please connect your Browse AI account.',
};
}
if (!robotId) {
return {
disabled: true,
options: [],
placeholder: 'Please select a robot first.',
};
}
try {
const response = await browseAiApiCall<BrowseAiTasksResponse>({
method: HttpMethod.GET,
resourceUri: `/robots/${robotId}/tasks`,
auth: { apiKey: auth.secret_text },
});
const tasks = response.result?.robotTasks?.items ?? [];
if (tasks.length === 0) {
return {
disabled: true,
options: [],
placeholder: 'No tasks found for the selected robot.',
};
}
return {
disabled: false,
options: tasks.map((task) => {
const createdDate = task.createdAt
? new Date(task.createdAt).toLocaleDateString()
: 'Unknown date';
return {
label: `${task.id} - ${task.status} (${createdDate})`,
value: task.id,
};
}),
};
} catch (e) {
return {
disabled: true,
options: [],
placeholder: `Error fetching tasks: ${
e instanceof Error ? e.message : 'Unknown error'
}`,
};
}
},
});
export const robotParameters = Property.DynamicProperties({
auth: browseAiAuth,
displayName: 'Input Parameters',
refreshers: ['robotId'],
required: true,
props: async ({ auth, robotId }) => {
if (!auth || !robotId) return {};
try {
const response = await browseAiApiCall<BrowseAiRobotResponse>({
method: HttpMethod.GET,
resourceUri: `/robots/${robotId}`,
auth: { apiKey: auth.secret_text },
});
const props: DynamicPropsValue = {};
const params = response.robot.inputParameters ?? [];
for (const param of params) {
switch (param.type) {
case 'number':
props[param.name] = Property.Number({
displayName: param.label,
required: param.required,
});
break;
case 'url':
case 'string':
props[param.name] = Property.ShortText({
displayName: param.label,
required: param.required,
});
break;
case 'select':
props[param.name] = Property.StaticDropdown({
displayName: param.label,
required: param.required,
options: {
disabled: false,
options: param.options ? param.options : [],
},
});
break;
default:
break;
}
}
return props;
} catch {
return {};
}
},
});

View File

@@ -0,0 +1,135 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { isNil } from '@activepieces/shared';
import { browseAiAuth } from '../common/auth';
import { browseAiApiCall } from '../common/client';
import { robotIdDropdown } from '../common/props';
const TRIGGER_KEY = 'browse-ai-task_finished_successfully';
export const taskFinishedSuccessfullyTrigger = createTrigger({
auth: browseAiAuth,
name: 'task_finished_successfully',
displayName: 'Task Finished Successfully',
description:
'Triggers when a robot finishes a task successfully.',
type: TriggerStrategy.WEBHOOK,
props: {
robotId: robotIdDropdown,
},
async onEnable(context) {
const { robotId } = context.propsValue;
const apiKey = context.auth.secret_text;
try {
// Verify robot exists and we have access
await browseAiApiCall({
method: HttpMethod.GET,
auth: { apiKey },
resourceUri: `/robots/${robotId}`,
});
const response = await browseAiApiCall<{
webhook: { id: string; url: string; status: string };
}>({
method: HttpMethod.POST,
auth: { apiKey },
resourceUri: `/robots/${robotId}/webhooks`,
body: {
hookUrl: context.webhookUrl,
eventType: 'taskFinishedSuccessfully',
},
});
await context.store.put<string>(TRIGGER_KEY, response.webhook.id);
} catch (error: any) {
if (error.response?.status === 404) {
throw new Error(
`Robot not found: The robot with ID "${robotId}" does not exist or you do not have access to it. Please verify the robot ID and your permissions.`
);
}
if (error.response?.status === 403) {
throw new Error(
'Access denied: You do not have permission to set up webhooks for this robot. Please check your Browse AI account permissions and ensure you have webhook access.'
);
}
if (error.response?.status === 400) {
throw new Error(
`Invalid webhook configuration: ${
error.response?.data?.message || error.message
}. Please check your webhook URL and robot ID.`
);
}
if (error.response?.status === 429) {
throw new Error(
'Rate limit exceeded: Too many webhook requests. Please wait a moment and try again.'
);
}
throw new Error(
`Failed to set up webhook: ${
error.message || 'Unknown error occurred'
}. Please check your robot ID and try again.`
);
}
},
async onDisable(context) {
const { robotId } = context.propsValue;
const webhookId = await context.store.get<string>(TRIGGER_KEY);
const apiKey = context.auth.secret_text;
if (!isNil(webhookId)) {
try {
await browseAiApiCall({
method: HttpMethod.DELETE,
auth: { apiKey },
resourceUri: `/robots/${robotId}/webhooks/${webhookId}`,
});
} catch (error: any) {
console.warn(
`Warning: Failed to clean up webhook ${webhookId}:`,
error.message
);
// Clean up the stored webhook ID even if deletion failed
await context.store.delete(TRIGGER_KEY);
}
}
},
async run(context) {
const payload = context.payload.body as {
task: Record<string, any>;
event: string;
};
if (payload.event !== 'task.finishedSuccessfully') return [];
return [payload.task];
},
async test(context) {
const { robotId } = context.propsValue;
const apiKey = context.auth.secret_text;
const response = await browseAiApiCall<{
result: { robotTasks: { items: { id: string }[] } };
}>({
method: HttpMethod.GET,
auth: { apiKey },
resourceUri: `/robots/${robotId}/tasks`,
query: { status: 'successful', sort: '-createdAt' },
});
return response.result.robotTasks.items;
},
sampleData: {},
});

View File

@@ -0,0 +1,134 @@
import { HttpMethod } from '@activepieces/pieces-common';
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
import { isNil } from '@activepieces/shared';
import { browseAiAuth } from '../common/auth';
import { browseAiApiCall } from '../common/client';
import { robotIdDropdown } from '../common/props';
const TRIGGER_KEY = 'browse-ai-task_finished_with_error';
export const taskFinishedWithErrorTrigger = createTrigger({
auth: browseAiAuth,
name: 'task_finished_with_error',
displayName: 'Task Finished with Error',
description: 'Triggers when a robot task run fails with an error.',
type: TriggerStrategy.WEBHOOK,
props: {
robotId: robotIdDropdown,
},
async onEnable(context) {
const { robotId } = context.propsValue;
const apiKey = context.auth.secret_text;
try {
// Verify robot exists and we have access
await browseAiApiCall({
method: HttpMethod.GET,
auth: { apiKey },
resourceUri: `/robots/${robotId}`,
});
const response = await browseAiApiCall<{
webhook: { id: string; url: string; status: string };
}>({
method: HttpMethod.POST,
auth: { apiKey },
resourceUri: `/robots/${robotId}/webhooks`,
body: {
hookUrl: context.webhookUrl,
eventType: 'taskFinishedWithError',
},
});
await context.store.put<string>(TRIGGER_KEY, response.webhook.id);
} catch (error: any) {
if (error.response?.status === 404) {
throw new Error(
`Robot not found: The robot with ID "${robotId}" does not exist or you do not have access to it. Please verify the robot ID and your permissions.`
);
}
if (error.response?.status === 403) {
throw new Error(
'Access denied: You do not have permission to set up webhooks for this robot. Please check your Browse AI account permissions and ensure you have webhook access.'
);
}
if (error.response?.status === 400) {
throw new Error(
`Invalid webhook configuration: ${
error.response?.data?.message || error.message
}. Please check your webhook URL and robot ID.`
);
}
if (error.response?.status === 429) {
throw new Error(
'Rate limit exceeded: Too many webhook requests. Please wait a moment and try again.'
);
}
throw new Error(
`Failed to set up webhook: ${
error.message || 'Unknown error occurred'
}. Please check your robot ID and try again.`
);
}
},
async onDisable(context) {
const { robotId } = context.propsValue;
const webhookId = await context.store.get<string>(TRIGGER_KEY);
const apiKey = context.auth.secret_text;
if (!isNil(webhookId)) {
try {
await browseAiApiCall({
method: HttpMethod.DELETE,
auth: { apiKey },
resourceUri: `/robots/${robotId}/webhooks/${webhookId}`,
});
} catch (error: any) {
console.warn(
`Warning: Failed to clean up webhook ${webhookId}:`,
error.message
);
// Clean up the stored webhook ID even if deletion failed
await context.store.delete(TRIGGER_KEY);
}
}
},
async run(context) {
const payload = context.payload.body as {
task: Record<string, any>;
event: string;
};
if (payload.event !== 'task.finishedWithError') return [];
return [payload.task];
},
async test(context) {
const { robotId } = context.propsValue;
const apiKey = context.auth.secret_text;
const response = await browseAiApiCall<{
result: { robotTasks: { items: { id: string }[] } };
}>({
method: HttpMethod.GET,
auth: { apiKey },
resourceUri: `/robots/${robotId}/tasks`,
query: { status: 'failed', sort: '-createdAt' },
});
return response.result.robotTasks.items;
},
sampleData: {},
});