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,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,
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -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;
|
||||
},
|
||||
});
|
||||
@@ -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
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user