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,61 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { harvestAuth } from '../..';
import {
getAccessTokenOrThrow,
HttpMethod,
} from '@activepieces/pieces-common';
import { callHarvestApi, filterDynamicFields } from '../common';
import { propsValidation } from '@activepieces/pieces-common';
import { z } from 'zod';
export const getClients = createAction({
name: 'get_clients',
auth: harvestAuth,
displayName: 'Get Clients',
description: 'Fetches Clients',
props: {
is_active: Property.ShortText({
description: 'Pass `true` to only return active clients and `false` to return inactive clients.',
displayName: 'Is Active',
required: false,
}),
updated_since: Property.ShortText({
description: 'Only return clients that have been updated since the given date and time.',
displayName: 'Updated since',
required: false,
}),
page: Property.ShortText({
description: 'DEPRECATED: The page number to use in pagination.',
displayName: 'Page',
required: false,
}),
per_page: Property.ShortText({
description: 'The number of records to return per page. (1-2000)',
displayName: 'Records per page',
required: false,
}),
},
async run(context) {
// Validate the input properties using Zod
await propsValidation.validateZod(context.propsValue, {
per_page: z
.string()
.optional()
.transform((val) => (val === undefined || val === '' ? undefined : parseInt(val, 10)))
.refine(
(val) => val === undefined || (Number.isInteger(val) && val >= 1 && val <= 2000),
'Per Page must be a number between 1 and 2000.'
),
});
const params = filterDynamicFields(context.propsValue);
const response = await callHarvestApi(
HttpMethod.GET,
`clients`,
getAccessTokenOrThrow(context.auth),
params
);
return response.body; },
});

View File

@@ -0,0 +1,78 @@
import { Property, createAction } from "@activepieces/pieces-framework";
import { harvestAuth } from '../..';
import {
getAccessTokenOrThrow,
HttpMethod,
} from '@activepieces/pieces-common';
import { callHarvestApi, filterDynamicFields } from '../common';
import { propsValidation } from '@activepieces/pieces-common';
import { z } from 'zod';
export const getEstimates = createAction({
name: 'get_estimates', // Must be a unique across the piece, this shouldn't be changed.
auth: harvestAuth,
displayName: 'Get Estimates',
description: 'Fetches Estimates',
props: {
from: Property.ShortText({
description: 'Only return estimates with an issue_date on or after the given date. (YYYY-MM-DD)',
displayName: 'From',
required: false,
}),
to: Property.ShortText({
description: 'Only return estimates with an issue_date on or before the given date. (YYYY-MM-DD)',
displayName: 'To',
required: false,
}),
state: Property.ShortText({
description: 'Only return estimates with a state matching the value provided. Options: draft, open, accepted, or declined.',
displayName: 'State',
required: false,
}),
updated_since: Property.ShortText({
description: 'Only return estimates that have been updated since the given date and time.',
displayName: 'Updated since',
required: false,
}),
client_id: Property.ShortText({
description: 'Only return estimates belonging to the client with the given ID.',
displayName: 'Client Id',
required: false,
}),
page: Property.ShortText({
description: 'The page number to use in pagination.',
displayName: 'Page',
required: false,
}),
per_page: Property.ShortText({
description: 'The number of records to return per page. (1-2000)',
displayName: 'Records per page',
required: false,
}),
},
async run(context) {
// Validate the input properties using Zod
await propsValidation.validateZod(context.propsValue, {
per_page: z
.string()
.optional()
.transform((val) => (val === undefined || val === '' ? undefined : parseInt(val, 10)))
.refine(
(val) => val === undefined || (Number.isInteger(val) && val >= 1 && val <= 2000),
'Per Page must be a number between 1 and 2000.'
),
});
const params = filterDynamicFields(context.propsValue);
const response = await callHarvestApi(
HttpMethod.GET,
`estimates`,
getAccessTokenOrThrow(context.auth),
params
);
return response.body; },
});

View File

@@ -0,0 +1,88 @@
import { Property, createAction } from "@activepieces/pieces-framework";
import { harvestAuth } from '../..';
import {
getAccessTokenOrThrow,
HttpMethod,
} from '@activepieces/pieces-common';
import { callHarvestApi, filterDynamicFields } from '../common';
import { propsValidation } from '@activepieces/pieces-common';
import { z } from 'zod';
export const getExpenses = createAction({
name: 'get_expenses', // Must be a unique across the piece, this shouldn't be changed.
auth: harvestAuth,
displayName: 'Get Expenses',
description: 'Fetches expenses',
props: {
from: Property.ShortText({
description: 'Only return expenses with an spent_date on or after the given date. (YYYY-MM-DD)',
displayName: 'From',
required: false,
}),
to: Property.ShortText({
description: 'Only return expenses with an spent_date on or before the given date. (YYYY-MM-DD)',
displayName: 'To',
required: false,
}),
user_id: Property.ShortText({
description: 'Only return expenses belonging to the user with the given ID.',
displayName: 'User Id',
required: false,
}),
client_id: Property.ShortText({
description: 'Only return expenses belonging to the client with the given ID.',
displayName: 'Client Id',
required: false,
}),
project_id: Property.ShortText({
description: 'Only return expenses belonging to the project with the given ID.',
displayName: 'Project Id',
required: false,
}),
is_billed: Property.ShortText({
description: 'Pass `true` to only return expenses that have been invoiced and `false` to return expenses that have not been invoiced.',
displayName: 'Is Billed',
required: false,
}),
updated_since: Property.ShortText({
description: 'Only return expenses that have been updated since the given date and time.',
displayName: 'Updated since',
required: false,
}),
page: Property.ShortText({
description: 'The page number to use in pagination.',
displayName: 'Page',
required: false,
}),
per_page: Property.ShortText({
description: 'The number of records to return per page. (1-2000)',
displayName: 'Records per page',
required: false,
}),
},
async run(context) {
// Validate the input properties using Zod
await propsValidation.validateZod(context.propsValue, {
per_page: z
.string()
.optional()
.transform((val) => (val === undefined || val === '' ? undefined : parseInt(val, 10)))
.refine(
(val) => val === undefined || (Number.isInteger(val) && val >= 1 && val <= 2000),
'Per Page must be a number between 1 and 2000.'
),
});
const params = filterDynamicFields(context.propsValue);
const response = await callHarvestApi(
HttpMethod.GET,
`expenses`,
getAccessTokenOrThrow(context.auth),
params
);
return response.body; },
});

View File

@@ -0,0 +1,83 @@
import { Property, createAction } from "@activepieces/pieces-framework";
import { harvestAuth } from '../..';
import {
getAccessTokenOrThrow,
HttpMethod,
} from '@activepieces/pieces-common';
import { callHarvestApi, filterDynamicFields } from '../common';
import { propsValidation } from '@activepieces/pieces-common';
import { z } from 'zod';
export const getInvoices = createAction({
name: 'get_invoices', // Must be a unique across the piece, this shouldn't be changed.
auth: harvestAuth,
displayName: 'Get Invoices',
description: 'Fetches invoices',
props: {
from: Property.ShortText({
description: 'Only return invoices with an issue_date on or after the given date. (YYYY-MM-DD)',
displayName: 'From',
required: false,
}),
to: Property.ShortText({
description: 'Only return invoices with an issue_date on or before the given date. (YYYY-MM-DD)',
displayName: 'To',
required: false,
}),
state: Property.ShortText({
description: 'Only return invoices with a state matching the value provided. Options: draft, open, paid, or closed.',
displayName: 'State',
required: false,
}),
updated_since: Property.ShortText({
description: 'Only return invoices that have been updated since the given date and time.',
displayName: 'Updated since',
required: false,
}),
client_id: Property.ShortText({
description: 'Only return invoices belonging to the client with the given ID.',
displayName: 'Client Id',
required: false,
}),
project_id: Property.ShortText({
description: 'Only return invoices belonging to the project with the given ID.',
displayName: 'Project Id',
required: false,
}),
page: Property.ShortText({
description: 'The page number to use in pagination.',
displayName: 'Page',
required: false,
}),
per_page: Property.ShortText({
description: 'The number of records to return per page. (1-2000)',
displayName: 'Records per page',
required: false,
}),
},
async run(context) {
// Validate the input properties using Zod
await propsValidation.validateZod(context.propsValue, {
per_page: z
.string()
.optional()
.transform((val) => (val === undefined || val === '' ? undefined : parseInt(val, 10)))
.refine(
(val) => val === undefined || (Number.isInteger(val) && val >= 1 && val <= 2000),
'Per Page must be a number between 1 and 2000.'
),
});
const params = filterDynamicFields(context.propsValue);
const response = await callHarvestApi(
HttpMethod.GET,
`invoices`,
getAccessTokenOrThrow(context.auth),
params
);
return response.body; },
});

View File

@@ -0,0 +1,67 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { harvestAuth } from '../..';
import {
getAccessTokenOrThrow,
HttpMethod,
} from '@activepieces/pieces-common';
import { callHarvestApi, filterDynamicFields } from '../common';
import { propsValidation } from '@activepieces/pieces-common';
import { z } from 'zod';
export const getProjects = createAction({
name: 'get_projects', // Must be a unique across the piece, this shouldn't be changed.
auth: harvestAuth,
displayName: 'Get Projects',
description: 'Fetches projects',
props: {
is_active: Property.ShortText({
description: 'Pass `true` to only return active projects and `false` to return inactive projects.',
displayName: 'Is Active',
required: false,
}),
updated_since: Property.ShortText({
description: 'Only return projects that have been updated since the given date and time.',
displayName: 'Updated since',
required: false,
}),
client_id: Property.ShortText({
description: 'Only return invoices belonging to the client with the given ID.',
displayName: 'Client Id',
required: false,
}),
page: Property.ShortText({
description: 'DEPRECATED: The page number to use in pagination.',
displayName: 'Page',
required: false,
}),
per_page: Property.ShortText({
description: 'The number of records to return per page. (1-2000)',
displayName: 'Records per page',
required: false,
}),
},
async run(context) {
// Validate the input properties using Zod
await propsValidation.validateZod(context.propsValue, {
per_page: z
.string()
.optional()
.transform((val) => (val === undefined || val === '' ? undefined : parseInt(val, 10)))
.refine(
(val) => val === undefined || (Number.isInteger(val) && val >= 1 && val <= 2000),
'Per Page must be a number between 1 and 2000.'
),
});
const params = filterDynamicFields(context.propsValue);
const response = await callHarvestApi(
HttpMethod.GET,
`projects`,
getAccessTokenOrThrow(context.auth),
params
);
return response.body; },
});

View File

@@ -0,0 +1,52 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { harvestAuth } from '../..';
import {
getAccessTokenOrThrow,
HttpMethod,
} from '@activepieces/pieces-common';
import { callHarvestApi, filterDynamicFields } from '../common';
import { propsValidation } from '@activepieces/pieces-common';
import { z } from 'zod';
export const getRoles = createAction({
name: 'get_roles', // Must be a unique across the piece, this shouldn't be changed.
auth: harvestAuth,
displayName: 'Get Roles',
description: 'Fetches Roles',
props: {
page: Property.ShortText({
description: 'DEPRECATED: The page number to use in pagination.',
displayName: 'Page',
required: false,
}),
per_page: Property.ShortText({
description: 'The number of records to return per page. (1-2000)',
displayName: 'Records per page',
required: false,
}),
},
async run(context) {
// Validate the input properties using Zod
await propsValidation.validateZod(context.propsValue, {
per_page: z
.string()
.optional()
.transform((val) => (val === undefined || val === '' ? undefined : parseInt(val, 10)))
.refine(
(val) => val === undefined || (Number.isInteger(val) && val >= 1 && val <= 2000),
'Per Page must be a number between 1 and 2000.'
),
});
const params = filterDynamicFields(context.propsValue);
const response = await callHarvestApi(
HttpMethod.GET,
`roles`,
getAccessTokenOrThrow(context.auth),
params
);
return response.body; },
});

View File

@@ -0,0 +1,62 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { harvestAuth } from '../..';
import {
getAccessTokenOrThrow,
HttpMethod,
} from '@activepieces/pieces-common';
import { callHarvestApi, filterDynamicFields } from '../common';
import { propsValidation } from '@activepieces/pieces-common';
import { z } from 'zod';
export const getTasks = createAction({
name: 'get_tasks', // Must be a unique across the piece, this shouldn't be changed.
auth: harvestAuth,
displayName: 'Get Tasks',
description: 'Fetches Tasks',
props: {
is_active: Property.ShortText({
description: 'Pass `true` to only return active tasks and `false` to return inactive tasks.',
displayName: 'Is Active',
required: false,
}),
updated_since: Property.ShortText({
description: 'Only return tasks that have been updated since the given date and time.',
displayName: 'Updated since',
required: false,
}),
page: Property.ShortText({
description: 'DEPRECATED: The page number to use in pagination.',
displayName: 'Page',
required: false,
}),
per_page: Property.ShortText({
description: 'The number of records to return per page. (1-2000)',
displayName: 'Records per page',
required: false,
}),
},
async run(context) {
// Validate the input properties using Zod
await propsValidation.validateZod(context.propsValue, {
per_page: z
.string()
.optional()
.transform((val) => (val === undefined || val === '' ? undefined : parseInt(val, 10)))
.refine(
(val) => val === undefined || (Number.isInteger(val) && val >= 1 && val <= 2000),
'Per Page must be a number between 1 and 2000.'
),
});
const params = filterDynamicFields(context.propsValue);
const response = await callHarvestApi(
HttpMethod.GET,
`tasks`,
getAccessTokenOrThrow(context.auth),
params
);
return response.body; },
});

View File

@@ -0,0 +1,103 @@
import { Property, createAction } from "@activepieces/pieces-framework";
import { harvestAuth } from '../..';
import {
getAccessTokenOrThrow,
HttpMethod,
} from '@activepieces/pieces-common';
import { callHarvestApi, filterDynamicFields } from '../common';
import { propsValidation } from '@activepieces/pieces-common';
import { z } from 'zod';
export const getTime_entries = createAction({
name: 'get_time_entries', // Must be a unique across the piece, this shouldn't be changed.
auth: harvestAuth,
displayName: 'Get Time Entries',
description: 'Fetches Time Entries',
props: {
from: Property.ShortText({
description: 'Only return time entries with an spent_date on or after the given date. (YYYY-MM-DD)',
displayName: 'From',
required: false,
}),
to: Property.ShortText({
description: 'Only return time entries with an spent_date on or before the given date. (YYYY-MM-DD)',
displayName: 'To',
required: false,
}),
user_id: Property.ShortText({
description: 'Only return time entries belonging to the user with the given ID.',
displayName: 'User Id',
required: false,
}),
client_id: Property.ShortText({
description: 'Only return time entries belonging to the client with the given ID.',
displayName: 'Client Id',
required: false,
}),
project_id: Property.ShortText({
description: 'Only return time entries belonging to the project with the given ID.',
displayName: 'Project Id',
required: false,
}),
task_id: Property.ShortText({
description: 'Only return time entries belonging to the task with the given ID.',
displayName: 'Task Id',
required: false,
}),
external_reference_id: Property.ShortText({
description: 'Only return time entries with the given external reference ID.',
displayName: 'External Reference Id',
required: false,
}),
is_billed: Property.ShortText({
description: 'Pass `true` to only return time entries that have been invoiced and `false` to return time entries that have not been invoiced.',
displayName: 'Is Billed',
required: false,
}),
is_running: Property.ShortText({
description: 'Pass `true` to only return running time entries and `false` to return non-running time entries.',
displayName: 'Is Running',
required: false,
}),
updated_since: Property.ShortText({
description: 'Only return time entries that have been updated since the given date and time.',
displayName: 'Updated since',
required: false,
}),
page: Property.ShortText({
description: 'The page number to use in pagination.',
displayName: 'Page',
required: false,
}),
per_page: Property.ShortText({
description: 'The number of records to return per page. (1-2000)',
displayName: 'Records per page',
required: false,
}),
},
async run(context) {
// Validate the input properties using Zod
await propsValidation.validateZod(context.propsValue, {
per_page: z
.string()
.optional()
.transform((val) => (val === undefined || val === '' ? undefined : parseInt(val, 10)))
.refine(
(val) => val === undefined || (Number.isInteger(val) && val >= 1 && val <= 2000),
'Per Page must be a number between 1 and 2000.'
),
});
const params = filterDynamicFields(context.propsValue);
const response = await callHarvestApi(
HttpMethod.GET,
`time_entries`,
getAccessTokenOrThrow(context.auth),
params
);
return response.body; },
});

View File

@@ -0,0 +1,61 @@
import { Property, createAction } from '@activepieces/pieces-framework';
import { harvestAuth } from '../..';
import {
getAccessTokenOrThrow,
HttpMethod,
} from '@activepieces/pieces-common';
import { callHarvestApi, filterDynamicFields } from '../common';
import { propsValidation } from '@activepieces/pieces-common';
import { z } from 'zod';
export const getUsers = createAction({
name: 'get_users',
auth: harvestAuth,
displayName: 'Get Users',
description: 'Fetches Users',
props: {
is_active: Property.ShortText({
description: 'Pass `true` to only return active users and `false` to return inactive users.',
displayName: 'Is Active',
required: false,
}),
updated_since: Property.ShortText({
description: 'Only return users that have been updated since the given date and time.',
displayName: 'Updated since',
required: false,
}),
page: Property.ShortText({
description: 'DEPRECATED: The page number to use in pagination.',
displayName: 'Page',
required: false,
}),
per_page: Property.ShortText({
description: 'The number of records to return per page. (1-2000)',
displayName: 'Records per page',
required: false,
}),
},
async run(context) {
// Validate the input properties using Zod
await propsValidation.validateZod(context.propsValue, {
per_page: z
.string()
.optional()
.transform((val) => (val === undefined || val === '' ? undefined : parseInt(val, 10)))
.refine(
(val) => val === undefined || (Number.isInteger(val) && val >= 1 && val <= 2000),
'Per Page must be a number between 1 and 2000.'
),
});
const params = filterDynamicFields(context.propsValue);
const response = await callHarvestApi(
HttpMethod.GET,
`users`,
getAccessTokenOrThrow(context.auth),
params
);
return response.body; },
});

View File

@@ -0,0 +1,66 @@
import { Property, createAction } from "@activepieces/pieces-framework";
import { harvestAuth } from '../..';
import {
getAccessTokenOrThrow,
HttpMethod,
} from '@activepieces/pieces-common';
import { callHarvestApi, filterDynamicFields } from '../common';
import { propsValidation } from '@activepieces/pieces-common';
import { z } from 'zod';
export const reportsUninvoiced = createAction({
name: 'reports-uninvoiced',
auth: harvestAuth,
displayName: 'Uninvoiced Report',
description: 'Uninvoiced hours and expenses for all billable projects',
props: {
from: Property.ShortText({
description: 'Only report on time entries and expenses with a spent_date on or after the given date. (YYYY-MM-DD)',
displayName: 'From',
required: true,
}),
to: Property.ShortText({
description: 'Only report on time entries and expenses with a spent_date on or before the given date. (YYYY-MM-DD)',
displayName: 'To',
required: true,
}),
include_fixed_fee: Property.ShortText({
description: 'Whether or not to include fixed-fee projects in the response. (Default: true)',
displayName: 'Include Fixed Fee',
required: false,
}),
page: Property.ShortText({
description: 'The page number to use in pagination.',
displayName: 'Page',
required: false,
}),
per_page: Property.ShortText({
description: 'The number of records to return per page. (1-2000)',
displayName: 'Records per page',
required: false,
}),
},
async run(context) {
// Validate the input properties using Zod
await propsValidation.validateZod(context.propsValue, {
per_page: z
.string()
.optional()
.transform((val) => (val === undefined || val === '' ? undefined : parseInt(val, 10)))
.refine(
(val) => val === undefined || (Number.isInteger(val) && val >= 1 && val <= 2000),
'Per Page must be a number between 1 and 2000.'
),
});
const params = filterDynamicFields(context.propsValue);
const response = await callHarvestApi(
HttpMethod.GET,
`reports/uninvoiced`,
getAccessTokenOrThrow(context.auth),
params
);
return response.body; },
});

View File

@@ -0,0 +1,48 @@
import { DynamicPropsValue } from '@activepieces/pieces-framework';
import {
HttpMethod,
HttpMessageBody,
HttpResponse,
httpClient,
AuthenticationType,
} from '@activepieces/pieces-common';
export async function callHarvestApi<T extends HttpMessageBody = any>(
method: HttpMethod,
apiUrl: string,
accessToken: string,
queryParams: any | undefined = undefined,
body: any | undefined = undefined,
headers: any | undefined = undefined
): Promise<HttpResponse<T>> {
return await httpClient.sendRequest<T>({
method: method,
url: `https://api.harvestapp.com/v2/${apiUrl}`,
authentication: {
type: AuthenticationType.BEARER_TOKEN,
token: accessToken,
},
headers,
body,
queryParams,
});
}
//Remove null/undefined values and create an array to be used for queryparams
export function filterDynamicFields(dynamicFields: DynamicPropsValue): { [key: string]: string } {
const fields: { [key: string]: string } = {};
const props = Object.entries(dynamicFields);
for (const [propertyKey, propertyValue] of props) {
if (
propertyValue !== null &&
propertyValue !== undefined &&
propertyValue !== '' &&
!(typeof propertyValue === 'string' && propertyValue.trim() === '')
) {
fields[propertyKey] = propertyValue;
}
}
return fields;
}