LLMs Technical Reviews
Home / API layer & connectors / activepieces

activepieces/activepieces

Self-hosted workflow automation platform with about 760 TypeScript "pieces", run by a worker engine and exposed to agents over MCP.

GitHub ↗★ 25kTypeScriptcommit e3b8c02 · 2026-10-06homepage ↗

Overview

Activepieces is a self-hosted workflow automation platform in the Zapier/n8n mould, with an MCP server added on top. Its integrations are called pieces: npm packages written in TypeScript against a small framework (createPiece, createAction, createTrigger). At the pinned commit the repo ships 736 community pieces and 27 core pieces (HTTP, webhook, schedule, tables, code helpers and so on). A piece declares props, auth and a run function. The platform supplies everything else: the flow builder UI, encrypted connection storage, OAuth2 refresh, trigger scheduling, a job queue, and a separate worker process that loads the piece and runs it.

For agents, the product’s “~400 MCP servers” label overstates what the code does. The MCP endpoint does not mount each piece as its own MCP server. It exposes one server per project with about 45 ap_* tools for building, testing and publishing flows. There is also a generic ap_run_action that runs any single piece action once, and every enabled flow with an MCP trigger becomes its own tool. An agent therefore reaches the pieces through a small, fixed tool surface plus search. It never sees a list of hundreds of per-API tools.

The core is MIT-licensed. Code under packages/ee/ and packages/server/api/src/app/ee falls under a separate commercial license (LICENSE).

Architecture

flowchart LR
  A["MCP client / agent"] --> M["POST /mcp (Streamable HTTP)"]
  UI["React builder"] --> API["Fastify API server"]
  M --> API
  API --> T["ap_* tools + flow tools"]
  T --> Q["Job queue (BullMQ on Redis)"]
  WH["Webhook / polling triggers"] --> Q
  Q --> W["Worker (Socket.IO poll loop)"]
  W --> S["Sandbox runtime"]
  S --> E["Engine process"]
  E --> P["Piece package: action.run()"]
  P --> X["Third-party API"]
  API --> DB["Postgres + pgvector"]
Component Path Role
Pieces framework packages/pieces/framework/ createPiece, createAction, createTrigger, PieceAuth, Property, metadata schemas
Pieces packages/pieces/community/, packages/pieces/core/ 736 + 27 integration packages
API server packages/server/api/src/app/ Fastify app: connections, flows, triggers, webhooks, MCP, tool search
MCP server packages/server/api/src/app/mcp/ Per-request McpServer, ap_* tools, flow tools, permissions, billing, OAuth
Tool search packages/server/api/src/app/tool-search/ pgvector cosine search over actions/triggers, keyword fallback (off by default)
Worker packages/server/worker/ Connects to the API over Socket.IO, polls jobs, dispatches to job handlers
Sandbox packages/server/sandbox/ Provisions piece/code caches, starts the engine in isolate or a plain child process
Engine packages/server/engine/ Loads the piece module, resolves props and auth, calls run, executes flows
Enterprise packages/ee/, packages/server/api/src/app/ee/ SSO, audit logs, platform features (commercial license)

How a request flows

Take an agent that calls ap_run_action to send one Slack message:

  1. MCP ingress. POST /mcp requires a Bearer token, resolves the MCP record and user, builds a fresh server, and serves the call through a stateless StreamableHTTPServerTransport (sessionIdGenerator: undefined), so every request builds its own server (mcp-oauth.controller.ts).
  2. Tool registration. buildMcpServer resolves call billing and a permission checker, then registers flow tools and static tools for the project (mcp-server-builder.ts). Each static tool is wrapped with the permission check, credit billing and activity recording (L292-L307).
  3. Validate. ap_run_action takes pieceName, actionName, input and a connection externalId (ap-run-action.ts). executePieceActionRun looks up the piece, turns the connection into a {{connections['…']}} reference, and diagnoses unknown, missing or invalid props before anything runs (flow-run-utils.ts).
  4. Enqueue and wait. It builds a one-step PieceAction and calls actionRunService.run, which submits an EXECUTE_ACTION job and waits synchronously, with the flow timeout as deadline (action-run.service.ts).
  5. Worker. The worker holds a Socket.IO connection to the API and polls for jobs over RPC (worker.ts). executeActionJob resolves the piece archive and calls ctx.runtime.execute with an EXECUTE_ACTION operation (execute-action.ts).
  6. Sandbox. The runtime acquires a box, provisions the piece and code caches, then starts the engine and sends it the operation (sandbox.ts).
  7. Engine. executeAction loads the piece module with require, resolves and validates props, and builds an ActionContext with auth, store, files, connections and run hooks for the piece’s run function (piece-executor.ts, piece-loader.ts).
  8. Return. The tool turns the outcome into MCP text. A timeout before the job started says “safe to retry”. A timeout after it started warns that the action may have partly completed (flow-run-utils.ts).

Key components

Piece framework

createPiece wraps display name, auth, actions and triggers into a Piece, rejects duplicate auth types, and clamps minimumSupportedRelease (piece.ts). createAction defaults requireAuth to true and both retryOnFailure and continueOnFailure to false (action.ts). Actions can carry agent-facing metadata: audience (human/ai/both), aiMetadata, and a classification of READ, SEARCH, WRITE or DESTRUCTIVE (piece-metadata.ts). Triggers add a type strategy, onEnable/onDisable/run hooks and optional webhook handshake and renewal (trigger.ts). All pieces are hand-written. Nothing is generated from OpenAPI.

MCP surface

Besides the per-project server, a platform-scoped server makes the agent call ap_set_project_context first and then routes each tool into the selected project (mcp-server-builder.ts). The server’s instructions string teaches the flow DSL: step references such as {{step_1['output'].id}}, full piece names, and “never delete and recreate a step” (L22-L39). Flow tools run the flow through the webhook service. They block for the result only when the MCP trigger is set to return a response (L202-L290).

ap_search_actions/ap_search_triggers embed the query and run a pgvector cosine scan over tool_search_index. The scan filters by tenant, piece and audience, a τ gate rejects weak top matches, and each hit is flagged connected when the project has a connection for that piece. Without an embedder, or when embedding fails, the tools fall back to Fuse keyword search (tool-search.service.ts). The feature ships disabled (AP_TOOL_SEARCH_ENABLED, tool-search-flag.ts).

Connections and OAuth2

Connection values are encrypted with AES-256-CBC under AP_ENCRYPTION_KEY. A key is generated automatically only in the in-memory-Redis dev mode (encryption.ts). On read, decryptAndRefreshConnection checks expiry and refreshes under a distributed lock keyed on platform and connection. It also strips the refresh token and client secret before the value leaves the service (app-connection-service.ts, app-connection.handler.ts). A token counts as expired 15 minutes before its real expiry (oauth2-util.ts).

Triggers

Enabling a polling trigger adds a repeating job keyed by flow version. The interval comes from the piece’s schedule or AP_TRIGGER_DEFAULT_POLL_INTERVAL (flow-trigger-side-effect.ts). Each poll runs the trigger’s RUN hook in the sandbox and submits any returned payloads as new runs (execute-polling.ts). Webhook flows get /:flowId (async) and /:flowId/sync (blocks for the flow’s response) on every HTTP method (webhook-controller.ts). Server-side dedupe is shallow: a payload’s _dedupe_key is counted in Redis for 30 seconds (dedupe-service.ts). Real “new item” detection lives in each piece’s polling helper and its store.

Sandbox modes

isolate (the IOI contest sandbox) is used only in SANDBOX_PROCESS and SANDBOX_CODE_AND_PROCESS modes. UNSANDBOXED and SANDBOX_CODE_ONLY start the engine as a plain child process (create-sandbox-for-job.ts). The isolate path validates mounts and env, then runs --cleanup/--init per box (isolate.ts). The server default is UNSANDBOXED (system.ts).

Extending it

  • New piece. Scaffold a package under packages/pieces/community/, export createPiece({...}) with actions and triggers (webhook piece is the smallest example). Use PieceAuth.OAuth2, SecretText or CustomAuth for credentials.
  • Make it agent-friendly. Set audience, aiMetadata and classification on actions. Search, tool annotations and read-only filtering use them.
  • Custom tools. Build a flow with an MCP trigger. It shows up as a tool named after toolName plus four characters of the flow id, with an input schema taken from the trigger settings.
  • Test before publishing. ap_validate_step_config returns field-level errors without touching the flow. ap_test_step runs the flow up to a given step, using sample or supplied trigger data.

Running it

  • Docker Compose. app and worker run the same image with AP_CONTAINER_TYPE set to APP or WORKER (5 worker replicas by default), plus pgvector/pgvector:0.8.0-pg14 and redis:7.0.7 (docker-compose.yml).
  • Required. Postgres (pgvector for tool search) and Redis. An AP_ENCRYPTION_KEY is needed in any non-dev setup.
  • Sandboxing. Set AP_EXECUTION_MODE to a SANDBOX_* mode on a host that can run isolate if pieces or code steps are not fully trusted.
  • Optional. AP_TOOL_SEARCH_ENABLED plus an OpenAI key for semantic search.

Strengths and caveats

  • Strength: a full platform, not just a library. Credential storage, OAuth refresh with locking, polling, webhooks, retries, run history and a UI all come in the box.
  • Strength: agent tooling is deliberate. Prop diagnosis before execution, started/not-started timeout messages, permission checks, per-call billing and classification metadata show real care for LLM callers.
  • Strength: pieces are plain TypeScript. A new integration is a small npm package, and the catalog is large.
  • Caveat: not 400 MCP servers. The MCP surface is one server with flow-building tools and a generic ap_run_action. An agent must search, read props and call through that indirection.
  • Caveat: no isolation by default. The default execution mode is UNSANDBOXED, so third-party piece code runs in an ordinary Node process next to the worker.
  • Caveat: heavy for “just tools”. Running one action means API, job queue, worker, sandbox and engine. Latency and moving parts are much higher than for an in-process SDK.
  • Caveat: split license. SSO, audit logs and several platform features sit under the commercial ee license.

Sources: code at e3b8c02, verified Q&A.

How it answers the API layer & connectors questions

Each answer was drafted by a code-reading agent at commit e3b8c02. Its citations were checked mechanically. Compare with the other api layer & connectors →

How is third-party authentication implemented?

answered

Third-party authentication is implemented through a layered system of piece-level auth schemas and server-side OAuth2 handlers.

Piece auth definitions live in each piece's source — Slack's slackAuth field shows the pattern: PieceAuth.OAuth2({ authUrl, tokenUrl, scope, required }) for OAuth2, PieceAuth.SecretText() for API keys, and PieceAuth.CustomAuth({ props, validate }) for multi-field credentials (Slack: botToken + userToken). Each auth type can carry a validate callback that is invoked at connection time (packages/pieces/community/slack/src/lib/auth.ts:11-47, 82-118).

Server-side OAuth2 flow is orchestrated by appConnectionService. On upsert, validateConnectionValue dispatches by connection type (packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts:691-788). For OAUTH2, the credentialsOauth2Service.claim() POSTs the auth code to the piece's tokenUrl using safeHttp.retryingAxios, supporting both authorization_code and client_credentials grants, BODY or HEADER authorization methods, and PKCE (packages/server/api/src/app/app-connection/app-connection-service/oauth2/services/credentials-oauth2-service.ts:16-106). After claiming, engineValidateAuth sends the credential to the engine sandbox for the piece to self-validate (packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts:790-841).

Token refresh occurs automatically on each connection read (via decryptAndRefreshConnection): isExpired() checks if the token is within 15 minutes of expiry (packages/server/api/src/app/app-connection/app-connection-service/oauth2/oauth2-util.ts:31-48), then credentialsOauth2Service.refresh() POSTs the refresh_token to the token URL with timing-safe client credential resolution through the secrets manager (packages/server/api/src/app/app-connection/app-connection-service/oauth2/services/credentials-oauth2-service.ts:108-181). A distributed lock prevents concurrent refreshes.

Credential storage encrypts the entire value object via AES-256-CBC (encryptUtils.encryptObject) using the AP_ENCRYPTION_KEY env var or an auto-generated secret (packages/server/api/src/app/helper/encryption.ts:45-47, 72-81). Three OAuth2 "flavors" exist: OAUTH2 (user-provided client), CLOUD_OAUTH2 (cloud-managed), and PLATFORM_OAUTH2 (platform-admin-managed central credentials).

Multi-tenant isolation is enforced through projectIds array columns, platformId, and scope (PROJECT/PLATFORM) — every query filters by these (packages/server/api/src/app/app-connection/app-connection-service/app-connection-service.ts:158-163, 194-198).

How is an integration / connector defined?

answered

An integration/connector is called a piece and is defined as a TypeScript class in the @activepieces/pieces-framework SDK. 764 pieces ship: 736 under packages/pieces/community/ and 28 core pieces under packages/pieces/core/.

Piece creation uses the createPiece() factory with a declarative params object: { displayName, description, auth, categories, actions: [...], triggers: [...] } (packages/pieces/framework/src/lib/piece.ts:78-80). Each piece exports its index.ts as the entry point — for example, the webhook piece is a single createPiece({ auth: PieceAuth.None(), actions: [returnResponse], triggers: [catchWebhook] }) call (packages/pieces/core/webhook/src/index.ts:7-17).

Actions are defined via createAction({ name, displayName, description, props, run }) (packages/pieces/framework/src/lib/action/action.ts:71-100). The props field accepts a declarative PiecePropertyMap of input fields: Property.Text, Property.Number, Property.File, Property.Dropdown, Property.StaticDropdown, Property.Array, Property.Object, Property.Json, Property.Markdown, and property-grouping via propertyGroups for UI layout (packages/pieces/framework/src/lib/piece-metadata.ts:82-108). Actions carry optional audience (human/ai/both), aiMetadata, and classification (READ/SEARCH/WRITE/DESTRUCTIVE).

Triggers follow the same pattern with createTrigger(...), adding type (one of TriggerStrategy: WEBHOOK, APP_WEBHOOK, POLLING, MANUAL), sampleData, optional handshakeConfiguration/renewConfiguration, and lifecycle hooks onEnable/onDisable/run (packages/pieces/framework/src/lib/trigger/trigger.ts:41-60).

Metadata schema is defined as PieceMetadata: extends PieceBase (name, displayName, logoUrl, description, authors, version, auth, categories) with actions: Record<string, ActionBase> and triggers: Record<string, TriggerBase> (packages/pieces/framework/src/lib/piece-metadata.ts:134-145). Versioning is via semver in package.json, with minimumSupportedRelease/maximumSupportedRelease constraints in the piece body.

No OpenAPI codegen is used — all pieces are hand-written TypeScript against the SDK. The packages/pieces/framework JIT-installs each piece package into the sandbox by resolving its npm package (via archive bundle from the server or npm registry).

How are integrations exposed to LLM agents?

answered

Integrations are exposed to LLM agents through two channels: MCP (Model Context Protocol) and semantic tool search.

MCP server is the primary agent interface. buildMcpServer() creates an McpServer from the @modelcontextprotocol/sdk with instructions describing the workflow DSL (step references, piece naming, auth patterns) (packages/server/api/src/app/mcp/mcp-server-builder.ts:22-39, 41-73). Two classes of MCP tools are registered: static tools (50+ Activepieces-managed operations like ap_create_flow, ap_add_step, ap_search_actions, ap_get_run, ap_test_flow) and flow tools (each MCP-triggered flow in a project becomes a tool calling its toolName, description, and input schema from the flow's trigger McpTrigger settings) (packages/server/api/src/app/mcp/mcp-server-builder.ts:202-233, 292-307). The MCP server supports OAuth2 client authentication, per-tool permission checking (McpPermissionChecker), per-call billing/usage tracking, and activity recording (packages/server/api/src/app/mcp/mcp-server-builder.ts:77-83).

Semantic tool search is powered by pgvector. toolSearchService.searchActions() and searchTriggers() embed the user query via an AI model into a vector, then run a cosine-similarity scan over the tool_search_index table: 1 - (embedding <=> $1::vector) AS cosine with tenant isolation, platform piece filtering, and audience exclusion (packages/server/api/src/app/tool-search/tool-search.service.ts:31-46, 70-141). A τ-gate (applyNoMatchGate) rejects semantically distant results. When no embedder is available or embedding fails, it degrades gracefully to a keyword search via the pieceMetadataService.list + piece suggestedActions/suggestedTriggers catalog (Fuse) (packages/server/api/src/app/tool-search/tool-search.service.ts:150-180). Results include connected status (whether the project has an active AppConnection for the piece). Each action also carries audience (human/ai/both) and classification (READ/SEARCH/WRITE/DESTRUCTIVE) for agent-appropriate filtering (packages/pieces/framework/src/lib/piece-metadata.ts:52-67).

Editor's note. Correction: semantic tool search is disabled by default (AP_TOOL_SEARCH_ENABLED=false), and with it off the ap_search_actions/ap_search_triggers tools are not registered. The generic ap_run_action tool, which runs any single piece action without a flow, is the main way an agent calls a piece directly.

How is a tool call executed?

answered

Tool call execution follows a Worker → Sandbox → Engine pipeline with sandboxed isolation.

Job dispatch: All execution starts in a BullMQ job queue. The API server enqueues jobs (e.g. WorkerJobType.EXECUTE_ACTION), and the worker picks them up via a polling loop. executeActionJob.ts handles individual action/tool calls: it resolves code steps (compiling user code into Deno/bundled JS artifacts via actionRunCache), resolves piece packages (fetching piece npm archives), provisions the sandbox with resolved pieces and code, then calls ctx.runtime.execute() (packages/server/worker/src/lib/execute/jobs/execute-action.ts:11-53).

Flow execution (executeFlowJob.ts) follows the same pattern: resolve the flow version → build an ExecuteFlowOperation (BEGIN or RESUME) → execute via sandbox runtime → report status to the API server (packages/server/worker/src/lib/execute/jobs/execute-flow.ts:12-125). The engine handles branch evaluation, step sequencing, AI model calls, and loop/approval gates internally.

Sandbox isolation uses the Linux isolate binary (from the IOI contest sandbox project), which creates a chroot-like environment with restricted filesystem mounts, process isolation, and resource limits (CPU time, memory, disk writes) (packages/server/sandbox/src/lib/sandbox/isolate.ts:1-80). The sandbox manager acquires a box, provisions caches (pieces + code), starts the engine node process, sends the operation via WebSocket (the AP_SANDBOX_WS_PORT/AP_SANDBOX_WS_TOKEN env vars), and returns the result. Timeout, memory overflow, and log-size-exceeded errors are detected and translated into structured error codes (TIMEOUT, MEMORY_LIMIT_EXCEEDED, LOG_SIZE_EXCEEDED) (packages/server/sandbox/src/lib/sandbox.ts:29-57, 66-80).

Retries and rate limiting: Actions can declare errorHandlingOptions.retryOnFailure (default false). BullMQ automatically retries INTERNAL_ERROR-terminated jobs (only if it's not the last attempt, !ctx.lastAttempt). MCP calls have per-call billing via mcpUsageTracker.resolveCallBilling(), which checks credit balance. The safeHttp.retryingAxios wrapper provides automatic retry with exponential backoff for HTTP requests (packages/server/worker/src/lib/execute/jobs/execute-flow.ts:91-97).

Error mapping: Engine statuses are mapped to run statuses — sandbox timeout → FlowRunStatus.TIMEOUT, memory issue → MEMORY_LIMIT_EXCEEDED, log overflow → LOG_SIZE_EXCEEDED, engine internal error → INTERNAL_ERROR, piece bundle not available → FAILED with a descriptive failedStep (packages/server/worker/src/lib/execute/jobs/execute-flow.ts:91-124).

Editor's note. Correction: the isolate sandbox is only used when AP_EXECUTION_MODE is SANDBOX_PROCESS or SANDBOX_CODE_AND_PROCESS; the default is UNSANDBOXED, where the engine runs as a plain Node child process (packages/server/sandbox/src/lib/create-sandbox-for-job.ts L48-L58). Workers also do not read BullMQ directly; they poll the API for jobs over a Socket.IO RPC channel.

How are data sync, webhooks and triggers implemented?

answered

Triggers implement four strategies via TriggerStrategy enum, each handling data ingestion differently.

POLLING (periodic sync): When a flow with a polling trigger is enabled, flowTriggerSideEffect.handlePollingTrigger() creates a BullMQ REPEATING job configured with the piece's scheduleOptions (or a default interval from TRIGGER_DEFAULT_POLL_INTERVAL) (packages/server/api/src/app/trigger/trigger-source/flow-trigger-side-effect.ts:183-208). The worker's executePollingJob resolves the flow, runs the trigger's RUN hook inside the sandbox, and if the hook returns output, submits the payloads via apiClient.submitPayloads() which triggers new flow runs (packages/server/worker/src/lib/execute/jobs/execute-polling.ts:9-74).

WEBHOOK (ingress per flow): Each webhook-triggered flow receives a unique URL path. The webhookController at /:flowId and /:flowId/sync handles ALL HTTP methods, supporting both async (immediate 200, queue execution) and sync (block until flow completion) modes (packages/server/api/src/app/webhooks/webhook-controller.ts:17-81). webhookService.handleWebhook() resolves the flow, optionally processes a handshake challenge, checks payload size limits, and either queues the job (async) or executes synchronously via handleSync() (packages/server/api/src/app/webhooks/webhook.service.ts:45-206).

APP_WEBHOOK (event routing): Used by pieces that send events to a central Activepieces endpoint (e.g. Slack, GitHub). When enabled, handleAppWebhookTrigger() registers listeners via appEventRoutingService.createListeners(), keyed by appName, event, and identifierValue (packages/server/api/src/app/trigger/app-event-routing/app-event-routing.service.ts:18-41). Incoming events are matched to flows by these keys.

Trigger lifecycle: triggerSourceService.enable() soft-deletes any existing trigger source for the flow, saves a new TriggerSource record, calls flowTriggerSideEffect.enable() which sends the piece's ON_ENABLE hook to the engine sandbox, and then the side-effect method creates the appropriate schedule/queue/webhook listener (packages/server/api/src/app/trigger/trigger-source/trigger-source-service.ts:17-97). Disabling reverses: calls ON_DISABLE, removes repeating jobs, and soft-deletes the source. TriggerEventService stores incoming events as files in the file service and manages pagination for UI display (packages/server/api/src/app/trigger/trigger-events/trigger-event.service.ts:16-139).

Deduplication for incoming webhook events is handled by dedupe-service.ts which implements a sliding-window dedup using Redis.

How is it self-hosted and what is open vs proprietary?

answered

Activepieces is self-hostable via Docker Compose. The docker-compose.yml defines four services: app (Fastify API server), worker (BullMQ job runners, 5 replicas), postgres (pgvector 0.8.0 on PostgreSQL 14), and redis (7.0.7) (docker-compose.yml:1-58). The app and worker use the same ghcr.io/activepieces/activepieces image with AP_CONTAINER_TYPE set to APP or WORKER. All configuration is via .env file. Required infrastructure: PostgreSQL with pgvector extension and Redis — no other external services are mandatory.

License split: The CE code (Community Edition) is available under the MIT Expat license. Everything outside packages/ee/ and packages/server/api/src/app/ee/ directories is MIT (LICENSE:1-25). The EE code is licensed under the Activepieces Enterprise License requiring a paid subscription for production use (packages/ee/LICENSE:1-20). CE and EE are kept separate through a hooksFactory pattern: CE exports a default implementation, EE hooks into it via hooksFactory.set() in app.ts, and CE code must never import from src/app/ee/.

Included in the repo: All 764 pieces (community + core), the entire engine, sandbox, worker, API server, and web frontend are present in the open repository. Proprietary components in packages/ee/ and packages/server/api/src/app/ee/ include: SSO/SAML/SCIM managed authentication, enterprise audit logs, API-keys with project scoping, OAuth apps management, managed authn (SAML/OIDC), global connections, platform webhooks, embedded project domain, alerts, secret managers, and advanced AI agent tools (cross-project tools, email, flow-gates, personalization). EE features are gated behind platform.plan.* feature flags enforced by platformMustHaveFeatureEnabled() middleware on the backend and LockedFeatureGuard on the frontend.

The cloud edition (AP_EDITION=cloud) adds Activepieces-hosted infrastructure — AppSumo integration, license key reporting, embed domain management — and is functionally similar to EE with usage-based billing. No component requires the hosted cloud to function; the open source runs fully standalone with PostgreSQL + Redis.