From f3e1b8f8bfd7aa6517d5bd811fb5dec499af7a79 Mon Sep 17 00:00:00 2001 From: poduck Date: Sat, 20 Dec 2025 00:18:42 -0500 Subject: [PATCH] Add Python/Ruby code pieces and fix template loading performance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Python code execution piece with subprocess-based runner - Add Ruby code execution piece with subprocess-based runner - Fix template loading: fetch individual templates from cloud for community edition - Add piece name aliasing for renamed pieces (piece-text-ai → piece-ai, etc.) - Add dev pieces caching to avoid disk reads on every request (60s TTL) - Add Python and Ruby logos to Django static files 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- activepieces-fork/Dockerfile | 12 +- .../pieces/community/python-code/package.json | 5 + .../pieces/community/python-code/project.json | 60 ++++++++++ .../pieces/community/python-code/src/index.ts | 18 +++ .../python-code/src/lib/run-python-code.ts | 112 +++++++++++++++++ .../community/python-code/tsconfig.json | 19 +++ .../community/python-code/tsconfig.lib.json | 11 ++ .../pieces/community/ruby-code/package.json | 5 + .../pieces/community/ruby-code/project.json | 60 ++++++++++ .../pieces/community/ruby-code/src/index.ts | 18 +++ .../ruby-code/src/lib/run-ruby-code.ts | 113 ++++++++++++++++++ .../pieces/community/ruby-code/tsconfig.json | 19 +++ .../community/ruby-code/tsconfig.lib.json | 11 ++ .../pieces/metadata/piece-metadata-service.ts | 56 ++++++++- .../community-flow-template.service.ts | 23 ++++ .../src/app/template/template.controller.ts | 8 ++ smoothschedule/.envs/.local/.activepieces | 11 +- .../static/images/python-logo.svg | 1 + .../static/images/ruby-logo.svg | 1 + 19 files changed, 557 insertions(+), 6 deletions(-) create mode 100644 activepieces-fork/packages/pieces/community/python-code/package.json create mode 100644 activepieces-fork/packages/pieces/community/python-code/project.json create mode 100644 activepieces-fork/packages/pieces/community/python-code/src/index.ts create mode 100644 activepieces-fork/packages/pieces/community/python-code/src/lib/run-python-code.ts create mode 100644 activepieces-fork/packages/pieces/community/python-code/tsconfig.json create mode 100644 activepieces-fork/packages/pieces/community/python-code/tsconfig.lib.json create mode 100644 activepieces-fork/packages/pieces/community/ruby-code/package.json create mode 100644 activepieces-fork/packages/pieces/community/ruby-code/project.json create mode 100644 activepieces-fork/packages/pieces/community/ruby-code/src/index.ts create mode 100644 activepieces-fork/packages/pieces/community/ruby-code/src/lib/run-ruby-code.ts create mode 100644 activepieces-fork/packages/pieces/community/ruby-code/tsconfig.json create mode 100644 activepieces-fork/packages/pieces/community/ruby-code/tsconfig.lib.json create mode 100644 smoothschedule/smoothschedule/static/images/python-logo.svg create mode 100644 smoothschedule/smoothschedule/static/images/ruby-logo.svg diff --git a/activepieces-fork/Dockerfile b/activepieces-fork/Dockerfile index 8c58227b..484e4bb5 100644 --- a/activepieces-fork/Dockerfile +++ b/activepieces-fork/Dockerfile @@ -14,6 +14,8 @@ RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \ apt-get install -y --no-install-recommends \ openssh-client \ python3 \ + python3-pip \ + ruby \ g++ \ build-essential \ git \ @@ -69,17 +71,21 @@ RUN --mount=type=cache,target=/root/.bun/install/cache \ # Copy source code after dependency installation COPY . . -# Build all projects including the SmoothSchedule piece -RUN npx nx run-many --target=build --projects=react-ui,server-api,pieces-smoothschedule --configuration production --parallel=2 --skip-nx-cache +# Build all projects including custom pieces +RUN npx nx run-many --target=build --projects=react-ui,server-api,pieces-smoothschedule,pieces-python-code,pieces-ruby-code --configuration production --parallel=2 --skip-nx-cache --verbose # Install production dependencies only for the backend API RUN --mount=type=cache,target=/root/.bun/install/cache \ cd dist/packages/server/api && \ bun install --production --frozen-lockfile -# Install dependencies for the SmoothSchedule piece +# Install dependencies for custom pieces RUN --mount=type=cache,target=/root/.bun/install/cache \ cd dist/packages/pieces/community/smoothschedule && \ + bun install --production && \ + cd ../python-code && \ + bun install --production && \ + cd ../ruby-code && \ bun install --production ### STAGE 2: Run ### diff --git a/activepieces-fork/packages/pieces/community/python-code/package.json b/activepieces-fork/packages/pieces/community/python-code/package.json new file mode 100644 index 00000000..248d8c92 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/python-code/package.json @@ -0,0 +1,5 @@ +{ + "name": "@activepieces/piece-python-code", + "version": "0.0.1", + "dependencies": {} +} diff --git a/activepieces-fork/packages/pieces/community/python-code/project.json b/activepieces-fork/packages/pieces/community/python-code/project.json new file mode 100644 index 00000000..bf1e39d9 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/python-code/project.json @@ -0,0 +1,60 @@ +{ + "name": "pieces-python-code", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/python-code/src", + "projectType": "library", + "release": { + "version": { + "currentVersionResolver": "git-tag", + "preserveLocalDependencyProtocols": false, + "manifestRootsToUpdate": [ + "dist/{projectRoot}" + ] + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/python-code", + "tsConfig": "packages/pieces/community/python-code/tsconfig.lib.json", + "packageJson": "packages/pieces/community/python-code/package.json", + "main": "packages/pieces/community/python-code/src/index.ts", + "assets": [ + "packages/pieces/community/python-code/*.md" + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true + }, + "dependsOn": [ + "^build", + "prebuild" + ] + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + }, + "prebuild": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/pieces/community/python-code", + "command": "bun install --no-save --silent" + }, + "dependsOn": [ + "^build" + ] + } + } +} diff --git a/activepieces-fork/packages/pieces/community/python-code/src/index.ts b/activepieces-fork/packages/pieces/community/python-code/src/index.ts new file mode 100644 index 00000000..18c562e1 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/python-code/src/index.ts @@ -0,0 +1,18 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { PieceCategory } from '@activepieces/shared'; +import { runPythonCode } from './lib/run-python-code'; + +// Python logo - hosted on Django backend +const PYTHON_LOGO = 'http://lvh.me:8000/static/images/python-logo.svg'; + +export const pythonCode = createPiece({ + displayName: 'Python Code', + description: 'Execute Python code in your automations', + auth: PieceAuth.None(), + minimumSupportedRelease: '0.36.1', + logoUrl: PYTHON_LOGO, + categories: [PieceCategory.CORE, PieceCategory.DEVELOPER_TOOLS], + authors: ['smoothschedule'], + actions: [runPythonCode], + triggers: [], +}); diff --git a/activepieces-fork/packages/pieces/community/python-code/src/lib/run-python-code.ts b/activepieces-fork/packages/pieces/community/python-code/src/lib/run-python-code.ts new file mode 100644 index 00000000..b52802f2 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/python-code/src/lib/run-python-code.ts @@ -0,0 +1,112 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +const execAsync = promisify(exec); + +export const runPythonCode = createAction({ + name: 'run_python_code', + displayName: 'Run Python Code', + description: 'Execute Python code and return the output. Use print() to output results.', + props: { + code: Property.LongText({ + displayName: 'Python Code', + description: 'The Python code to execute. Use print() to output results that will be captured.', + required: true, + defaultValue: `# Example: Process input data +import json + +# Access inputs via the 'inputs' variable (parsed from JSON) +# Example: name = inputs.get('name', 'World') + +# Your code here +result = "Hello from Python!" + +# Print your output (will be captured as the action result) +print(result)`, + }), + inputs: Property.Object({ + displayName: 'Inputs', + description: 'Input data to pass to the Python code. Available as the `inputs` variable (dict).', + required: false, + defaultValue: {}, + }), + timeout: Property.Number({ + displayName: 'Timeout (seconds)', + description: 'Maximum execution time in seconds', + required: false, + defaultValue: 30, + }), + }, + async run(context) { + const { code, inputs, timeout } = context.propsValue; + const timeoutMs = (timeout || 30) * 1000; + + // Create a temporary file for the Python code + const tmpDir = os.tmpdir(); + const scriptPath = path.join(tmpDir, `ap_python_${Date.now()}.py`); + const inputPath = path.join(tmpDir, `ap_python_input_${Date.now()}.json`); + + try { + // Write inputs to a JSON file + await fs.promises.writeFile(inputPath, JSON.stringify(inputs || {})); + + // Wrap the user code to load inputs + const wrappedCode = ` +import json +import sys + +# Load inputs from JSON file +with open('${inputPath.replace(/\\/g, '\\\\')}', 'r') as f: + inputs = json.load(f) + +# User code starts here +${code} +`; + + // Write the script to a temp file + await fs.promises.writeFile(scriptPath, wrappedCode); + + // Execute Python + const { stdout, stderr } = await execAsync(`python3 "${scriptPath}"`, { + timeout: timeoutMs, + maxBuffer: 10 * 1024 * 1024, // 10MB buffer + }); + + // Clean up temp files + await fs.promises.unlink(scriptPath).catch(() => {}); + await fs.promises.unlink(inputPath).catch(() => {}); + + // Try to parse output as JSON, otherwise return as string + const output = stdout.trim(); + let result: unknown; + try { + result = JSON.parse(output); + } catch { + result = output; + } + + return { + success: true, + output: result, + stdout: stdout, + stderr: stderr || null, + }; + } catch (error: unknown) { + // Clean up temp files on error + await fs.promises.unlink(scriptPath).catch(() => {}); + await fs.promises.unlink(inputPath).catch(() => {}); + + const execError = error as { stderr?: string; message?: string; killed?: boolean }; + + if (execError.killed) { + throw new Error(`Python script timed out after ${timeout} seconds`); + } + + throw new Error(`Python execution failed: ${execError.stderr || execError.message}`); + } + }, +}); diff --git a/activepieces-fork/packages/pieces/community/python-code/tsconfig.json b/activepieces-fork/packages/pieces/community/python-code/tsconfig.json new file mode 100644 index 00000000..29c9dd1b --- /dev/null +++ b/activepieces-fork/packages/pieces/community/python-code/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/activepieces-fork/packages/pieces/community/python-code/tsconfig.lib.json b/activepieces-fork/packages/pieces/community/python-code/tsconfig.lib.json new file mode 100644 index 00000000..28369ef7 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/python-code/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/activepieces-fork/packages/pieces/community/ruby-code/package.json b/activepieces-fork/packages/pieces/community/ruby-code/package.json new file mode 100644 index 00000000..36f3dbd5 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/ruby-code/package.json @@ -0,0 +1,5 @@ +{ + "name": "@activepieces/piece-ruby-code", + "version": "0.0.1", + "dependencies": {} +} diff --git a/activepieces-fork/packages/pieces/community/ruby-code/project.json b/activepieces-fork/packages/pieces/community/ruby-code/project.json new file mode 100644 index 00000000..7a2198fb --- /dev/null +++ b/activepieces-fork/packages/pieces/community/ruby-code/project.json @@ -0,0 +1,60 @@ +{ + "name": "pieces-ruby-code", + "$schema": "../../../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/pieces/community/ruby-code/src", + "projectType": "library", + "release": { + "version": { + "currentVersionResolver": "git-tag", + "preserveLocalDependencyProtocols": false, + "manifestRootsToUpdate": [ + "dist/{projectRoot}" + ] + } + }, + "tags": [], + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": [ + "{options.outputPath}" + ], + "options": { + "outputPath": "dist/packages/pieces/community/ruby-code", + "tsConfig": "packages/pieces/community/ruby-code/tsconfig.lib.json", + "packageJson": "packages/pieces/community/ruby-code/package.json", + "main": "packages/pieces/community/ruby-code/src/index.ts", + "assets": [ + "packages/pieces/community/ruby-code/*.md" + ], + "buildableProjectDepsInPackageJsonType": "dependencies", + "updateBuildableProjectDepsInPackageJson": true + }, + "dependsOn": [ + "^build", + "prebuild" + ] + }, + "nx-release-publish": { + "options": { + "packageRoot": "dist/{projectRoot}" + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": [ + "{options.outputFile}" + ] + }, + "prebuild": { + "executor": "nx:run-commands", + "options": { + "cwd": "packages/pieces/community/ruby-code", + "command": "bun install --no-save --silent" + }, + "dependsOn": [ + "^build" + ] + } + } +} diff --git a/activepieces-fork/packages/pieces/community/ruby-code/src/index.ts b/activepieces-fork/packages/pieces/community/ruby-code/src/index.ts new file mode 100644 index 00000000..31c18a84 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/ruby-code/src/index.ts @@ -0,0 +1,18 @@ +import { createPiece, PieceAuth } from '@activepieces/pieces-framework'; +import { PieceCategory } from '@activepieces/shared'; +import { runRubyCode } from './lib/run-ruby-code'; + +// Ruby logo - hosted on Django backend +const RUBY_LOGO = 'http://lvh.me:8000/static/images/ruby-logo.svg'; + +export const rubyCode = createPiece({ + displayName: 'Ruby Code', + description: 'Execute Ruby code in your automations', + auth: PieceAuth.None(), + minimumSupportedRelease: '0.36.1', + logoUrl: RUBY_LOGO, + categories: [PieceCategory.CORE, PieceCategory.DEVELOPER_TOOLS], + authors: ['smoothschedule'], + actions: [runRubyCode], + triggers: [], +}); diff --git a/activepieces-fork/packages/pieces/community/ruby-code/src/lib/run-ruby-code.ts b/activepieces-fork/packages/pieces/community/ruby-code/src/lib/run-ruby-code.ts new file mode 100644 index 00000000..277c8ef5 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/ruby-code/src/lib/run-ruby-code.ts @@ -0,0 +1,113 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { exec } from 'child_process'; +import { promisify } from 'util'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; + +const execAsync = promisify(exec); + +export const runRubyCode = createAction({ + name: 'run_ruby_code', + displayName: 'Run Ruby Code', + description: 'Execute Ruby code and return the output. Use puts or print to output results.', + props: { + code: Property.LongText({ + displayName: 'Ruby Code', + description: 'The Ruby code to execute. Use puts or print to output results that will be captured.', + required: true, + defaultValue: `# Example: Process input data +require 'json' + +# Access inputs via the 'inputs' variable (parsed from JSON) +# Example: name = inputs['name'] || 'World' + +# Your code here +result = "Hello from Ruby!" + +# Print your output (will be captured as the action result) +puts result`, + }), + inputs: Property.Object({ + displayName: 'Inputs', + description: 'Input data to pass to the Ruby code. Available as the `inputs` variable (Hash).', + required: false, + defaultValue: {}, + }), + timeout: Property.Number({ + displayName: 'Timeout (seconds)', + description: 'Maximum execution time in seconds', + required: false, + defaultValue: 30, + }), + }, + async run(context) { + const { code, inputs, timeout } = context.propsValue; + const timeoutMs = (timeout || 30) * 1000; + + // Create a temporary file for the Ruby code + const tmpDir = os.tmpdir(); + const scriptPath = path.join(tmpDir, `ap_ruby_${Date.now()}.rb`); + const inputPath = path.join(tmpDir, `ap_ruby_input_${Date.now()}.json`); + + try { + // Write inputs to a JSON file + await fs.promises.writeFile(inputPath, JSON.stringify(inputs || {})); + + // Escape the input path for Ruby + const escapedInputPath = inputPath.replace(/\\/g, '\\\\').replace(/'/g, "\\'"); + + // Wrap the user code to load inputs + const wrappedCode = ` +require 'json' + +# Load inputs from JSON file +inputs = JSON.parse(File.read('${escapedInputPath}')) + +# User code starts here +${code} +`; + + // Write the script to a temp file + await fs.promises.writeFile(scriptPath, wrappedCode); + + // Execute Ruby + const { stdout, stderr } = await execAsync(`ruby "${scriptPath}"`, { + timeout: timeoutMs, + maxBuffer: 10 * 1024 * 1024, // 10MB buffer + }); + + // Clean up temp files + await fs.promises.unlink(scriptPath).catch(() => {}); + await fs.promises.unlink(inputPath).catch(() => {}); + + // Try to parse output as JSON, otherwise return as string + const output = stdout.trim(); + let result: unknown; + try { + result = JSON.parse(output); + } catch { + result = output; + } + + return { + success: true, + output: result, + stdout: stdout, + stderr: stderr || null, + }; + } catch (error: unknown) { + // Clean up temp files on error + await fs.promises.unlink(scriptPath).catch(() => {}); + await fs.promises.unlink(inputPath).catch(() => {}); + + const execError = error as { stderr?: string; message?: string; killed?: boolean }; + + if (execError.killed) { + throw new Error(`Ruby script timed out after ${timeout} seconds`); + } + + throw new Error(`Ruby execution failed: ${execError.stderr || execError.message}`); + } + }, +}); diff --git a/activepieces-fork/packages/pieces/community/ruby-code/tsconfig.json b/activepieces-fork/packages/pieces/community/ruby-code/tsconfig.json new file mode 100644 index 00000000..29c9dd1b --- /dev/null +++ b/activepieces-fork/packages/pieces/community/ruby-code/tsconfig.json @@ -0,0 +1,19 @@ +{ + "extends": "../../../../tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "forceConsistentCasingInFileNames": true, + "strict": true, + "noImplicitOverride": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noPropertyAccessFromIndexSignature": true + }, + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + } + ] +} diff --git a/activepieces-fork/packages/pieces/community/ruby-code/tsconfig.lib.json b/activepieces-fork/packages/pieces/community/ruby-code/tsconfig.lib.json new file mode 100644 index 00000000..28369ef7 --- /dev/null +++ b/activepieces-fork/packages/pieces/community/ruby-code/tsconfig.lib.json @@ -0,0 +1,11 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../../../dist/out-tsc", + "declaration": true, + "types": ["node"] + }, + "exclude": ["jest.config.ts", "src/**/*.spec.ts", "src/**/*.test.ts"], + "include": ["src/**/*.ts"] +} diff --git a/activepieces-fork/packages/server/api/src/app/pieces/metadata/piece-metadata-service.ts b/activepieces-fork/packages/server/api/src/app/pieces/metadata/piece-metadata-service.ts index 1ba46250..51c0c3ad 100644 --- a/activepieces-fork/packages/server/api/src/app/pieces/metadata/piece-metadata-service.ts +++ b/activepieces-fork/packages/server/api/src/app/pieces/metadata/piece-metadata-service.ts @@ -37,6 +37,19 @@ import { pieceListUtils } from './utils' export const pieceRepos = repoFactory(PieceMetadataEntity) +// Map of old/renamed piece names to their current names +// This allows templates with old piece references to still work +const PIECE_NAME_ALIASES: Record = { + '@activepieces/piece-text-ai': '@activepieces/piece-ai', + '@activepieces/piece-utility-ai': '@activepieces/piece-ai', + '@activepieces/piece-image-ai': '@activepieces/piece-ai', +} + +// Cache for dev pieces to avoid reading from disk on every request +let devPiecesCache: PieceMetadataSchema[] | null = null +let devPiecesCacheTime: number = 0 +const DEV_PIECES_CACHE_TTL_MS = 60000 // 1 minute cache + export const pieceMetadataService = (log: FastifyBaseLogger) => { return { async setup(): Promise { @@ -89,13 +102,35 @@ export const pieceMetadataService = (log: FastifyBaseLogger) => { release: undefined, log, }) - const piece = originalPieces.find((piece) => { + let piece = originalPieces.find((piece) => { const strictlyLessThan = (isNil(versionToSearch) || ( semVer.compare(piece.version, versionToSearch.nextExcludedVersion) < 0 && semVer.compare(piece.version, versionToSearch.baseVersion) >= 0 )) return piece.name === name && strictlyLessThan }) + + // Fall back to latest version if specific version not found + // This allows templates with old piece versions to still work + if (isNil(piece) && !isNil(version)) { + piece = originalPieces.find((p) => p.name === name) + if (!isNil(piece)) { + log.info(`Piece ${name} version ${version} not found, falling back to latest version ${piece.version}`) + } + } + + // Try piece name alias if piece still not found + // This handles renamed pieces (e.g., piece-text-ai -> piece-ai) + if (isNil(piece)) { + const aliasedName = PIECE_NAME_ALIASES[name] + if (!isNil(aliasedName)) { + piece = originalPieces.find((p) => p.name === aliasedName) + if (!isNil(piece)) { + log.info(`Piece ${name} not found, using alias ${aliasedName} (version ${piece.version})`) + } + } + } + const isFiltered = !isNil(piece) && await enterpriseFilteringUtils.isFiltered({ piece, projectId, @@ -287,10 +322,20 @@ const loadDevPiecesIfEnabled = async (log: FastifyBaseLogger): Promise ({ + const result = pieces.map((p): PieceMetadataSchema => ({ id: apId(), ...p, projectUsage: 0, @@ -299,6 +344,13 @@ const loadDevPiecesIfEnabled = async (log: FastifyBaseLogger): Promise => { diff --git a/activepieces-fork/packages/server/api/src/app/template/community-flow-template.service.ts b/activepieces-fork/packages/server/api/src/app/template/community-flow-template.service.ts index b2b03b5f..fa5a7e7b 100644 --- a/activepieces-fork/packages/server/api/src/app/template/community-flow-template.service.ts +++ b/activepieces-fork/packages/server/api/src/app/template/community-flow-template.service.ts @@ -25,6 +25,29 @@ export const communityTemplates = { const templates = await response.json() return templates }, + getById: async (id: string): Promise