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,241 @@
import { ContextVersion } from '@activepieces/pieces-framework'
import { DEFAULT_MCP_DATA, EngineGenericError, ExecuteFlowOperation, ExecutePropsOptions, ExecuteToolOperation, ExecuteTriggerOperation, ExecutionType, FlowVersionState, PlatformId, ProgressUpdateType, Project, ProjectId, ResumePayload, RunEnvironment, TriggerHookType } from '@activepieces/shared'
import { createPropsResolver, PropsResolver } from '../../variables/props-resolver'
type RetryConstants = {
maxAttempts: number
retryExponential: number
retryInterval: number
}
type EngineConstantsParams = {
flowId: string
flowVersionId: string
flowVersionState: FlowVersionState
triggerPieceName: string
flowRunId: string
publicApiUrl: string
internalApiUrl: string
retryConstants: RetryConstants
engineToken: string
projectId: ProjectId
progressUpdateType: ProgressUpdateType
serverHandlerId: string | null
httpRequestId: string | null
resumePayload?: ResumePayload
runEnvironment?: RunEnvironment
stepNameToTest?: string
logsUploadUrl?: string
logsFileId?: string
timeoutInSeconds: number
platformId: PlatformId
}
const DEFAULT_RETRY_CONSTANTS: RetryConstants = {
maxAttempts: 4,
retryExponential: 2,
retryInterval: 2000,
}
const DEFAULT_TRIGGER_EXECUTION = 'execute-trigger'
const DEFAULT_EXECUTE_PROPERTY = 'execute-property'
export class EngineConstants {
public static readonly BASE_CODE_DIRECTORY = process.env.AP_BASE_CODE_DIRECTORY ?? './codes'
public static readonly INPUT_FILE = './input.json'
public static readonly OUTPUT_FILE = './output.json'
public static readonly DEV_PIECES = process.env.AP_DEV_PIECES?.split(',') ?? []
public static readonly TEST_MODE = process.env.AP_TEST_MODE === 'true'
public readonly platformId: string
public readonly timeoutInSeconds: number
public readonly flowId: string
public readonly flowVersionId: string
public readonly flowVersionState: FlowVersionState
public readonly triggerPieceName: string
public readonly flowRunId: string
public readonly publicApiUrl: string
public readonly internalApiUrl: string
public readonly retryConstants: RetryConstants
public readonly engineToken: string
public readonly projectId: ProjectId
public readonly progressUpdateType: ProgressUpdateType
public readonly serverHandlerId: string | null
public readonly httpRequestId: string | null
public readonly resumePayload?: ResumePayload
public readonly runEnvironment?: RunEnvironment
public readonly stepNameToTest?: string
public readonly logsUploadUrl?: string
public readonly logsFileId?: string
private project: Project | null = null
public get isRunningApTests(): boolean {
return EngineConstants.TEST_MODE
}
public get baseCodeDirectory(): string {
return EngineConstants.BASE_CODE_DIRECTORY
}
public get devPieces(): string[] {
return EngineConstants.DEV_PIECES
}
public constructor(params: EngineConstantsParams) {
if (!params.publicApiUrl.endsWith('/api/')) {
throw new EngineGenericError('PublicUrlNotEndsWithSlashError', `Public URL must end with a slash, got: ${params.publicApiUrl}`)
}
if (!params.internalApiUrl.endsWith('/')) {
throw new EngineGenericError('InternalApiUrlNotEndsWithSlashError', `Internal API URL must end with a slash, got: ${params.internalApiUrl}`)
}
this.flowId = params.flowId
this.flowVersionId = params.flowVersionId
this.flowVersionState = params.flowVersionState
this.flowRunId = params.flowRunId
this.publicApiUrl = params.publicApiUrl
this.internalApiUrl = params.internalApiUrl
this.retryConstants = params.retryConstants
this.triggerPieceName = params.triggerPieceName
this.engineToken = params.engineToken
this.projectId = params.projectId
this.progressUpdateType = params.progressUpdateType
this.serverHandlerId = params.serverHandlerId
this.httpRequestId = params.httpRequestId
this.resumePayload = params.resumePayload
this.runEnvironment = params.runEnvironment
this.stepNameToTest = params.stepNameToTest
this.logsUploadUrl = params.logsUploadUrl
this.logsFileId = params.logsFileId
this.platformId = params.platformId
this.timeoutInSeconds = params.timeoutInSeconds
}
public static fromExecuteFlowInput(input: ExecuteFlowOperation): EngineConstants {
return new EngineConstants({
flowId: input.flowVersion.flowId,
flowVersionId: input.flowVersion.id,
flowVersionState: input.flowVersion.state,
triggerPieceName: input.flowVersion.trigger.settings.pieceName,
flowRunId: input.flowRunId,
publicApiUrl: input.publicApiUrl,
internalApiUrl: input.internalApiUrl,
retryConstants: DEFAULT_RETRY_CONSTANTS,
engineToken: input.engineToken,
projectId: input.projectId,
progressUpdateType: input.progressUpdateType,
serverHandlerId: input.serverHandlerId ?? null,
httpRequestId: input.httpRequestId ?? null,
resumePayload: input.executionType === ExecutionType.RESUME ? input.resumePayload : undefined,
runEnvironment: input.runEnvironment,
stepNameToTest: input.stepNameToTest ?? undefined,
logsUploadUrl: input.logsUploadUrl,
logsFileId: input.logsFileId,
timeoutInSeconds: input.timeoutInSeconds,
platformId: input.platformId,
})
}
public static fromExecuteActionInput(input: ExecuteToolOperation): EngineConstants {
return new EngineConstants({
flowId: DEFAULT_MCP_DATA.flowId,
flowVersionId: DEFAULT_MCP_DATA.flowVersionId,
flowVersionState: DEFAULT_MCP_DATA.flowVersionState,
triggerPieceName: DEFAULT_MCP_DATA.triggerPieceName,
flowRunId: DEFAULT_MCP_DATA.flowRunId,
publicApiUrl: input.publicApiUrl,
internalApiUrl: addTrailingSlashIfMissing(input.internalApiUrl),
retryConstants: DEFAULT_RETRY_CONSTANTS,
engineToken: input.engineToken,
projectId: input.projectId,
progressUpdateType: ProgressUpdateType.NONE,
serverHandlerId: null,
httpRequestId: null,
resumePayload: undefined,
runEnvironment: undefined,
stepNameToTest: undefined,
timeoutInSeconds: input.timeoutInSeconds,
platformId: input.platformId,
})
}
public static fromExecutePropertyInput(input: Omit<ExecutePropsOptions, 'piece'> & { pieceName: string, pieceVersion: string }): EngineConstants {
return new EngineConstants({
flowId: input.flowVersion?.flowId ?? DEFAULT_MCP_DATA.flowId,
flowVersionId: input.flowVersion?.id ?? DEFAULT_MCP_DATA.flowVersionId,
flowVersionState: input.flowVersion?.state ?? DEFAULT_MCP_DATA.flowVersionState,
triggerPieceName: input.flowVersion?.trigger?.settings.pieceName ?? DEFAULT_MCP_DATA.triggerPieceName,
flowRunId: DEFAULT_EXECUTE_PROPERTY,
publicApiUrl: input.publicApiUrl,
internalApiUrl: addTrailingSlashIfMissing(input.internalApiUrl),
retryConstants: DEFAULT_RETRY_CONSTANTS,
engineToken: input.engineToken,
projectId: input.projectId,
progressUpdateType: ProgressUpdateType.NONE,
serverHandlerId: null,
httpRequestId: null,
resumePayload: undefined,
runEnvironment: undefined,
stepNameToTest: undefined,
timeoutInSeconds: input.timeoutInSeconds,
platformId: input.platformId,
})
}
public static fromExecuteTriggerInput(input: ExecuteTriggerOperation<TriggerHookType>): EngineConstants {
return new EngineConstants({
flowId: input.flowVersion.flowId,
flowVersionId: input.flowVersion.id,
flowVersionState: input.flowVersion.state,
triggerPieceName: input.flowVersion.trigger.settings.pieceName,
flowRunId: DEFAULT_TRIGGER_EXECUTION,
publicApiUrl: input.publicApiUrl,
internalApiUrl: addTrailingSlashIfMissing(input.internalApiUrl),
retryConstants: DEFAULT_RETRY_CONSTANTS,
engineToken: input.engineToken,
projectId: input.projectId,
progressUpdateType: ProgressUpdateType.NONE,
serverHandlerId: null,
httpRequestId: null,
resumePayload: undefined,
runEnvironment: undefined,
stepNameToTest: undefined,
timeoutInSeconds: input.timeoutInSeconds,
platformId: input.platformId,
})
}
public getPropsResolver(contextVersion: ContextVersion | undefined): PropsResolver {
return createPropsResolver({
projectId: this.projectId,
engineToken: this.engineToken,
apiUrl: this.internalApiUrl,
contextVersion,
})
}
private async getProject(): Promise<Project> {
if (this.project) {
return this.project
}
const getWorkerProjectEndpoint = `${this.internalApiUrl}v1/worker/project`
const response = await fetch(getWorkerProjectEndpoint, {
headers: {
Authorization: `Bearer ${this.engineToken}`,
},
})
this.project = await response.json() as Project
return this.project
}
public externalProjectId = async (): Promise<string | undefined> => {
const project = await this.getProject()
return project.externalId
}
}
const addTrailingSlashIfMissing = (url: string): string => {
return url.endsWith('/') ? url : url + '/'
}

View File

@@ -0,0 +1,213 @@
import { assertEqual, EngineGenericError, FailedStep, FlowActionType, FlowRunStatus, GenericStepOutput, isNil, LoopStepOutput, LoopStepResult, PauseMetadata, PauseType, RespondResponse, StepOutput, StepOutputStatus } from '@activepieces/shared'
import dayjs from 'dayjs'
import { nanoid } from 'nanoid'
import { loggingUtils } from '../../helper/logging-utils'
import { StepExecutionPath } from './step-execution-path'
export type FlowVerdict = {
status: FlowRunStatus.PAUSED
pauseMetadata: PauseMetadata
} | {
status: FlowRunStatus.SUCCEEDED
stopResponse: RespondResponse | undefined
} | {
status: FlowRunStatus.FAILED
failedStep: FailedStep
} | {
status: FlowRunStatus.RUNNING
}
export class FlowExecutorContext {
tags: readonly string[]
steps: Readonly<Record<string, StepOutput>>
pauseRequestId: string
verdict: FlowVerdict
currentPath: StepExecutionPath
stepNameToTest?: boolean
stepsCount: number
/**
* Execution time in milliseconds
*/
duration: number
constructor(copyFrom?: FlowExecutorContext) {
this.tags = copyFrom?.tags ?? []
this.steps = copyFrom?.steps ?? {}
this.pauseRequestId = copyFrom?.pauseRequestId ?? nanoid()
this.duration = copyFrom?.duration ?? -1
this.verdict = copyFrom?.verdict ?? { status: FlowRunStatus.RUNNING }
this.currentPath = copyFrom?.currentPath ?? StepExecutionPath.empty()
this.stepNameToTest = copyFrom?.stepNameToTest ?? false
this.stepsCount = copyFrom?.stepsCount ?? 0
}
static empty(): FlowExecutorContext {
return new FlowExecutorContext()
}
public setPauseRequestId(pauseRequestId: string): FlowExecutorContext {
return new FlowExecutorContext({
...this,
pauseRequestId,
})
}
public getDelayedInSeconds(): number | undefined {
if (this.verdict.status === FlowRunStatus.PAUSED && this.verdict.pauseMetadata.type === PauseType.DELAY) {
return dayjs(this.verdict.pauseMetadata.resumeDateTime).diff(Date.now(), 'seconds')
}
return undefined
}
public finishExecution(): FlowExecutorContext {
if (this.verdict.status === FlowRunStatus.RUNNING) {
return new FlowExecutorContext({
...this,
verdict: { status: FlowRunStatus.SUCCEEDED },
})
}
return this
}
public trimmedSteps(): Promise<Record<string, StepOutput>> {
return loggingUtils.trimExecution(this.steps)
}
public getLoopStepOutput({ stepName }: { stepName: string }): LoopStepOutput | undefined {
const stateAtPath = getStateAtPath({ currentPath: this.currentPath, steps: this.steps })
const stepOutput = stateAtPath[stepName]
if (isNil(stepOutput)) {
return undefined
}
assertEqual(stepOutput.type, FlowActionType.LOOP_ON_ITEMS, 'stepOutput.type', 'LOOP_ON_ITEMS')
// The new LoopStepOutput is needed as casting directly to LoopClassOutput will just cast the data but the class methods will not be available
return new LoopStepOutput(stepOutput as GenericStepOutput<FlowActionType.LOOP_ON_ITEMS, LoopStepResult>)
}
public isCompleted({ stepName }: { stepName: string }): boolean {
const stateAtPath = getStateAtPath({ currentPath: this.currentPath, steps: this.steps })
const stepOutput = stateAtPath[stepName]
if (isNil(stepOutput)) {
return false
}
return stepOutput.status !== StepOutputStatus.PAUSED
}
public isPaused({ stepName }: { stepName: string }): boolean {
const stateAtPath = getStateAtPath({ currentPath: this.currentPath, steps: this.steps })
const stepOutput = stateAtPath[stepName]
if (isNil(stepOutput)) {
return false
}
return stepOutput.status === StepOutputStatus.PAUSED
}
public setDuration(duration: number): FlowExecutorContext {
return new FlowExecutorContext({
...this,
duration,
})
}
public addTags(tags: string[]): FlowExecutorContext {
return new FlowExecutorContext({
...this,
tags: [...this.tags, ...tags].filter((value, index, self) => {
return self.indexOf(value) === index
}),
})
}
public upsertStep(stepName: string, stepOutput: StepOutput): FlowExecutorContext {
const steps = {
...this.steps,
}
const targetMap = getStateAtPath({ currentPath: this.currentPath, steps })
targetMap[stepName] = stepOutput
return new FlowExecutorContext({
...this,
steps,
})
}
public getStepOutput(stepName: string): StepOutput | undefined {
const stateAtPath = getStateAtPath({ currentPath: this.currentPath, steps: this.steps })
return stateAtPath[stepName]
}
public setCurrentPath(currentStatePath: StepExecutionPath): FlowExecutorContext {
return new FlowExecutorContext({
...this,
currentPath: currentStatePath,
})
}
public setVerdict(verdict: FlowVerdict): FlowExecutorContext {
return new FlowExecutorContext({
...this,
verdict,
})
}
public setRetryable(retryable: boolean): FlowExecutorContext {
return new FlowExecutorContext({
...this,
retryable,
})
}
public incrementStepsExecuted(): FlowExecutorContext {
return new FlowExecutorContext({
...this,
stepsCount: this.stepsCount + 1,
})
}
public currentState(): Record<string, unknown> {
let flattenedSteps: Record<string, unknown> = extractOutput(this.steps)
let targetMap = this.steps
this.currentPath.path.forEach(([stepName, iteration]) => {
const stepOutput = targetMap[stepName]
if (!stepOutput.output || stepOutput.type !== FlowActionType.LOOP_ON_ITEMS) {
throw new EngineGenericError('NotInstanceOfLoopOnItemsStepOutputError', '[ExecutionState#getTargetMap] Not instance of Loop On Items step output')
}
targetMap = stepOutput.output.iterations[iteration]
flattenedSteps = {
...flattenedSteps,
...extractOutput(targetMap),
}
})
return flattenedSteps
}
}
function extractOutput(steps: Record<string, StepOutput>): Record<string, unknown> {
return Object.entries(steps).reduce((acc: Record<string, unknown>, [stepName, step]) => {
acc[stepName] = step.output
return acc
}, {} as Record<string, unknown>)
}
function getStateAtPath({ currentPath, steps }: { currentPath: StepExecutionPath, steps: Record<string, StepOutput> }): Record<string, StepOutput> {
let targetMap = steps
currentPath.path.forEach(([stepName, iteration]) => {
const stepOutput = targetMap[stepName]
if (!stepOutput.output || stepOutput.type !== FlowActionType.LOOP_ON_ITEMS) {
throw new EngineGenericError('NotInstanceOfLoopOnItemsStepOutputError', `[ExecutionState#getTargetMap] Not instance of Loop On Items step output: ${stepOutput.type}`)
}
targetMap = stepOutput.output.iterations[iteration]
})
return targetMap
}

View File

@@ -0,0 +1,20 @@
export class StepExecutionPath {
public path: readonly [string, number][] = []
constructor(path: readonly [string, number][]) {
this.path = [...path]
}
loopIteration({ loopName, iteration }: { loopName: string, iteration: number }): StepExecutionPath {
return new StepExecutionPath([...this.path, [loopName, iteration]])
}
static empty(): StepExecutionPath {
return new StepExecutionPath([])
}
removeLast(): StepExecutionPath {
const newPath = this.path.slice(0, -1)
return new StepExecutionPath(newPath)
}
}

View File

@@ -0,0 +1,99 @@
import { LATEST_CONTEXT_VERSION } from '@activepieces/pieces-framework'
import {
FlowActionType,
flowStructureUtil,
FlowTriggerType,
FlowVersion,
GenericStepOutput,
isNil,
LoopStepOutput,
RouterStepOutput,
spreadIfDefined,
StepOutputStatus,
} from '@activepieces/shared'
import { createPropsResolver } from '../../variables/props-resolver'
import { FlowExecutorContext } from './flow-execution-context'
export const testExecutionContext = {
async stateFromFlowVersion({
flowVersion,
excludedStepName,
projectId,
engineToken,
apiUrl,
sampleData,
}: TestExecutionParams): Promise<FlowExecutorContext> {
let flowExecutionContext = FlowExecutorContext.empty()
if (isNil(flowVersion)) {
return flowExecutionContext
}
const flowSteps = flowStructureUtil.getAllSteps(flowVersion.trigger)
for (const step of flowSteps) {
const { name } = step
if (name === excludedStepName) {
continue
}
const stepType = step.type
switch (stepType) {
case FlowActionType.ROUTER:
flowExecutionContext = flowExecutionContext.upsertStep(
step.name,
RouterStepOutput.create({
input: step.settings,
type: stepType,
status: StepOutputStatus.SUCCEEDED,
...spreadIfDefined('output', sampleData?.[step.name]),
}),
)
break
case FlowActionType.LOOP_ON_ITEMS: {
const { resolvedInput } = await createPropsResolver({
apiUrl,
projectId,
engineToken,
contextVersion: LATEST_CONTEXT_VERSION,
}).resolve<{ items: unknown[] }>({
unresolvedInput: step.settings,
executionState: flowExecutionContext,
})
flowExecutionContext = flowExecutionContext.upsertStep(
step.name,
LoopStepOutput.init({
input: step.settings,
}).setOutput({
item: resolvedInput.items[0],
index: 1,
iterations: [],
}),
)
break
}
case FlowActionType.PIECE:
case FlowActionType.CODE:
case FlowTriggerType.EMPTY:
case FlowTriggerType.PIECE:
flowExecutionContext = flowExecutionContext.upsertStep(step.name, GenericStepOutput.create({
input: {},
type: stepType,
status: StepOutputStatus.SUCCEEDED,
...spreadIfDefined('output', sampleData?.[step.name]),
}))
break
}
}
return flowExecutionContext
},
}
type TestExecutionParams = {
flowVersion?: FlowVersion
excludedStepName?: string
projectId: string
apiUrl: string
engineToken: string
sampleData?: Record<string, unknown>
}