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:
@@ -0,0 +1,85 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { assembledCommon } from '../common';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { assembledAuth } from '../common/auth';
|
||||
|
||||
export const addShift = createAction({
|
||||
auth: assembledAuth,
|
||||
name: 'add_shift',
|
||||
displayName: 'Add Shift on Assembled',
|
||||
description: 'Add a new shift to a user\'s schedule in Assembled',
|
||||
props: {
|
||||
agent_id: Property.ShortText({
|
||||
displayName: 'Agent ID',
|
||||
description: 'Agent ID of the person to assign the shift to (use the agent_id field from /people endpoint, not the person id)',
|
||||
required: true,
|
||||
}),
|
||||
start_time: Property.DateTime({
|
||||
displayName: 'Start Time',
|
||||
description: 'Start time of the shift',
|
||||
required: true,
|
||||
}),
|
||||
end_time: Property.DateTime({
|
||||
displayName: 'End Time',
|
||||
description: 'End time of the shift',
|
||||
required: true,
|
||||
}),
|
||||
shift_type: Property.StaticDropdown({
|
||||
displayName: 'Shift Type',
|
||||
description: 'Type of shift',
|
||||
required: false,
|
||||
defaultValue: 'regular',
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Regular', value: 'regular' },
|
||||
{ label: 'Overtime', value: 'overtime' },
|
||||
{ label: 'On-call', value: 'on_call' },
|
||||
{ label: 'Training', value: 'training' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
activity_type_id: Property.ShortText({
|
||||
displayName: 'Activity Type ID',
|
||||
description: 'UUID of the activity type for this shift (get from /activity_types endpoint)',
|
||||
required: true,
|
||||
}),
|
||||
notes: Property.LongText({
|
||||
displayName: 'Notes',
|
||||
description: 'Additional notes for the shift',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { agent_id, start_time, end_time, shift_type, activity_type_id, notes } = context.propsValue;
|
||||
|
||||
try {
|
||||
// Convert to Unix timestamps as required by Assembled API
|
||||
const startTimestamp = Math.floor(new Date(start_time).getTime() / 1000);
|
||||
const endTimestamp = Math.floor(new Date(end_time).getTime() / 1000);
|
||||
|
||||
const shiftData = {
|
||||
agent_id: agent_id,
|
||||
type_id: activity_type_id,
|
||||
start_time: startTimestamp,
|
||||
end_time: endTimestamp,
|
||||
description: notes || `${shift_type || 'regular'} shift`,
|
||||
};
|
||||
|
||||
const response = await assembledCommon.makeRequest(
|
||||
context.auth.secret_text,
|
||||
HttpMethod.POST,
|
||||
'/activities',
|
||||
shiftData
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
shift_id: response.body.id,
|
||||
message: 'Shift added successfully',
|
||||
data: response.body,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to add shift: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,14 @@
|
||||
import { createCustomApiCallAction } from '@activepieces/pieces-common';
|
||||
import { assembledAuth } from '../common/auth';
|
||||
|
||||
export const customApiCall = createCustomApiCallAction({
|
||||
auth: assembledAuth,
|
||||
name: 'custom_api_call',
|
||||
displayName: 'Custom API Call',
|
||||
description: 'Make custom API calls to Assembled endpoints',
|
||||
baseUrl: () => 'https://api.assembledhq.com/v0',
|
||||
authMapping: async (auth) => ({
|
||||
'Authorization': `Basic ${Buffer.from(auth.secret_text + ':').toString('base64')}`,
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { assembledCommon } from '../common';
|
||||
import { assembledAuth } from '../common/auth';
|
||||
|
||||
export const customGraphql = createAction({
|
||||
name: 'custom_graphql',
|
||||
displayName: 'Custom GraphQL',
|
||||
description: 'Perform a custom GraphQL query',
|
||||
auth: assembledAuth,
|
||||
props: {
|
||||
query: Property.LongText({ displayName: 'Query', required: true }),
|
||||
variables: Property.Object({ displayName: 'Parameters', required: false }),
|
||||
},
|
||||
async run({ auth, propsValue }) {
|
||||
const client = assembledCommon.makeClient(auth.secret_text);
|
||||
const result = await client.rawRequest(
|
||||
propsValue.query,
|
||||
propsValue.variables
|
||||
);
|
||||
return result;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,33 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { assembledCommon } from '../common';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { assembledAuth } from '../common/auth';
|
||||
|
||||
export const deleteOOO = createAction({
|
||||
auth: assembledAuth,
|
||||
name: 'delete_OOO',
|
||||
displayName: 'Delete OOO Request',
|
||||
description: 'Cancel/delete a OOO request.',
|
||||
props: {
|
||||
OOO_id: Property.ShortText({
|
||||
displayName: 'OOO ID',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { OOO_id } = context.propsValue;
|
||||
|
||||
const response = await assembledCommon.makeRequest(
|
||||
context.auth.secret_text,
|
||||
HttpMethod.POST,
|
||||
`/time_off/${OOO_id}/cancel`,
|
||||
{}
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'OOO request deleted successfully',
|
||||
data: response.body,
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,41 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { assembledCommon } from '../common';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { assembledAuth } from '../common/auth';
|
||||
|
||||
export const getUserSchedule = createAction({
|
||||
auth: assembledAuth,
|
||||
name: 'get_user_schedule',
|
||||
displayName: 'Get User Schedule',
|
||||
description: 'Retrieves user\'s schedule for specified period.',
|
||||
props: {
|
||||
user_id: Property.ShortText({
|
||||
displayName: 'User ID',
|
||||
required: true,
|
||||
}),
|
||||
start_date: Property.DateTime({
|
||||
displayName: 'Start Date',
|
||||
required: true,
|
||||
}),
|
||||
end_date: Property.DateTime({
|
||||
displayName: 'End Date',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { user_id, start_date, end_date } = context.propsValue;
|
||||
|
||||
const params = new URLSearchParams({
|
||||
start_date: assembledCommon.formatDate(start_date),
|
||||
end_date: assembledCommon.formatDate(end_date),
|
||||
});
|
||||
|
||||
const response = await assembledCommon.makeRequest(
|
||||
context.auth.secret_text,
|
||||
HttpMethod.GET,
|
||||
`/users/${user_id}/schedule?${params.toString()}`
|
||||
);
|
||||
|
||||
return response.body;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,97 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { assembledCommon } from '../common';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { assembledAuth } from '../common/auth';
|
||||
|
||||
export const OOO = createAction({
|
||||
auth: assembledAuth,
|
||||
name: 'OOO',
|
||||
displayName: 'Create OOO Request',
|
||||
description: 'Create an Out of Office request in Assembled.',
|
||||
props: {
|
||||
mock_mode: Property.Checkbox({
|
||||
displayName: 'Mock Mode',
|
||||
description: 'Use mock data for testing',
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
}),
|
||||
user_id: Property.ShortText({
|
||||
displayName: 'User ID',
|
||||
description: 'ID of the user requesting time off',
|
||||
required: true,
|
||||
}),
|
||||
start_date: Property.DateTime({
|
||||
displayName: 'Start Date',
|
||||
description: 'Start date of the OOO period',
|
||||
required: true,
|
||||
}),
|
||||
end_date: Property.DateTime({
|
||||
displayName: 'End Date',
|
||||
description: 'End date of the OOO period',
|
||||
required: true,
|
||||
}),
|
||||
activity_type_id: Property.ShortText({
|
||||
displayName: 'Activity Type ID',
|
||||
description: 'UUID of the activity type for time off (can be retrieved from activity types endpoints)',
|
||||
required: true,
|
||||
}),
|
||||
all_day: Property.Checkbox({
|
||||
displayName: 'All Day Event',
|
||||
description: 'Whether this is an all-day OOO event',
|
||||
required: false,
|
||||
defaultValue: true,
|
||||
}),
|
||||
reason: Property.LongText({
|
||||
displayName: 'Reason',
|
||||
description: 'Reason for the OOO request',
|
||||
required: false,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { mock_mode, user_id, start_date, end_date, activity_type_id, all_day, reason } = context.propsValue;
|
||||
|
||||
// Mock response for testing
|
||||
if (mock_mode) {
|
||||
return {
|
||||
success: true,
|
||||
message: 'Mock OOO created successfully',
|
||||
data: {
|
||||
id: `mock_ooo_${Date.now()}`,
|
||||
user_id,
|
||||
start_date,
|
||||
end_date,
|
||||
activity_type_id: activity_type_id || 'mock-activity-type-id',
|
||||
status: 'pending',
|
||||
created_at: new Date().toISOString(),
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const oooData = {
|
||||
user_id: user_id,
|
||||
start_time: Math.floor(new Date(start_date).getTime() / 1000),
|
||||
end_time: Math.floor(new Date(end_date).getTime() / 1000),
|
||||
activity_type_id,
|
||||
all_day: all_day ?? true,
|
||||
description: reason || '',
|
||||
};
|
||||
|
||||
const response = await assembledCommon.makeRequest(
|
||||
context.auth.secret_text,
|
||||
HttpMethod.POST,
|
||||
'/time_off',
|
||||
oooData
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
ooo_id: response.body.id,
|
||||
message: 'OOO request created successfully',
|
||||
data: response.body,
|
||||
};
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to create OOO request: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,95 @@
|
||||
import { createAction, Property } from '@activepieces/pieces-framework';
|
||||
import { assembledCommon } from '../common';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { assembledAuth } from '../common/auth';
|
||||
|
||||
export const updateOOO = createAction({
|
||||
auth: assembledAuth,
|
||||
name: 'update_OOO',
|
||||
displayName: 'Update OOO Request',
|
||||
description: 'Updates an existing OOO request.',
|
||||
props: {
|
||||
OOO_id: Property.ShortText({
|
||||
displayName: 'OOO ID',
|
||||
required: true,
|
||||
}),
|
||||
start_date: Property.DateTime({
|
||||
displayName: 'Start Date',
|
||||
required: false,
|
||||
}),
|
||||
end_date: Property.DateTime({
|
||||
displayName: 'End Date',
|
||||
required: false,
|
||||
}),
|
||||
status: Property.StaticDropdown({
|
||||
displayName: 'Status',
|
||||
required: false,
|
||||
options: {
|
||||
options: [
|
||||
{ label: 'Pending', value: 'pending' },
|
||||
{ label: 'Approved', value: 'approved' },
|
||||
{ label: 'Rejected', value: 'rejected' },
|
||||
{ label: 'Cancelled', value: 'cancelled' },
|
||||
],
|
||||
},
|
||||
}),
|
||||
reason: Property.LongText({
|
||||
displayName: 'Reason',
|
||||
required: false,
|
||||
}),
|
||||
user_id: Property.ShortText({
|
||||
displayName: 'User ID',
|
||||
description: 'ID of the user for the time off request (required for creating new request)',
|
||||
required: true,
|
||||
}),
|
||||
activity_type_id: Property.ShortText({
|
||||
displayName: 'Activity Type ID',
|
||||
description: 'UUID of the activity type for time off (required for creating new request)',
|
||||
required: true,
|
||||
}),
|
||||
},
|
||||
async run(context) {
|
||||
const { OOO_id, start_date, end_date, status, reason, user_id, activity_type_id } = context.propsValue;
|
||||
|
||||
// no direct endpoint to update a time off request, so need to cancel and create new time off request
|
||||
try {
|
||||
// cancel the existing time off request
|
||||
console.log(`Canceling existing time off request: ${OOO_id}`);
|
||||
await assembledCommon.makeRequest(
|
||||
context.auth.secret_text,
|
||||
HttpMethod.POST,
|
||||
`/time_off/${OOO_id}/cancel`
|
||||
);
|
||||
|
||||
// create new time off request with updated details
|
||||
console.log('Creating new time off request with updated details');
|
||||
const newRequestData: Record<string, unknown> = {
|
||||
user_id,
|
||||
activity_type_id,
|
||||
all_day: true,
|
||||
};
|
||||
|
||||
if (start_date) newRequestData['start_time'] = Math.floor(new Date(start_date).getTime() / 1000);
|
||||
if (end_date) newRequestData['end_time'] = Math.floor(new Date(end_date).getTime() / 1000);
|
||||
if (reason) newRequestData['description'] = reason;
|
||||
|
||||
const response = await assembledCommon.makeRequest(
|
||||
context.auth.secret_text,
|
||||
HttpMethod.POST,
|
||||
'/time_off',
|
||||
newRequestData
|
||||
);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
message: 'Time off request updated successfully (canceled old and created new)',
|
||||
canceled_request_id: OOO_id,
|
||||
new_request_id: response.body.id,
|
||||
data: response.body,
|
||||
};
|
||||
|
||||
} catch (error) {
|
||||
throw new Error(`Failed to update time off request: ${error instanceof Error ? error.message : 'Unknown error'}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { PieceAuth } from '@activepieces/pieces-framework';
|
||||
|
||||
export const assembledAuth = PieceAuth.SecretText({
|
||||
displayName: 'API Key',
|
||||
description: `You can obtain API key by navigating to [Settings->API](https://app.assembledhq.com/settings/api) page.`,
|
||||
required: true,
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
|
||||
|
||||
export const assembledCommon = {
|
||||
baseUrl: 'https://api.assembledhq.com/v0',
|
||||
|
||||
async makeRequest(
|
||||
auth: string,
|
||||
method: HttpMethod,
|
||||
endpoint: string,
|
||||
body?: unknown,
|
||||
headers?: Record<string, string>
|
||||
) {
|
||||
const url = endpoint.startsWith('http') ? endpoint : `${this.baseUrl}${endpoint}`;
|
||||
|
||||
return await httpClient.sendRequest({
|
||||
method,
|
||||
url,
|
||||
headers: {
|
||||
'Authorization': `Basic ${Buffer.from(auth + ':').toString('base64')}`,
|
||||
'Content-Type': 'application/json',
|
||||
...headers,
|
||||
},
|
||||
body,
|
||||
});
|
||||
},
|
||||
|
||||
formatDateTime(date: string | Date): string {
|
||||
return new Date(date).toISOString();
|
||||
},
|
||||
|
||||
formatDate(date: string | Date): string {
|
||||
return new Date(date).toISOString().split('T')[0];
|
||||
},
|
||||
|
||||
makeClient(auth: string) {
|
||||
return {
|
||||
rawRequest: async (query: string, variables?: Record<string, unknown>) => {
|
||||
const response = await this.makeRequest(
|
||||
auth,
|
||||
HttpMethod.POST,
|
||||
'/graphql',
|
||||
{ query, variables }
|
||||
);
|
||||
return response.body;
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,64 @@
|
||||
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
|
||||
import { assembledCommon } from '../common';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { assembledAuth } from '../common/auth';
|
||||
|
||||
export const timeOffStatusChanged = createTrigger({
|
||||
auth: assembledAuth,
|
||||
name: 'OOO_status_changed',
|
||||
displayName: 'OOO Status Changed',
|
||||
description: 'Triggers on approval/rejection of OOO.',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {},
|
||||
sampleData: {
|
||||
id: '<uuid>',
|
||||
time_off_request_id: '<uuid>',
|
||||
created_at: 1546303260,
|
||||
comment: 'Enjoy your vacation',
|
||||
type: 'approve',
|
||||
time_off_request: {
|
||||
id: '<uuid>',
|
||||
agent_id: '<uuid>',
|
||||
start_time: 1546303260,
|
||||
end_time: 1546303270,
|
||||
created_at: 1546303260,
|
||||
description: 'Going to the dentist',
|
||||
status: 'approved',
|
||||
activity_type_id: '<uuid>'
|
||||
}
|
||||
},
|
||||
async onEnable(context) {
|
||||
await context.store.put('lastStatusCheck', Math.floor(Date.now() / 1000));
|
||||
},
|
||||
async onDisable() {
|
||||
// No cleanup needed
|
||||
},
|
||||
async run(context) {
|
||||
const lastCheck = await context.store.get('lastStatusCheck') || Math.floor(Date.now() / 1000) - 86400; // 24 hours ago in Unix timestamp
|
||||
|
||||
const response = await assembledCommon.makeRequest(
|
||||
context.auth.secret_text,
|
||||
HttpMethod.GET,
|
||||
`/time_off/updates?updated_since=${lastCheck}&type=approve`
|
||||
);
|
||||
|
||||
// Handle the documented response structure
|
||||
const timeOffUpdates = response.body.time_off_updates || {};
|
||||
const timeOffRequests = response.body.time_off_requests || {};
|
||||
|
||||
// Transform the response to include both update and request data
|
||||
const statusChanges = Object.values(timeOffUpdates).map((update: any) => {
|
||||
const request = timeOffRequests[update.time_off_request_id];
|
||||
return {
|
||||
...update,
|
||||
time_off_request: request
|
||||
};
|
||||
});
|
||||
|
||||
if (statusChanges.length > 0) {
|
||||
await context.store.put('lastStatusCheck', Math.floor(Date.now() / 1000));
|
||||
}
|
||||
|
||||
return statusChanges;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
|
||||
import { assembledCommon } from '../common';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { assembledAuth } from '../common/auth';
|
||||
|
||||
export const newTimeOffRequest = createTrigger({
|
||||
auth: assembledAuth,
|
||||
name: 'new_OOO_request',
|
||||
displayName: 'New OOO Request',
|
||||
description: 'Triggers when a new OOO request is created.',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {},
|
||||
sampleData: {
|
||||
id: '<uuid>',
|
||||
agent_id: '<uuid>',
|
||||
start_time: 1546303260,
|
||||
end_time: 1546303270,
|
||||
created_at: 1546303260,
|
||||
description: 'Going to the dentist',
|
||||
status: 'approved',
|
||||
activity_type_id: '<uuid>',
|
||||
},
|
||||
async onEnable(context) {
|
||||
await context.store.put('lastCheck', Math.floor(Date.now() / 1000));
|
||||
},
|
||||
async onDisable() {
|
||||
// Cleanup if needed
|
||||
},
|
||||
async run(context) {
|
||||
const lastCheck = await context.store.get('lastCheck') || Math.floor(Date.now() / 1000) - 86400;
|
||||
|
||||
const response = await assembledCommon.makeRequest(
|
||||
context.auth.secret_text,
|
||||
HttpMethod.GET,
|
||||
`/time_off/requests?updated_since=${lastCheck}&limit=100`
|
||||
);
|
||||
|
||||
// Handle the documented response structure
|
||||
const timeOffRequests = response.body.time_off_requests || {};
|
||||
const newRequests = Object.values(timeOffRequests);
|
||||
|
||||
if (newRequests.length > 0) {
|
||||
await context.store.put('lastCheck', Math.floor(Date.now() / 1000));
|
||||
}
|
||||
|
||||
return newRequests;
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
import { createTrigger, TriggerStrategy } from '@activepieces/pieces-framework';
|
||||
import { assembledCommon } from '../common';
|
||||
import { HttpMethod } from '@activepieces/pieces-common';
|
||||
import { assembledAuth } from '../common/auth';
|
||||
|
||||
export const scheduleUpdated = createTrigger({
|
||||
auth: assembledAuth,
|
||||
name: 'schedule_updated',
|
||||
displayName: 'Schedule Updated',
|
||||
description: 'Triggers when user schedule is modified.',
|
||||
type: TriggerStrategy.POLLING,
|
||||
props: {},
|
||||
sampleData: {
|
||||
user_id: 'user_456',
|
||||
schedule_id: 'sched_123',
|
||||
changes: ['shift_added', 'shift_modified'],
|
||||
updated_at: '2025-01-15T10:00:00Z',
|
||||
},
|
||||
async onEnable(context) {
|
||||
await context.store.put('lastScheduleCheck', new Date().toISOString());
|
||||
},
|
||||
async onDisable() {
|
||||
// Cleanup if needed
|
||||
},
|
||||
async run(context) {
|
||||
const lastCheck = await context.store.get('lastScheduleCheck') || new Date(Date.now() - 60 * 60 * 1000).toISOString();
|
||||
|
||||
const response = await assembledCommon.makeRequest(
|
||||
context.auth.secret_text,
|
||||
HttpMethod.GET,
|
||||
`/events?type=schedule_updated&after=${lastCheck}&limit=100`
|
||||
);
|
||||
|
||||
const scheduleUpdates = response.body.data || [];
|
||||
|
||||
if (scheduleUpdates.length > 0) {
|
||||
await context.store.put('lastScheduleCheck', new Date().toISOString());
|
||||
}
|
||||
|
||||
return scheduleUpdates;
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user