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,153 @@
import {
createAction,
DynamicPropsValue,
PieceAuth,
Property,
} from '@activepieces/pieces-framework';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { ExecutionType, FAIL_PARENT_ON_FAILURE_HEADER, isNil, PauseType, PARENT_RUN_ID_HEADER } from '@activepieces/shared';
import { CallableFlowRequest, CallableFlowResponse, findFlowByExternalIdOrThrow, listEnabledFlowsWithSubflowTrigger } from '../common';
type FlowValue = {
externalId: string;
exampleData: unknown;
};
export const callFlow = createAction({
name: 'callFlow',
displayName: 'Call Flow',
description: 'Call a flow that has "Callable Flow" trigger',
props: {
flow: Property.Dropdown<FlowValue>({
auth: PieceAuth.None(),
displayName: 'Flow',
description: 'The flow to execute',
required: true,
options: async (_, context) => {
const flows = await listEnabledFlowsWithSubflowTrigger({
flowsContext: context.flows,
});
return {
options: flows.map((flow) => ({
value: {
externalId: flow.externalId ?? flow.id,
exampleData: flow.version.trigger.settings.input.exampleData,
},
label: flow.version.displayName,
})),
};
},
refreshers: [],
}),
mode: Property.StaticDropdown({
displayName: 'Mode',
required: true,
description: 'Choose Simple for key-value or Advanced for JSON.',
defaultValue: 'simple',
options: {
disabled: false,
options: [
{
label: 'Simple',
value: 'simple',
},
{
label: 'Advanced',
value: 'advanced',
},
],
},
}),
flowProps: Property.DynamicProperties({
auth: PieceAuth.None(),
description: '',
displayName: '',
required: true,
refreshers: ['flow', 'mode'],
props: async (propsValue) => {
const castedFlowValue = propsValue['flow'] as unknown as FlowValue;
const mode = propsValue['mode'] as unknown as string;
const fields: DynamicPropsValue = {};
if (!isNil(castedFlowValue)) {
if (mode === 'simple') {
fields['payload'] = Property.Object({
displayName: 'Payload',
required: true,
defaultValue: (castedFlowValue.exampleData as unknown as { sampleData: object }).sampleData,
});
}
else{
fields['payload'] = Property.Json({
displayName: 'Payload',
description:
'Provide the data to be passed to the flow',
required: true,
defaultValue: (castedFlowValue.exampleData as unknown as { sampleData: object }).sampleData,
});
}
}
return fields;
},
}),
waitForResponse: Property.Checkbox({
displayName: 'Wait for Response',
required: false,
defaultValue: false,
}),
},
async run(context) {
if (context.executionType === ExecutionType.RESUME) {
const response = context.resumePayload.body as CallableFlowResponse;
const shouldFailParentRun = response.status === 'error' && context.propsValue.waitForResponse
if (shouldFailParentRun) {
throw new Error(JSON.stringify(response.data, null, 2))
}
return {
status: response.status,
data: response.data
}
}
const payload = context.propsValue.flowProps['payload'];
const flow = await findFlowByExternalIdOrThrow({
flowsContext: context.flows,
externalId: context.propsValue.flow?.externalId,
});
const response = await httpClient.sendRequest<CallableFlowRequest>({
method: HttpMethod.POST,
url: `${context.server.apiUrl}v1/webhooks/${flow?.id}`,
headers: {
'Content-Type': 'application/json',
[PARENT_RUN_ID_HEADER]: context.run.id,
[FAIL_PARENT_ON_FAILURE_HEADER]: context.propsValue.waitForResponse ? 'true' : 'false',
},
body: {
data: payload,
callbackUrl: context.propsValue.waitForResponse ? context.generateResumeUrl({
queryParams: {}
}) : undefined,
},
});
if (context.propsValue.waitForResponse) {
context.run.pause({
pauseMetadata: {
type: PauseType.WEBHOOK,
response: {},
}
})
}
return response.body;
},
errorHandlingOptions: {
continueOnFailure: {
defaultValue:false,
hide:false,
},
retryOnFailure: {
defaultValue:false,
hide:false,
}
}
});

View File

@@ -0,0 +1,74 @@
import { DynamicPropsValue, PieceAuth, Property, StoreScope, createAction } from '@activepieces/pieces-framework';
import { callableFlowKey, CallableFlowResponse, MOCK_CALLBACK_IN_TEST_FLOW_URL } from '../common';
import { httpClient, HttpMethod } from '@activepieces/pieces-common';
import { isNil } from '@activepieces/shared';
export const response = createAction({
name: 'returnResponse',
displayName: 'Return Response',
description: 'Return response to the original flow',
props: {
mode: Property.StaticDropdown({
displayName: 'Mode',
description: 'Choose Simple for key-value or Advanced for JSON.',
required: true,
defaultValue: 'simple',
options: {
disabled: false,
options: [
{
label: 'Simple',
value: 'simple',
},
{
label: 'Advanced',
value: 'advanced',
},
],
},
}),
response: Property.DynamicProperties({
auth: PieceAuth.None(),
displayName: 'Response',
required: true,
refreshers: ['mode'],
props: async (propsValue) => {
const mode = propsValue['mode'] as unknown as string;
const fields: DynamicPropsValue = {};
if (mode === 'simple') {
fields['response'] = Property.Object({
displayName: 'Response',
required: true,
});
} else {
fields['response'] = Property.Json({
displayName: 'Response',
required: true,
});
}
return fields;
},
}),
},
async test(context) {
return context.propsValue.response['response'];
},
async run(context) {
const response = context.propsValue.response['response'];
const callbackUrl = await context.store.get<string>(callableFlowKey(context.run.id), StoreScope.FLOW);
const isNotTestFlow = callbackUrl !== MOCK_CALLBACK_IN_TEST_FLOW_URL;
if (isNotTestFlow && !isNil(callbackUrl)) {
await httpClient.sendRequest<CallableFlowResponse>({
method: HttpMethod.POST,
url: callbackUrl,
body: {
status: 'success',
data: response
},
retries: 10,
});
}
return response;
},
});

View File

@@ -0,0 +1,64 @@
import { FlowStatus, FlowTriggerType, isNil, PopulatedFlow } from "@activepieces/shared";
import { FlowsContext, ListFlowsContextParams } from "@activepieces/pieces-framework";
export const callableFlowKey = (runId: string) => `callableFlow_${runId}`;
export type CallableFlowRequest = {
data: unknown;
callbackUrl: string;
}
export type CallableFlowResponse = {
status: 'success' | 'error';
data: unknown;
}
export const MOCK_CALLBACK_IN_TEST_FLOW_URL = 'MOCK';
export async function listEnabledFlowsWithSubflowTrigger({
flowsContext,
params,
}: ListParams) {
const allFlows = (await flowsContext.list(params)).data;
const flows = allFlows.filter(
(flow) =>
flow.status === FlowStatus.ENABLED &&
flow.version.trigger.type === FlowTriggerType.PIECE &&
flow.version.trigger.settings.pieceName ==
'@activepieces/piece-subflows'
);
return flows;
}
export async function findFlowByExternalIdOrThrow({
flowsContext,
externalId,
}: {
flowsContext: FlowsContext;
externalId: string | undefined;
}): Promise<PopulatedFlow> {
if (isNil(externalId)) {
throw new Error(JSON.stringify({
message: 'Please select a flow',
}));
}
const externalIds = [externalId];
const allFlows = await listEnabledFlowsWithSubflowTrigger({
flowsContext,
params: {
externalIds
}
});
if (allFlows.length === 0) {
throw new Error(JSON.stringify({
message: 'Flow not found',
externalId,
}));
}
return allFlows[0];
}
type ListParams = {
flowsContext: FlowsContext,
params?: ListFlowsContextParams
}

View File

@@ -0,0 +1,83 @@
import {
createTrigger,
DynamicPropsValue,
PieceAuth,
Property,
StoreScope,
TriggerStrategy,
} from '@activepieces/pieces-framework';
import { callableFlowKey, CallableFlowRequest, MOCK_CALLBACK_IN_TEST_FLOW_URL } from '../common';
export const callableFlow = createTrigger({
name: 'callableFlow',
displayName: 'Callable Flow',
description: 'Waiting to be triggered from another flow',
props: {
mode: Property.StaticDropdown({
displayName: 'Mode',
required: true,
description: 'Choose Simple for key-value or Advanced for JSON.',
defaultValue: 'simple',
options: {
disabled: false,
options: [
{
label: 'Simple',
value: 'simple',
},
{
label: 'Advanced',
value: 'advanced',
},
],
},
}),
exampleData: Property.DynamicProperties({
auth: PieceAuth.None(),
displayName: 'Sample Data',
description: 'The schema to be passed to the flow',
required: true,
refreshers: ['mode'],
props: async (propsValue) => {
const mode = propsValue['mode'] as unknown as string;
const fields: DynamicPropsValue = {};
if (mode === 'simple') {
fields['sampleData'] = Property.Object({
displayName: 'Sample Data',
required: true,
});
} else {
fields['sampleData'] = Property.Json({
displayName: 'Sample Data',
required: true,
});
}
return fields;
},
}),
},
sampleData: null,
type: TriggerStrategy.WEBHOOK,
async onEnable() {
// ignore
},
async onDisable() {
// ignore
},
async test(context) {
const request: CallableFlowRequest = {
data: context.propsValue.exampleData['sampleData'],
callbackUrl: MOCK_CALLBACK_IN_TEST_FLOW_URL
}
return [request];
},
async run(context) {
return [context.payload.body];
},
async onStart(context) {
const request = context.payload as CallableFlowRequest;
if (request.callbackUrl) {
await context.store.put(callableFlowKey(context.run.id), request.callbackUrl, StoreScope.FLOW);
}
}
});