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,35 @@
import { workableAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { getAccountSubdomain } from '../common/get-subdomain';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
export const getCandidate = createAction({
auth: workableAuth,
name: 'getCandidate',
displayName: 'Get Candidate',
description: "Gets candidate's information.",
props: {
id: Property.ShortText({
displayName: "Candidate's Id",
required: true
})
},
async run(context) {
// Action logic here
const candidateId = context.propsValue.id;
const accessToken = context.auth.secret_text;
const account = await getAccountSubdomain(accessToken);
//get candidate information
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `https://${account}.workable.com/spi/v3/candidates/${candidateId}`,
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
});
return response.body;
},
});

View File

@@ -0,0 +1,39 @@
import { workableAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { getAccountSubdomain } from '../common/get-subdomain';
export const getJob = createAction({
auth: workableAuth,
name: 'getJob',
displayName: 'Get Job',
description: 'Gets specific job deatils.',
props: {
shortcode: Property.ShortText({
displayName: "Shortcode",
description: "Shortcode of specific job",
required: true
})
},
async run(context) {
// Action logic here
const shortcode = context.propsValue?.shortcode;
const accessToken = context.auth.secret_text;
// get account subdomain
const account = await getAccountSubdomain(accessToken);
const url = `https://${account}.workable.com/spi/v3/jobs/${shortcode}`;
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: url,
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
});
return response.body;
},
});

View File

@@ -0,0 +1,79 @@
import { workableAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { getAccountSubdomain } from '../common/get-subdomain';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
export const getMembers = createAction({
auth: workableAuth,
name: 'getMembers',
displayName: 'Get Members',
description: 'Gets members of hiring team.',
props: {
limit: Property.Number({
displayName: "Limit",
description: "Default is 50",
required: false
}),
role: Property.ShortText({
displayName: "Role",
description: "Filter for member of specified role",
required: false
}),
shortcode: Property.ShortText({
displayName: "Shortcode",
description: "Shortcode of specific job",
required: false
}),
email: Property.ShortText({
displayName: "Member's email",
description: "Filter for specific member by email",
required: false
}),
name: Property.ShortText({
displayName: "Member's name",
description: "Filter for members of specified name (Exact Match)",
required: false
}),
},
async run(context) {
// Action logic here
const limit = context.propsValue['limit'];
const role = context.propsValue['role'];
const shortcode = context.propsValue['shortcode'];
const email = context.propsValue['email'];
const name = context.propsValue['name'];
const accessToken = context.auth.secret_text;
const queryParams: Record<string, any> = {};
if(limit !== undefined && limit !== null) {
queryParams["limit"] = limit;
}
if(role && role.trim() !== ''){
queryParams["role"] = role;
}
if(shortcode && shortcode.trim() !== ""){
queryParams["shortcode"] = shortcode;
}
if(email && email.trim() !== ""){
queryParams["email"] = email;
}
if(name && name.trim() !== ''){
queryParams["name"] = name;
}
const subdomain = await getAccountSubdomain(accessToken);
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `https://${subdomain}.workable.com/spi/v3/members`,
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
},
queryParams
})
return response.body;
},
});

View File

@@ -0,0 +1,35 @@
import { workableAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { getAccountSubdomain } from '../common/get-subdomain';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
export const getStages = createAction({
auth: workableAuth,
name: 'getStages',
displayName: 'Get Stages',
description: 'Gets stages in your recruitment pipeline stages.',
props: {
shortcode: Property.ShortText({
displayName: "Shortcode",
description: "Shortcode of specific job",
required: true
})
},
async run(context) {
// Action logic here
const shortcode = context.propsValue.shortcode;
const accessToken = context.auth.secret_text;
const account = await getAccountSubdomain(accessToken);
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `https://${account}.workable.com/spi/v3/jobs/${shortcode}/stages`,
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
}
});
return response.body;
},
});

View File

@@ -0,0 +1,54 @@
import { workableAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { getAccountSubdomain } from '../common/get-subdomain';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
export const moveCandidate = createAction({
auth: workableAuth,
name: 'moveCandidate',
displayName: 'Move Candidate',
description: 'Moves candidate to the specified stage.',
props: {
id: Property.ShortText({
displayName: "Candidate's Id",
description: "Id of candidate",
required: true
}),
member_id: Property.ShortText({
displayName: "Member's Id",
description: "This person's Id is used to move the candidate to the next stage",
required: true
}),
target_stage: Property.ShortText({
displayName: "Target stage name",
description: "Slug of stage target should be moved to",
required: true
})
},
async run(context) {
// Action logic here
const id = context.propsValue.id;
const member_id = context.propsValue.member_id;
const target_stage = context.propsValue.target_stage;
const accessToken = context.auth.secret_text;
const subdomain = await getAccountSubdomain(accessToken);
const payload: Record<string, any> = {
member_id: member_id,
target_stage: target_stage
}
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `https://${subdomain}.workable.com/spi/v3/candidates/${id}/move`,
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
},
body: payload
})
return response;
},
});

View File

@@ -0,0 +1,76 @@
import { workableAuth } from '../../index';
import { createAction, Property } from '@activepieces/pieces-framework';
import { getAccountSubdomain } from '../common/get-subdomain';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
export const rateCandidate = createAction({
auth: workableAuth,
name: 'rateCandidate',
displayName: 'Rate candidate',
description: 'Rates the candidate on workable.',
props: {
id: Property.ShortText({
displayName: "Candidate's Id",
description: "Id of candidate",
required: true
}),
member_id: Property.ShortText({
displayName: "Member's Id",
description: "Id of member that is adding the rating",
required: true
}),
comment: Property.LongText({
displayName: "Comment",
description: "Comment about the scoring of the candidate",
required: true
}),
scale: Property.StaticDropdown({
displayName: "Rating scale",
description: "Select scale to rate candidate on",
required: true,
options: {
options: [
{label: "Thumbs", value: "thumbs"},
{label: "Stars", value: "stars"},
{label: "Numbers", value: "numbers"}
]
}
}),
grade: Property.Number({
displayName: "Grade",
description: "Thumbs scale: 0-2, Stars scale: 0-4, Numbers scale: 0-9",
required: true
})
},
async run(context) {
// Action logic here
const id = context.propsValue.id;
const member_id = context.propsValue.member_id;
const comment = context.propsValue.comment;
const scale = context.propsValue.scale;
const grade = context.propsValue.grade;
const accessToken = context.auth.secret_text;
const subdomain = await getAccountSubdomain(accessToken);
const body: Record<string, any> = {
comment,
scale,
member_id,
grade
};
const response = await httpClient.sendRequest({
method: HttpMethod.POST,
url: `https://${subdomain}.workable.com/spi/v3/candidates/${id}/ratings`,
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
},
body: body
})
return response;
},
});

View File

@@ -0,0 +1,15 @@
import { httpClient, HttpMethod } from "@activepieces/pieces-common";
export async function getAccountSubdomain(accessToken: string){
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: "https://workable.com/spi/v3/accounts",
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json'
}
});
console.log("this is response" + response.body.accounts[0].subdomain);
return response.body.accounts[0]?.subdomain;
}

View File

@@ -0,0 +1,45 @@
import { HttpMethod, httpClient, HttpRequest} from '@activepieces/pieces-common';
export const workableCommon = {
subscribeWebhook: async (
subdomain: string,
accessToken: string,
webhookUrl: string,
event: string,
args: Record<string, any> = {}
) => {
const request: HttpRequest = {
method: HttpMethod.POST,
url: `https://${subdomain}.workable.com/spi/v3/subscriptions`,
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
},
body: {
target: webhookUrl,
event,
args,
}
};
const response = await httpClient.sendRequest<{id: string}>(request);
return response.body;
},
unsubscribeWebhook: async (
subdomain: string,
accessToken: string,
subscriptionId: string
) => {
const request: HttpRequest = {
method: HttpMethod.DELETE,
url: `https://${subdomain}.workable.com/spi/v3/subscriptions/${subscriptionId}`,
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json'
}
}
return await httpClient.sendRequest(request);
}
}

View File

@@ -0,0 +1,125 @@
import { workableAuth } from '../../index';
import { createTrigger, Property, TriggerStrategy, WebhookResponse } from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { getAccountSubdomain } from '../common/get-subdomain';
import { workableCommon } from '../common/webhooks';
interface WebhookInformation {
webhookId: string;
}
interface WorkableWebhookPayload {
data: any;
event_type: string;
fired_at: string;
id: string;
resource_type: string;
}
export const newCandidate = createTrigger({
auth: workableAuth,
name: 'newCandidate',
displayName: 'New Candidate',
description: 'Triggers when new candidate submits application. Can be filtered by specific job and/or hiring pipeline stage.',
props: {
shortcode: Property.ShortText({
displayName: "Shortcode",
description: "Shortcode of specific job. Leave empty to trigger for all jobs.",
required: false
}),
stage_slug: Property.ShortText({
displayName: "Stage Slug",
description: "Stage slug to filter by specific hiring pipeline stage (e.g., 'applied', 'phone_screen', 'interview', 'offer'). Leave empty to trigger for all stages.",
required: false
})
},
sampleData: {
id: '123',
name: 'John Doe',
firstname: 'John',
lastname: 'Doe',
headline: 'Software Engineer',
account: {
subdomain: 'example',
name: 'Example Inc.'
},
job: {
shortcode: 'ENG123',
title: 'Software Engineer'
},
stage: 'applied',
disqualified: false,
disqualification_reason: '',
sourced: false,
profile_url: 'https://example.workable.com/candidates/123',
email: 'john.doe@example.com',
domain: 'example.com',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
},
type: TriggerStrategy.WEBHOOK,
async onEnable(context){
const accessToken = context.auth.secret_text;
const subdomain = await getAccountSubdomain(accessToken);
const shortcode = context.propsValue.shortcode || '';
const stageSlug = context.propsValue.stage_slug || '';
const webhookUrl = context.webhookUrl;
const event = "candidate_created";
const subscription = await workableCommon.subscribeWebhook(
subdomain,
accessToken,
webhookUrl,
event,
{job_shortcode: shortcode, stage_slug: stageSlug}
);
await context.store?.put<WebhookInformation>('_new_candidate_created', {
webhookId: subscription.id,
})
},
async onDisable(context){
// implement webhook deletion logic
const accessToken = context.auth.secret_text;
const webhookInfo = await context.store.get<WebhookInformation>('_new_candidate_created');
if(webhookInfo?.webhookId) {
const subdomain = await getAccountSubdomain(accessToken);
await workableCommon.unsubscribeWebhook(subdomain, accessToken, webhookInfo.webhookId);
}
},
async test(context){
const accessToken = context.auth.secret_text;
const subdomain = await getAccountSubdomain(accessToken);
const shortcode = context.propsValue.shortcode || '';
const stageSlug = context.propsValue.stage_slug || '';
const queryParams: any = {};
if (shortcode) {
queryParams.shortcode = shortcode;
}
if (stageSlug) {
queryParams.stage = stageSlug;
}
const response = await httpClient.sendRequest({
method: HttpMethod.GET,
url: `https://${subdomain}.workable.com/spi/v3/candidates/`,
headers: {
Authorization: `Bearer ${accessToken}`,
Accept: "application/json"
},
queryParams: queryParams
});
const candidates = response.body.candidates?.slice(0, 3) ?? [];
return candidates;
},
async run(context){
const payload = context.payload.body as WorkableWebhookPayload;
return [payload.data]
}
})