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,94 @@
{
"Automate your complete HR hiring and onboarding workflow. Manage applicants, job openings, interview scheduling, and track progress through hiring stages with real-time webhooks.": "Automate your complete HR hiring and onboarding workflow. Manage applicants, job openings, interview scheduling, and track progress through hiring stages with real-time webhooks.",
"Enter your Fountain API key from Profile > Manage API Keys or Settings > Integrations & API Keys": "Enter your Fountain API key from Profile > Manage API Keys or Settings > Integrations & API Keys",
"Create Applicant": "Create Applicant",
"Delete Applicant": "Delete Applicant",
"Get Applicant Details": "Get Applicant Details",
"Get Interview Sessions": "Get Interview Sessions",
"Get Opening": "Get Opening",
"Get Stage": "Get Stage",
"List All Applicant Details": "List All Applicant Details",
"List All Opening Details": "List All Opening Details",
"List All Stages": "List All Stages",
"Update Applicant Info": "Update Applicant Info",
"Add a new applicant to your hiring pipeline": "Add a new applicant to your hiring pipeline",
"Delete an applicant by their ID": "Delete an applicant by their ID",
"Get complete applicant information": "Get complete applicant information",
"Returns the applicant's interview sessions": "Returns the applicant's interview sessions",
"Get details for a specific job opening": "Get details for a specific job opening",
"Retrieves a specific Stage": "Retrieves a specific Stage",
"List applicants with optional filters": "List applicants with optional filters",
"List job openings with optional filters": "List job openings with optional filters",
"Retrieves an Opening's stages": "Retrieves an Opening's stages",
"Update an applicant's information": "Update an applicant's information",
"Full Name": "Full Name",
"Email": "Email",
"Phone Number": "Phone Number",
"Check for Duplicates": "Check for Duplicates",
"Applicant": "Applicant",
"Opening": "Opening",
"Stage": "Stage",
"Location": "Location",
"Labels": "Labels",
"Exclude Temporary": "Exclude Temporary",
"Results Per Page": "Results Per Page",
"Cursor": "Cursor",
"Include Sub-accounts": "Include Sub-accounts",
"Active Only": "Active Only",
"Hiring Funnels Only": "Hiring Funnels Only",
"Sourcing Funnels Only": "Sourcing Funnels Only",
"Private Only": "Private Only",
"Owner": "Owner",
"Name": "Name",
"Custom Data": "Custom Data",
"Secure Data": "Secure Data",
"Rejection Reason": "Rejection Reason",
"Applicant's full name": "Applicant's full name",
"Applicant's email address": "Applicant's email address",
"Applicant's complete phone number": "Applicant's complete phone number",
"Apply additional duplicate applicant blocking rules specified in company duplicate settings": "Apply additional duplicate applicant blocking rules specified in company duplicate settings",
"The applicant to delete (shows 50 most recent applicants)": "The applicant to delete (shows 50 most recent applicants)",
"The applicant to retrieve details for (shows 50 most recent applicants)": "The applicant to retrieve details for (shows 50 most recent applicants)",
"The applicant to get interview sessions for (shows 50 most recent applicants)": "The applicant to get interview sessions for (shows 50 most recent applicants)",
"The opening to retrieve details for": "The opening to retrieve details for",
"The opening that contains the stage": "The opening that contains the stage",
"The stage to retrieve details for": "The stage to retrieve details for",
"Filter applicants by opening": "Filter applicants by opening",
"Filter applicants by location": "Filter applicants by location",
"Filter applicants by stage": "Filter applicants by stage",
"Filter applicants by stage name": "Filter applicants by stage name",
"Filter applicants by labels": "Filter applicants by labels",
"Filter applicants by phone number": "Filter applicants by phone number",
"Whether to exclude temporary/test applicants": "Whether to exclude temporary/test applicants",
"Number of results per page (default: 25)": "Number of results per page (default: 25)",
"Cursor for pagination": "Cursor for pagination",
"Whether to include applicants from sub-accounts": "Whether to include applicants from sub-accounts",
"Filter to only active openings": "Filter to only active openings",
"Filter openings by location": "Filter openings by location",
"Filter to only hiring funnels": "Filter to only hiring funnels",
"Filter to only sourcing funnels": "Filter to only sourcing funnels",
"Filter to only private openings": "Filter to only private openings",
"Filter openings by owner": "Filter openings by owner",
"The opening to get stages for": "The opening to get stages for",
"The applicant to update (shows 50 most recent applicants)": "The applicant to update (shows 50 most recent applicants)",
"Custom data fields to update (key-value pairs)": "Custom data fields to update (key-value pairs)",
"Secure data fields to update (key-value pairs like SSN, etc.)": "Secure data fields to update (key-value pairs like SSN, etc.)",
"Manually set reason for rejection": "Manually set reason for rejection",
"Applicant Webhook": "Applicant Webhook",
"Worker Webhook": "Worker Webhook",
"Custom Attribute Webhook": "Custom Attribute Webhook",
"Universal Tasks Webhook": "Universal Tasks Webhook",
"Triggers when Fountain sends applicant webhook events": "Triggers when Fountain sends applicant webhook events",
"Triggers when Fountain sends worker webhook events": "Triggers when Fountain sends worker webhook events",
"Triggers when Fountain worker custom attribute values change": "Triggers when Fountain worker custom attribute values change",
"Triggers when workers complete tasks or reach flow endpoints in Fountain Onboard": "Triggers when workers complete tasks or reach flow endpoints in Fountain Onboard",
"Markdown": "Markdown",
"Verify Authentication": "Verify Authentication",
"Authentication Key": "Authentication Key",
"\n## Setup Instructions\n\n1. Go to your Fountain account Settings > Automations\n2. Click \"Create automation\"\n3. Select \"Applicant\" as the source\n4. Choose your trigger conditions (e.g., when applicant status changes)\n5. Select \"Send a webhook...\" as the action\n6. Copy and paste the webhook URL below into the URL field\n7. Set the Authentication key (this will be sent as Authorization header)\n8. Make sure to select \"Send applicant payload\" in the webhook options\n\n**Webhook URL:**\n```text\n{{webhookUrl}}\n```\n": "\n## Setup Instructions\n\n1. Go to your Fountain account Settings > Automations\n2. Click \"Create automation\"\n3. Select \"Applicant\" as the source\n4. Choose your trigger conditions (e.g., when applicant status changes)\n5. Select \"Send a webhook...\" as the action\n6. Copy and paste the webhook URL below into the URL field\n7. Set the Authentication key (this will be sent as Authorization header)\n8. Make sure to select \"Send applicant payload\" in the webhook options\n\n**Webhook URL:**\n```text\n{{webhookUrl}}\n```\n",
"Verify the Authorization header matches the expected key": "Verify the Authorization header matches the expected key",
"The authentication key set in Fountain webhook configuration": "The authentication key set in Fountain webhook configuration",
"\n## Setup Instructions\n\n1. Go to your Fountain account Settings > Automations\n2. Click \"Create automation\"\n3. Select \"Worker\" as the source\n4. Choose your trigger conditions (e.g., when worker status changes)\n5. Select \"Send a webhook...\" as the action\n6. Copy and paste the webhook URL below into the URL field\n7. Set the Authentication key (this will be sent as Authorization header)\n8. Make sure to select \"Send worker payload\" in the webhook options\n\n**Webhook URL:**\n```text\n{{webhookUrl}}\n```\n": "\n## Setup Instructions\n\n1. Go to your Fountain account Settings > Automations\n2. Click \"Create automation\"\n3. Select \"Worker\" as the source\n4. Choose your trigger conditions (e.g., when worker status changes)\n5. Select \"Send a webhook...\" as the action\n6. Copy and paste the webhook URL below into the URL field\n7. Set the Authentication key (this will be sent as Authorization header)\n8. Make sure to select \"Send worker payload\" in the webhook options\n\n**Webhook URL:**\n```text\n{{webhookUrl}}\n```\n",
"\n## Setup Instructions\n\n1. Go to your Fountain account Settings > Worker Attributes\n2. Find the custom attribute you want to monitor\n3. Click \"...\" > Manage Webhooks\n4. In the modal, click \"Add Webhook\"\n5. Copy and paste the webhook URL below into the URL field\n6. Save the webhook configuration\n\n**Webhook URL:**\n```text\n{{webhookUrl}}\n```\n\nThis trigger will fire whenever the specified custom attribute's value changes for any worker.\n": "\n## Setup Instructions\n\n1. Go to your Fountain account Settings > Worker Attributes\n2. Find the custom attribute you want to monitor\n3. Click \"...\" > Manage Webhooks\n4. In the modal, click \"Add Webhook\"\n5. Copy and paste the webhook URL below into the URL field\n6. Save the webhook configuration\n\n**Webhook URL:**\n```text\n{{webhookUrl}}\n```\n\nThis trigger will fire whenever the specified custom attribute's value changes for any worker.\n",
"\n## Setup Instructions\n\n1. Go to your Fountain Onboard system\n2. Add a Universal Task with Webhook integration\n3. Select the trigger type (task completion or flow end)\n4. Copy and paste the webhook URL below into the webhook configuration\n5. Publish the flow\n\n**Webhook URL:**\n```text\n{{webhookUrl}}\n```\n\nThis trigger will fire when workers complete tasks or reach flow endpoints in Onboard.\n": "\n## Setup Instructions\n\n1. Go to your Fountain Onboard system\n2. Add a Universal Task with Webhook integration\n3. Select the trigger type (task completion or flow end)\n4. Copy and paste the webhook URL below into the webhook configuration\n5. Publish the flow\n\n**Webhook URL:**\n```text\n{{webhookUrl}}\n```\n\nThis trigger will fire when workers complete tasks or reach flow endpoints in Onboard.\n"
}

View File

@@ -0,0 +1,46 @@
import { createPiece, PieceAuth, Property } from "@activepieces/pieces-framework";
import { fountainCreateApplicant } from './lib/actions/create-applicant';
import { fountainDeleteApplicant } from './lib/actions/delete-applicant';
import { fountainGetApplicantDetails } from './lib/actions/get-applicant-details';
import { fountainGetInterviewSessions } from './lib/actions/get-interview-sessions';
import { fountainGetOpening } from './lib/actions/get-opening';
import { fountainGetStage } from './lib/actions/get-stage';
import { fountainListApplicants } from './lib/actions/list-applicants';
import { fountainListOpenings } from './lib/actions/list-openings';
import { fountainListStages } from './lib/actions/list-stages';
import { fountainUpdateApplicant } from './lib/actions/update-applicant';
import { fountainApplicantWebhook, fountainWorkerWebhook } from './lib/triggers';
import { fountainCustomAttributeWebhook } from './lib/triggers/custom-attribute-webhook';
import { fountainUniversalTasksWebhook } from './lib/triggers/universal-tasks-webhook';
export const API_BASE_URL_DEFAULT = 'https://api.fountain.com';
export const fountainAuth = PieceAuth.CustomAuth({
description: 'Enter your Fountain API key and base URL',
required: true,
props: {
apiKey: PieceAuth.SecretText({
displayName: 'API Key',
description: 'Enter your Fountain API key from Profile > Manage API Keys or Settings > Integrations & API Keys',
required: true,
}),
baseUrl: Property.ShortText({
displayName: 'Base URL',
description: `The base URL for your Fountain API (default: ${API_BASE_URL_DEFAULT}). For example: us-2.fountain.com/api/v2`,
required: false,
}),
},
});
export const fountain = createPiece({
displayName: "Fountain",
description: "Automate your complete HR hiring and onboarding workflow. Manage applicants, job openings, interview scheduling, and track progress through hiring stages with real-time webhooks.",
auth: fountainAuth,
minimumSupportedRelease: '0.36.1',
logoUrl: "https://cdn.activepieces.com/pieces/fountain.png",
authors: ["onyedikachi-david"],
actions: [fountainCreateApplicant, fountainDeleteApplicant, fountainGetApplicantDetails, fountainGetInterviewSessions, fountainGetOpening, fountainGetStage, fountainListApplicants, fountainListOpenings, fountainListStages, fountainUpdateApplicant],
triggers: [fountainApplicantWebhook, fountainWorkerWebhook, fountainCustomAttributeWebhook, fountainUniversalTasksWebhook],
});

View File

@@ -0,0 +1,51 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from '../common/auth';
export const fountainCreateApplicant = createAction({
name: 'create_applicant',
auth: fountainAuth,
displayName: 'Create Applicant',
description: 'Add a new applicant to your hiring pipeline',
props: {
name: Property.ShortText({
displayName: 'Full Name',
description: 'Applicant\'s full name',
required: true,
}),
email: Property.ShortText({
displayName: 'Email',
description: 'Applicant\'s email address',
required: true,
}),
phone_number: Property.ShortText({
displayName: 'Phone Number',
description: 'Applicant\'s complete phone number',
required: true,
}),
check_if_applicant_is_duplicate: Property.Checkbox({
displayName: 'Check for Duplicates',
description: 'Apply additional duplicate applicant blocking rules specified in company duplicate settings',
required: false,
defaultValue: false,
}),
},
async run(context) {
const requestBody = {
name: context.propsValue.name,
email: context.propsValue.email,
phone_number: context.propsValue.phone_number,
check_if_applicant_is_duplicate: context.propsValue.check_if_applicant_is_duplicate,
};
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: getApiUrl(context.auth, '/applicants'),
headers: getAuthHeaders(context.auth),
body: requestBody,
});
return response.body;
},
});

View File

@@ -0,0 +1,58 @@
import { AppConnectionValueForAuthProperty, createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from '../common/auth';
async function getApplicantsDropdown(auth: AppConnectionValueForAuthProperty<typeof fountainAuth>): Promise<{ label: string; value: string }[]> {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(auth, '/applicants'),
headers: getAuthHeaders(auth),
queryParams: { per_page: '50' },
});
const applicants = response.body?.applicants || [];
return applicants.map((applicant: any) => ({
label: `${applicant.name} (${applicant.email})`,
value: applicant.id,
}));
} catch (error) {
return [];
}
}
export const fountainDeleteApplicant = createAction({
name: 'delete_applicant',
auth: fountainAuth,
displayName: 'Delete Applicant',
description: 'Delete an applicant by their ID',
props: {
id: Property.Dropdown({
auth: fountainAuth,
displayName: 'Applicant',
description: 'The applicant to delete (shows 50 most recent applicants)',
required: true,
refreshers: [],
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getApplicantsDropdown(auth) };
},
}),
},
async run(context) {
const applicantId = context.propsValue.id;
const response = await httpClient.sendRequest({
method: HttpMethod.DELETE,
url: getApiUrl(context.auth, `/applicants/${applicantId}`),
headers: getAuthHeaders(context.auth),
});
return {
success: response.status === 204,
status: response.status,
message: response.status === 204 ? 'Applicant deleted successfully' : 'Failed to delete applicant',
};
},
});

View File

@@ -0,0 +1,55 @@
import { AppConnectionValueForAuthProperty, createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from '../common/auth';
import { PiecePropValueSchema } from '@activepieces/pieces-framework';
async function getApplicantsDropdown(auth: AppConnectionValueForAuthProperty<typeof fountainAuth>): Promise<{ label: string; value: string }[]> {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(auth, '/applicants'),
headers: getAuthHeaders(auth),
queryParams: { per_page: '50' },
});
const applicants = response.body?.applicants || [];
return applicants.map((applicant: any) => ({
label: `${applicant.name} (${applicant.email})`,
value: applicant.id,
}));
} catch (error) {
return [];
}
}
export const fountainGetApplicantDetails = createAction({
name: 'get_applicant_details',
auth: fountainAuth,
displayName: 'Get Applicant Details',
description: 'Get complete applicant information',
props: {
id: Property.Dropdown({
displayName: 'Applicant',
description: 'The applicant to delete (shows 50 most recent applicants)',
required: true,
refreshers: [],
auth: fountainAuth,
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getApplicantsDropdown(auth as any) };
},
}),
},
async run(context) {
const applicantId = context.propsValue.id;
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(context.auth, `/applicants/${applicantId}`),
headers: getAuthHeaders(context.auth),
});
return response.body;
},
});

View File

@@ -0,0 +1,54 @@
import { AppConnectionValueForAuthProperty, createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from '../common/auth';
async function getApplicantsDropdown(auth: AppConnectionValueForAuthProperty<typeof fountainAuth>): Promise<{ label: string; value: string }[]> {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(auth, '/applicants'),
headers: getAuthHeaders(auth),
queryParams: { per_page: '50' },
});
const applicants = response.body?.applicants || [];
return applicants.map((applicant: any) => ({
label: `${applicant.name} (${applicant.email})`,
value: applicant.id,
}));
} catch (error) {
return [];
}
}
export const fountainGetInterviewSessions = createAction({
name: 'get_interview_sessions',
auth: fountainAuth,
displayName: 'Get Interview Sessions',
description: 'Returns the applicant\'s interview sessions',
props: {
id: Property.Dropdown({
displayName: 'Applicant',
description: 'The applicant to get interview sessions for (shows 50 most recent applicants)',
required: true,
refreshers: [],
auth: fountainAuth,
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getApplicantsDropdown(auth as any) };
},
}),
},
async run(context) {
const applicantId = context.propsValue.id;
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(context.auth, `/applicants/${applicantId}/booked_slots`),
headers: getAuthHeaders(context.auth),
});
return response.body;
},
});

View File

@@ -0,0 +1,36 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from '../common/auth';
import { getFunnelsDropdown } from '../common/dropdowns';
export const fountainGetOpening = createAction({
name: 'get_opening',
auth: fountainAuth,
displayName: 'Get Opening',
description: 'Get details for a specific job opening',
props: {
id: Property.Dropdown({
displayName: 'Opening',
description: 'The opening to retrieve details for',
required: true,
refreshers: [],
auth: fountainAuth,
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getFunnelsDropdown(auth) };
},
}),
},
async run(context) {
const openingId = context.propsValue.id;
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(context.auth, `/funnels/${openingId}`),
headers: getAuthHeaders(context.auth),
});
return response.body;
},
});

View File

@@ -0,0 +1,48 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from '../common/auth';
import { getFunnelsDropdown, getStagesForFunnelDropdown } from '../common/dropdowns';
export const fountainGetStage = createAction({
name: 'get_stage',
auth: fountainAuth,
displayName: 'Get Stage',
description: 'Retrieves a specific Stage',
props: {
funnel_id: Property.Dropdown({
displayName: 'Opening',
description: 'The opening that contains the stage',
required: true,
refreshers: [],
auth: fountainAuth,
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getFunnelsDropdown(auth) };
},
}),
id: Property.Dropdown({
displayName: 'Stage',
description: 'The stage to retrieve details for',
required: true,
refreshers: ['funnel_id'],
auth: fountainAuth,
options: async ({ auth, funnel_id }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
if (!funnel_id) return { disabled: true, options: [], placeholder: 'Select opening first' };
return { disabled: false, options: await getStagesForFunnelDropdown(auth, funnel_id as string) };
},
}),
},
async run(context) {
const stageId = context.propsValue.id;
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(context.auth, `/stages/${stageId}`),
headers: getAuthHeaders(context.auth),
});
return response.body;
},
});

View File

@@ -0,0 +1,109 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from '../common/auth';
import { getFunnelsDropdown, getLocationsDropdown, getStagesForFunnelDropdown } from '../common/dropdowns';
export const fountainListApplicants = createAction({
name: 'list_applicants',
auth: fountainAuth,
displayName: 'List All Applicant Details',
description: 'List applicants with optional filters',
props: {
funnel_id: Property.Dropdown({
displayName: 'Opening',
description: 'Filter applicants by opening',
required: false,
refreshers: [],
auth: fountainAuth,
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getFunnelsDropdown(auth) };
},
}),
location_id: Property.Dropdown({
displayName: 'Location',
description: 'Filter applicants by location',
required: false,
refreshers: [],
auth: fountainAuth,
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getLocationsDropdown(auth) };
},
}),
stage_id: Property.Dropdown({
displayName: 'Stage',
description: 'Filter applicants by stage',
required: false,
refreshers: ['funnel_id'],
auth: fountainAuth,
options: async ({ auth, funnel_id }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
if (!funnel_id) return { disabled: true, options: [], placeholder: 'Select opening first' };
return { disabled: false, options: await getStagesForFunnelDropdown(auth, funnel_id as string) };
},
}),
stage: Property.ShortText({
displayName: 'Stage',
description: 'Filter applicants by stage name',
required: false,
}),
labels: Property.ShortText({
displayName: 'Labels',
description: 'Filter applicants by labels',
required: false,
}),
phone: Property.ShortText({
displayName: 'Phone Number',
description: 'Filter applicants by phone number',
required: false,
}),
exclude_temporary: Property.Checkbox({
displayName: 'Exclude Temporary',
description: 'Whether to exclude temporary/test applicants',
required: false,
defaultValue: false,
}),
per_page: Property.Number({
displayName: 'Results Per Page',
description: 'Number of results per page (default: 25)',
required: false,
defaultValue: 25,
}),
cursor: Property.ShortText({
displayName: 'Cursor',
description: 'Cursor for pagination',
required: false,
}),
include_subaccounts: Property.Checkbox({
displayName: 'Include Sub-accounts',
description: 'Whether to include applicants from sub-accounts',
required: false,
defaultValue: false,
}),
},
async run(context) {
const queryParams: Record<string, any> = {};
if (context.propsValue['funnel_id']) queryParams['funnel_id'] = context.propsValue['funnel_id'];
if (context.propsValue['location_id']) queryParams['location_id'] = context.propsValue['location_id'];
if (context.propsValue['stage_id']) queryParams['stage_id'] = context.propsValue['stage_id'];
if (context.propsValue['stage']) queryParams['stage'] = context.propsValue['stage'];
if (context.propsValue['labels']) queryParams['labels'] = context.propsValue['labels'];
if (context.propsValue['phone']) queryParams['phone'] = context.propsValue['phone'];
if (context.propsValue['exclude_temporary'] !== undefined) queryParams['exclude_temporary'] = context.propsValue['exclude_temporary'];
if (context.propsValue['per_page']) queryParams['per_page'] = context.propsValue['per_page'];
if (context.propsValue['cursor']) queryParams['cursor'] = context.propsValue['cursor'];
if (context.propsValue['include_subaccounts'] !== undefined) queryParams['include_subaccounts'] = context.propsValue['include_subaccounts'];
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(context.auth, '/applicants'),
headers: getAuthHeaders(context.auth),
queryParams,
});
return response.body;
},
});

View File

@@ -0,0 +1,88 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from '../common/auth';
import { getLocationsDropdown, getUsersDropdown } from '../common/dropdowns';
export const fountainListOpenings = createAction({
name: 'list_openings',
auth: fountainAuth,
displayName: 'List All Opening Details',
description: 'List job openings with optional filters',
props: {
active: Property.Checkbox({
displayName: 'Active Only',
description: 'Filter to only active openings',
required: false,
}),
location_id: Property.Dropdown({
displayName: 'Location',
description: 'Filter openings by location',
required: false,
refreshers: [],
auth: fountainAuth,
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getLocationsDropdown(auth) };
},
}),
is_hiring_funnel: Property.Checkbox({
displayName: 'Hiring Funnels Only',
description: 'Filter to only hiring funnels',
required: false,
}),
is_sourcing_funnel: Property.Checkbox({
displayName: 'Sourcing Funnels Only',
description: 'Filter to only sourcing funnels',
required: false,
}),
is_private: Property.Checkbox({
displayName: 'Private Only',
description: 'Filter to only private openings',
required: false,
}),
owner_id: Property.Dropdown({
displayName: 'Owner',
description: 'Filter openings by owner',
required: false,
refreshers: [],
auth: fountainAuth,
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getUsersDropdown(auth) };
},
}),
per_page: Property.Number({
displayName: 'Results Per Page',
description: 'Number of results per page (default: 25)',
required: false,
defaultValue: 25,
}),
cursor: Property.ShortText({
displayName: 'Cursor',
description: 'Cursor for pagination',
required: false,
}),
},
async run(context) {
const queryParams: Record<string, any> = {};
if (context.propsValue['active'] !== undefined) queryParams['active'] = context.propsValue['active'];
if (context.propsValue['location_id']) queryParams['location_id'] = context.propsValue['location_id'];
if (context.propsValue['is_hiring_funnel'] !== undefined) queryParams['is_hiring_funnel'] = context.propsValue['is_hiring_funnel'];
if (context.propsValue['is_sourcing_funnel'] !== undefined) queryParams['is_sourcing_funnel'] = context.propsValue['is_sourcing_funnel'];
if (context.propsValue['is_private'] !== undefined) queryParams['is_private'] = context.propsValue['is_private'];
if (context.propsValue['owner_id']) queryParams['owner_id'] = context.propsValue['owner_id'];
if (context.propsValue['per_page']) queryParams['per_page'] = context.propsValue['per_page'];
if (context.propsValue['cursor']) queryParams['cursor'] = context.propsValue['cursor'];
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(context.auth, '/funnels'),
headers: getAuthHeaders(context.auth),
queryParams,
});
return response.body;
},
});

View File

@@ -0,0 +1,36 @@
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from '../common/auth';
import { getFunnelsDropdown } from '../common/dropdowns';
export const fountainListStages = createAction({
name: 'list_stages',
auth: fountainAuth,
displayName: 'List All Stages',
description: 'Retrieves an Opening\'s stages',
props: {
funnel_id: Property.Dropdown({
displayName: 'Opening',
description: 'The opening to get stages for',
required: true,
refreshers: [],
auth: fountainAuth,
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getFunnelsDropdown(auth) };
},
}),
},
async run(context) {
const funnelId = context.propsValue.funnel_id;
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(context.auth, `/funnels/${funnelId}/stages`),
headers: getAuthHeaders(context.auth),
});
return response.body;
},
});

View File

@@ -0,0 +1,94 @@
import { AppConnectionValueForAuthProperty, createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from '../common/auth';
async function getApplicantsDropdown(auth: AppConnectionValueForAuthProperty<typeof fountainAuth>): Promise<{ label: string; value: string }[]> {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(auth, '/applicants'),
headers: getAuthHeaders(auth),
queryParams: { per_page: '50' },
});
const applicants = response.body?.applicants || [];
return applicants.map((applicant: any) => ({
label: `${applicant.name} (${applicant.email})`,
value: applicant.id,
}));
} catch (error) {
return [];
}
}
export const fountainUpdateApplicant = createAction({
name: 'update_applicant',
auth: fountainAuth,
displayName: 'Update Applicant Info',
description: 'Update an applicant\'s information',
props: {
id: Property.Dropdown({
displayName: 'Applicant',
description: 'The applicant to update (shows 50 most recent applicants)',
required: true,
refreshers: [],
auth: fountainAuth,
options: async ({ auth }) => {
if (!auth) return { disabled: true, options: [], placeholder: 'Connect account first' };
return { disabled: false, options: await getApplicantsDropdown(auth as any) };
},
}),
name: Property.ShortText({
displayName: 'Name',
description: 'Applicant\'s full name',
required: false,
}),
email: Property.ShortText({
displayName: 'Email',
description: 'Applicant\'s email address',
required: false,
}),
phone_number: Property.ShortText({
displayName: 'Phone Number',
description: 'Applicant\'s complete phone number',
required: false,
}),
data: Property.Object({
displayName: 'Custom Data',
description: 'Custom data fields to update (key-value pairs)',
required: false,
}),
secure_data: Property.Object({
displayName: 'Secure Data',
description: 'Secure data fields to update (key-value pairs like SSN, etc.)',
required: false,
}),
rejection_reason: Property.ShortText({
displayName: 'Rejection Reason',
description: 'Manually set reason for rejection',
required: false,
}),
},
async run(context) {
const applicantId = context.propsValue.id;
const requestBody: Record<string, any> = {};
if (context.propsValue['name']) requestBody['name'] = context.propsValue['name'];
if (context.propsValue['email']) requestBody['email'] = context.propsValue['email'];
if (context.propsValue['phone_number']) requestBody['phone_number'] = context.propsValue['phone_number'];
if (context.propsValue['data']) requestBody['data'] = context.propsValue['data'];
if (context.propsValue['secure_data']) requestBody['secure_data'] = context.propsValue['secure_data'];
if (context.propsValue['rejection_reason']) requestBody['rejection_reason'] = context.propsValue['rejection_reason'];
const response = await httpClient.sendRequest({
method: HttpMethod.PUT,
url: getApiUrl(context.auth, `/applicants/${applicantId}`),
headers: getAuthHeaders(context.auth),
body: requestBody,
});
return response.body;
},
});

View File

@@ -0,0 +1,32 @@
import { fountainAuth, API_BASE_URL_DEFAULT } from '../../';
import { AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
export function getBaseUrl(auth: AppConnectionValueForAuthProperty<typeof fountainAuth>): string {
const baseUrl = auth.props.baseUrl
if (baseUrl) {
let url = baseUrl;
if (!baseUrl.startsWith('http://') && !baseUrl.startsWith('https://')) {
url = `https://${baseUrl}`;
}
return url.replace(/\/$/, '');
}
return API_BASE_URL_DEFAULT;
}
export function getApiUrl(auth: AppConnectionValueForAuthProperty<typeof fountainAuth>, endpoint: string): string {
const baseUrl = getBaseUrl(auth);
if (baseUrl.includes('/api/v2') || baseUrl.endsWith('/v2')) {
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `${baseUrl}${cleanEndpoint}`;
}
const cleanEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
return `${baseUrl}/v2${cleanEndpoint}`;
}
export function getAuthHeaders(auth: AppConnectionValueForAuthProperty<typeof fountainAuth>) {
const apiKey = auth.props.apiKey
return {
'Authorization': `Bearer ${apiKey}`,
'Content-Type': 'application/json',
};
}

View File

@@ -0,0 +1,103 @@
import { PiecePropValueSchema, DropdownOption, AppConnectionValueForAuthProperty } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { fountainAuth } from '../../';
import { getAuthHeaders, getApiUrl } from './auth';
export async function getFunnelsDropdown(auth: AppConnectionValueForAuthProperty<typeof fountainAuth>): Promise<DropdownOption<string>[]> {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(auth, '/funnels'),
headers: getAuthHeaders(auth),
queryParams: { per_page: '100' },
});
const funnels = response.body?.funnels || [];
return funnels.map((funnel: any) => ({
label: funnel.title || `Opening ${funnel.id}`,
value: funnel.id,
}));
} catch (error) {
return [];
}
}
export async function getLocationsDropdown(auth: AppConnectionValueForAuthProperty<typeof fountainAuth>): Promise<DropdownOption<string>[]> {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(auth, '/funnels'),
headers: getAuthHeaders(auth),
queryParams: { per_page: '100' },
});
const funnels = response.body?.funnels || [];
const locations = new Map();
funnels.forEach((funnel: any) => {
if (funnel.location && funnel.location.id && !locations.has(funnel.location.id)) {
locations.set(funnel.location.id, {
label: funnel.location.name || `Location ${funnel.location.id}`,
value: funnel.location.id,
});
}
});
return Array.from(locations.values());
} catch (error) {
return [];
}
}
export async function getUsersDropdown(auth: AppConnectionValueForAuthProperty<typeof fountainAuth>): Promise<DropdownOption<string>[]> {
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(auth, '/funnels'),
headers: getAuthHeaders(auth),
queryParams: { per_page: '100' },
});
const funnels = response.body?.funnels || [];
const users = new Map();
funnels.forEach((funnel: any) => {
if (funnel.owner && funnel.owner.id && !users.has(funnel.owner.id)) {
users.set(funnel.owner.id, {
label: funnel.owner.name || funnel.owner.email || `User ${funnel.owner.id}`,
value: funnel.owner.id,
});
}
});
return Array.from(users.values());
} catch (error) {
return [];
}
}
export async function getStagesForFunnelDropdown(
auth: AppConnectionValueForAuthProperty<typeof fountainAuth>,
funnelId?: string
): Promise<DropdownOption<string>[]> {
if (!funnelId) {
return [];
}
try {
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: getApiUrl(auth, `/funnels/${funnelId}/stages`),
headers: getAuthHeaders(auth),
});
const stages = response.body?.stages || [];
return stages.map((stage: any) => ({
label: stage.title || `Stage ${stage.id}`,
value: stage.id,
}));
} catch (error) {
return [];
}
}

View File

@@ -0,0 +1,84 @@
import {
createTrigger,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { MarkdownVariant } from '@activepieces/shared';
const setupMarkdown = `
## Setup Instructions
1. Go to your Fountain account Settings > Automations
2. Click "Create automation"
3. Select "Applicant" as the source
4. Choose your trigger conditions (e.g., when applicant status changes)
5. Select "Send a webhook..." as the action
6. Copy and paste the webhook URL below into the URL field
7. Set the Authentication key (this will be sent as Authorization header)
8. Make sure to select "Send applicant payload" in the webhook options
**Webhook URL:**
\`\`\`text
{{webhookUrl}}
\`\`\`
`;
export const fountainApplicantWebhook = createTrigger({
name: 'applicant_webhook',
displayName: 'Applicant Webhook',
description: 'Triggers when Fountain sends applicant webhook events',
props: {
setup: Property.MarkDown({
value: setupMarkdown,
variant: MarkdownVariant.INFO,
}),
verify_auth: Property.Checkbox({
displayName: 'Verify Authentication',
description: 'Verify the Authorization header matches the expected key',
required: false,
defaultValue: false,
}),
auth_key: Property.ShortText({
displayName: 'Authentication Key',
description: 'The authentication key set in Fountain webhook configuration',
required: false,
}),
},
sampleData: {
applicant: {
id: "12345",
name: "John Doe",
email: "john.doe@example.com",
phone_number: "+1234567890",
created_at: "2024-01-15T10:30:00Z",
updated_at: "2024-01-15T10:30:00Z",
funnel: {
id: "67890",
title: "Software Engineer Position"
},
stage: {
id: "abc123",
title: "Interview Scheduled"
}
}
},
type: TriggerStrategy.WEBHOOK,
async onEnable() {
// No setup needed - webhook URL is provided for manual configuration in Fountain
},
async onDisable() {
// No cleanup needed
},
async run(context) {
const payload = context.payload.body;
if (context.propsValue.verify_auth && context.propsValue.auth_key) {
const authHeader = context.payload.headers['authorization'];
if (!authHeader || authHeader !== context.propsValue.auth_key) {
throw new Error('Authentication failed');
}
}
return [payload];
},
});

View File

@@ -0,0 +1,89 @@
import {
createTrigger,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { MarkdownVariant } from '@activepieces/shared';
const setupMarkdown = `
## Setup Instructions
1. Go to your Fountain account Settings > Worker Attributes
2. Find the custom attribute you want to monitor
3. Click "..." > Manage Webhooks
4. In the modal, click "Add Webhook"
5. Copy and paste the webhook URL below into the URL field
6. Save the webhook configuration
**Webhook URL:**
\`\`\`text
{{webhookUrl}}
\`\`\`
This trigger will fire whenever the specified custom attribute's value changes for any worker.
`;
export const fountainCustomAttributeWebhook = createTrigger({
name: 'custom_attribute_webhook',
displayName: 'Custom Attribute Webhook',
description: 'Triggers when Fountain worker custom attribute values change',
props: {
setup: Property.MarkDown({
value: setupMarkdown,
variant: MarkdownVariant.INFO,
}),
},
sampleData: {
webhookUuid: "36ea7dc3-2378-4769-a749-54387dea22c6",
companyUuid: "b69527e2-cf03-45e4-b667-48db36133391",
trigger: {
type: "customAttribute",
resourceIdentifier: "583ff255-459d-47b5-9f9f-b3a300c22c02"
},
action: "update",
timestamp: "2024-07-28T13:06:11.443Z",
workerUuid: "3b8d5848-c96a-4639-ac7d-132fe36a43f0",
previousState: {
customAttributeUuid: "583ff255-459d-47b5-9f9f-b3a300c22c02",
customAttribute: {
label: "test",
dataType: "string",
key: "test",
readOnly: false,
hidden: false,
protected: false,
uuid: "583ff255-459d-47b5-9f9f-b3a300c22c02"
},
value: "before"
},
newState: {
customAttributeUuid: "583ff255-459d-47b5-9f9f-b3a300c22c02",
customAttribute: {
label: "test",
dataType: "string",
key: "test",
readOnly: false,
hidden: false,
protected: false,
uuid: "583ff255-459d-47b5-9f9f-b3a300c22c02"
},
value: "after"
}
},
type: TriggerStrategy.WEBHOOK,
async onEnable() {
// No setup needed - webhook URL is provided for manual configuration in Fountain
},
async onDisable() {
// No cleanup needed
},
async run(context) {
const payload = context.payload.body as any;
if (!payload.trigger || payload.trigger.type !== 'customAttribute') {
return [];
}
return [payload];
},
});

View File

@@ -0,0 +1,4 @@
export { fountainApplicantWebhook } from './applicant-webhook';
export { fountainWorkerWebhook } from './worker-webhook';
export { fountainCustomAttributeWebhook } from './custom-attribute-webhook';
export { fountainUniversalTasksWebhook } from './universal-tasks-webhook';

View File

@@ -0,0 +1,84 @@
import {
createTrigger,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { MarkdownVariant } from '@activepieces/shared';
const setupMarkdown = `
## Setup Instructions
1. Go to your Fountain Onboard system
2. Add a Universal Task with Webhook integration
3. Select the trigger type (task completion or flow end)
4. Copy and paste the webhook URL below into the webhook configuration
5. Publish the flow
**Webhook URL:**
\`\`\`text
{{webhookUrl}}
\`\`\`
This trigger will fire when workers complete tasks or reach flow endpoints in Onboard.
`;
export const fountainUniversalTasksWebhook = createTrigger({
name: 'universal_tasks_webhook',
displayName: 'Universal Tasks Webhook',
description: 'Triggers when workers complete tasks or reach flow endpoints in Fountain Onboard',
props: {
setup: Property.MarkDown({
value: setupMarkdown,
variant: MarkdownVariant.INFO,
}),
},
sampleData: {
worker: {
uuid: "b897bd4d-3bd3-4e98-aa94-69cba600c679",
firstName: "Harry",
lastName: "Potter",
startDate: "2024-08-07",
email: "harry.potter+fyd9xrr@example.com",
customAttributes: {
"Compliance Drivers License": {
url: null,
complianceCheckTemplateUuid: "85fb6026-be74-4fe2-9054-f806f577a852"
},
"In Compliance": false,
"Onboard completed": false
},
workerPageUrl: "http://localhost:8200/employer/workers/b897bd4d-3bd3-4e98-aa94-69cba600c679"
},
tasks: [
{
title: "Welcome to the team!",
uuid: "5bed4436-2ef0-4f0f-a49f-539cf7e4671a",
status: "completed",
type: "informationSharing",
data: {
inputData: [],
w4Profile: null,
i9Profile: null,
hirePapiProfile: null
}
}
],
flowTitle: "webhook"
},
type: TriggerStrategy.WEBHOOK,
async onEnable() {
// No setup needed - webhook URL is provided for manual configuration in Fountain
},
async onDisable() {
// No cleanup needed
},
async run(context) {
const payload = context.payload.body as any;
if (!payload.worker || !payload.tasks || !payload.flowTitle) {
return [];
}
return [payload];
},
});

View File

@@ -0,0 +1,93 @@
import {
createTrigger,
Property,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { MarkdownVariant } from '@activepieces/shared';
const setupMarkdown = `
## Setup Instructions
1. Go to your Fountain account Settings > Automations
2. Click "Create automation"
3. Select "Worker" as the source
4. Choose your trigger conditions (e.g., when worker status changes)
5. Select "Send a webhook..." as the action
6. Copy and paste the webhook URL below into the URL field
7. Set the Authentication key (this will be sent as Authorization header)
8. Make sure to select "Send worker payload" in the webhook options
**Webhook URL:**
\`\`\`text
{{webhookUrl}}
\`\`\`
`;
export const fountainWorkerWebhook = createTrigger({
name: 'worker_webhook',
displayName: 'Worker Webhook',
description: 'Triggers when Fountain sends worker webhook events',
props: {
setup: Property.MarkDown({
value: setupMarkdown,
variant: MarkdownVariant.INFO,
}),
verify_auth: Property.Checkbox({
displayName: 'Verify Authentication',
description: 'Verify the Authorization header matches the expected key',
required: false,
defaultValue: false,
}),
auth_key: Property.ShortText({
displayName: 'Authentication Key',
description: 'The authentication key set in Fountain webhook configuration',
required: false,
}),
},
sampleData: {
worker: {
_id: "6916f97536b4690e60234ecf",
createdAt: "2025-11-14T09:42:13.646Z",
firstname: "Test",
lastname: "Profile",
displayFullName: "Test Profile",
personalEmail: {
email: "whatever@example.com"
},
employmentStatus: {
type: "created",
subtype: "employee"
},
customAttributes: [
{
customAttributeUuid: "56606812-8f05-4ea3-8a17-642da842317c",
key: "requirement_drivers_license_status",
value: "OUT"
}
],
company: {
uuid: "4651064c-ac9a-4063-9fe1-1c356843a93c",
name: "Fountain"
}
}
},
type: TriggerStrategy.WEBHOOK,
async onEnable() {
// No setup needed - webhook URL is provided for manual configuration in Fountain
},
async onDisable() {
// No cleanup needed
},
async run(context) {
const payload = context.payload.body;
if (context.propsValue.verify_auth && context.propsValue.auth_key) {
const authHeader = context.payload.headers['authorization'];
if (!authHeader || authHeader !== context.propsValue.auth_key) {
throw new Error('Authentication failed');
}
}
return [payload];
},
});