Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion brain/knowledge/execution-runtime/benchmark-cli.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ icon: ⏱️

# Benchmark CLI

`activepieces benchmark` load-tests a deployment's sync-webhook path and attributes *where* latency goes, so a self-hosted setup can be compared apples-to-apples against Activepieces' published reference numbers. Available in CE, EE, Cloud.
`npx @activepieces/cli benchmark` load-tests a deployment's sync-webhook path and attributes *where* latency goes, so a self-hosted setup can be compared apples-to-apples against Activepieces' published reference numbers. Available in CE, EE, Cloud.

### How it works
- Builds a `webhook → data-mapper → return-response` flow. Instead of a raw `--concurrency`, it **auto-discovers the deployment shape** (`GET /v1/worker-machines`) and drives load = the effective **execution slot** count, so a healthy deploy queues ~zero by construction. Any queue-wait it reports is a real finding (usually driven concurrency > slots).
Expand All @@ -22,6 +22,7 @@ icon: ⏱️
- **App Instance Registry** (`app-machine-cache.ts`): apps have no inbound healthcheck, so each self-registers into a Redis hash `appMachines` on its `systemSnapshot` tick; `list()` drops rows untouched >120s. Kept separate from `workerMachines` so an app is never counted as an execution slot. Write gated off on Cloud.

### Gotchas
- **CLI identity — no `ap`, no `activepieces` package.** Applies to every CLI command, not just benchmark. Published as `@activepieces/cli` on npm (`activepieces` returns 404); bin is `pieces-cli`. All user docs should invoke it as `npx @activepieces/cli <command>`. `docs/admin-guide/guides/project-replace-cli.mdx` was shipped with `npm install -g activepieces` + `ap project replace`, both fictional; this brain page had the same slip in its opening line.
- Auth is **platform API key only** (`AP_API_KEY`/`--api-key` + `--project-id`) — email/password login was removed; SERVICE principal gets the full diagnostic bundle.
- The infra round-trip block is **self-hosted only** — `/v1/health/diagnostics` returns `FEATURE_DISABLED` on `AP_EDITION=cloud` (a Cloud admin is a tenant, not the infra operator); the CLI degrades gracefully.

Expand Down
2 changes: 2 additions & 0 deletions brain/knowledge/flows-execution/triggers.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ Triggers define how and when a flow starts. The module handles registration, eve
### Gotchas
- **Deduplication** (polling): extracts `__DEDUPE_KEY_PROPERTY`, Redis INCR with 30s TTL — first passes, duplicates filtered; the dedupe key is stripped from returned payloads.
- **Republish preserves the polling checkpoint** (`isRepublish`): republishing a running flow does `onDisable(old) → onEnable(new)`, which used to reset `lastPoll`/`lastItem` to now and silently drop events created in between. `flowService.update` sets `isRepublish=true` only for a `LOCK_AND_PUBLISH` of an already-`ENABLED` flow whose trigger is unchanged — same piece, same trigger name, **and deep-equal `settings.input`** (`flowPublishUtils.isSameTrigger`); the flag is threaded through the ON_ENABLE job → `ExecuteTriggerOperation` → trigger context (`context.isRepublish`), and `pollingHelper.onEnable` then keeps the existing checkpoint. A fresh enable, a manual off→on toggle, a trigger swap, and any change to the trigger's props all still reset to now. The props check is not cosmetic: a checkpoint kept across a props change points at a resource that is no longer being polled, and `pollingHelper.poll` treats a `LAST_ITEM` id it cannot find in the fetched page (`findIndex → -1`) the same as "no checkpoint", emitting **every** item. Custom polling triggers that don't use `pollingHelper` can opt in by reading `context.isRepublish`.
- **A missing timestamp permanently kills a TIMEBASED polling trigger** — `pollingHelper.poll` advances the checkpoint with `items.reduce((acc, i) => Math.max(acc, i.epochMilliSeconds), lastPoll)`, and `Math.max(n, NaN)` is `NaN`, so a single item whose date field was never requested (`dayjs(undefined).valueOf()` → `NaN`) writes `NaN` into `lastPoll`; every later poll then filters on `> NaN` → false and the trigger silently never fires again, with no error anywhere. Two guards, both needed: request the date in the API's `fields`/select mask, and drop items with an unusable date before mapping to `epochMilliSeconds`. The filter must check the raw value, not just `.isValid()` — `dayjs(undefined)` is *now* and reports valid.
- **The timestamp you poll on may be client-supplied, and a future one is fatal.** Google Drive sets `modifiedTime` from the *local file mtime* on upload, not the upload moment, so it can be older than `createdTime` — or years ahead if the uploader's clock is fast. A TIMEBASED watermark is `max(epochMilliSeconds)` over the emitted items, so one future-dated row pushes `lastPoll` into the future and the trigger emits nothing until wall-clock catches up. Hold back items timestamped after `Date.now()` — they fire once the clock passes them. Do **not** clamp the watermark instead: a clamped watermark re-emits that same row on every poll forever.
- The **simulate flag** lets a production source and a test source coexist independently.
- **Renewal jobs** re-register expiring webhook pieces via the ON_RENEW hook.
- **`*/X` cron is not "every X minutes"** — it means "minutes divisible by X", so it double-fires at :00 and :X for X > 30 and gaps unevenly when X doesn't divide 60. Use `INTERVAL`/`intervalMs` for a rolling interval; reserve cron for wall-clock schedules. This bit the default poll schedule until GIT-1632.
Expand Down
10 changes: 5 additions & 5 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 4 additions & 13 deletions docs/admin-guide/guides/project-replace-cli.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,10 @@ description: 'Mirror a project across Activepieces deployments from CI/CD withou
icon: 'arrow-right-arrow-left'
---

The `ap project replace` CLI mirrors a project's flows, table schemas, and folders from one Activepieces deployment to another using direct API calls. Use it from CI/CD to promote work between independent staging and production instances when Git-based [Project Releases](/admin-guide/guides/project-releases) aren't a fit.
The `@activepieces/cli` `project replace` command mirrors a project's flows, table schemas, and folders from one Activepieces deployment to another using direct API calls. Use it from CI/CD to promote work between independent staging and production instances when Git-based [Project Releases](/admin-guide/guides/project-releases) aren't a fit.

<Tip>
**Use case:** A nightly GitHub Action runs `ap project replace --source-url=staging --dest-url=prod`. Staging is treated as the source of truth; production becomes a byte-for-byte mirror. The job fails before any write if the destination is missing required pieces or referenced connections.
**Use case:** A nightly GitHub Action runs `npx @activepieces/cli project replace --source-url=staging --dest-url=prod`. Staging is treated as the source of truth; production becomes a byte-for-byte mirror. The job fails before any write if the destination is missing required pieces or referenced connections.
</Tip>

## Prerequisites
Expand All @@ -27,22 +27,14 @@ The `ap project replace` CLI mirrors a project's flows, table schemas, and folde
| Connections | **Metadata auto-mirrored** (externalId, pieceName, displayName); secret values never cross the wire. New connections land on the destination as placeholders with `status: MISSING`. Operator authorizes each one in the destination UI before flows can run. |
| MCP servers, agents, project metadata, custom domains, app credentials | **Out of scope.** Not touched. |

## Installation

```bash
npm install -g activepieces
```

The `activepieces` CLI is shipped with each Activepieces release; pin the version that matches your destination instance to keep request shapes aligned.

## Command

```bash
# API keys via env vars (recommended for CI — keeps secrets out of process args and shell history)
export AP_SOURCE_API_KEY="$STAGING_API_KEY"
export AP_DEST_API_KEY="$PROD_API_KEY"

ap project replace \
npx @activepieces/cli project replace \
--source-url https://staging.activepieces.com \
--source-project "$STAGING_PROJECT_ID" \
--dest-url https://prod.activepieces.com \
Expand Down Expand Up @@ -198,9 +190,8 @@ jobs:
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm install -g activepieces
- run: |
ap project replace \
npx @activepieces/cli project replace \
--source-url "${{ vars.STAGING_URL }}" \
--source-project "${{ vars.STAGING_PROJECT_ID }}" \
--dest-url "${{ vars.PROD_URL }}" \
Expand Down
2 changes: 1 addition & 1 deletion packages/pieces/community/google-drive/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@activepieces/piece-google-drive",
"version": "0.8.5",
"version": "0.9.0",
"main": "./dist/src/index.js",
"types": "./dist/src/index.d.ts",
"dependencies": {
Expand Down
12 changes: 11 additions & 1 deletion packages/pieces/community/google-drive/src/i18n/translation.json
Original file line number Diff line number Diff line change
Expand Up @@ -314,5 +314,15 @@
"Trigger when a new file is uploaded.": "Trigger when a new file is uploaded.",
"Trigger when a new folder is created or uploaded.": "Trigger when a new folder is created or uploaded.",
"Include File Content": "Include File Content",
"Include the file content in the output. This will increase the time taken to fetch the files and might cause issues with large files.": "Include the file content in the output. This will increase the time taken to fetch the files and might cause issues with large files."
"Include the file content in the output. This will increase the time taken to fetch the files and might cause issues with large files.": "Include the file content in the output. This will increase the time taken to fetch the files and might cause issues with large files.",
"Include the file content in the output. This will increase the time taken to fetch the files and might cause issues with large files. If a download fails the event still arrives, carrying a File Content Error instead of the content.": "Include the file content in the output. This will increase the time taken to fetch the files and might cause issues with large files. If a download fails the event still arrives, carrying a File Content Error instead of the content.",
"New or Updated File": "New or Updated File",
"Trigger when a file is created or updated, checked on a schedule. Each event carries a Change Type of created or updated, and several edits between two checks arrive as a single event. Renaming a file counts as an update. Trashing a file is not an event, and neither is restoring one from the bin nor moving an existing file into the watched folder. Selecting a parent folder watches its direct children only, not sub-folders.": "Trigger when a file is created or updated, checked on a schedule. Each event carries a Change Type of created or updated, and several edits between two checks arrive as a single event. Renaming a file counts as an update. Trashing a file is not an event, and neither is restoring one from the bin nor moving an existing file into the watched folder. Selecting a parent folder watches its direct children only, not sub-folders.",
"File Types": "File Types",
"Only fire for files of these types. Leave empty to watch every type. If the type you need is not listed, switch this field to 'Dynamic value' (the toggle next to the field) and provide a list of MIME types.": "Only fire for files of these types. Leave empty to watch every type. If the type you need is not listed, switch this field to 'Dynamic value' (the toggle next to the field) and provide a list of MIME types.",
"Excel 97-2003 (XLS)": "Excel 97-2003 (XLS)",
"Plain Text (TXT)": "Plain Text (TXT)",
"PNG": "PNG",
"JPEG": "JPEG",
"ZIP": "ZIP"
}
3 changes: 2 additions & 1 deletion packages/pieces/community/google-drive/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import { googleDriveSearchFolder } from './lib/action/search-folder-or-file.acti
import { googleDriveUploadFile } from './lib/action/upload-file';
import { newFile } from './lib/triggers/new-file';
import { newFolder } from './lib/triggers/new-folder';
import { newOrUpdatedFile } from './lib/triggers/new-or-updated-file';
import { setPublicAccess } from './lib/action/set-public-access';
import { moveFileAction } from './lib/action/move-file';
import { googleDriveDeleteFile } from './lib/action/delete-file';
Expand Down Expand Up @@ -84,7 +85,7 @@ export const googleDrive = createPiece({
'abuaboud',
'geekyme'
],
triggers: [newFile, newFolder],
triggers: [newFile, newFolder, newOrUpdatedFile],
actions: [
googleDriveCreateNewFolder,
googleDriveCreateNewTextFile,
Expand Down
32 changes: 29 additions & 3 deletions packages/pieces/community/google-drive/src/lib/common/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,10 @@ export const common = {
parent?: string;
createdTime?: string | number | Date;
createdTimeOp?: string;
changedSince?: string | number | Date;
mimeTypes?: string[];
excludeShortcuts?: boolean;
maxPages?: number;
includeTeamDrive?: boolean;
},
order?: string
Expand All @@ -125,20 +129,39 @@ export const common = {
const drive = googleDrive({ version: 'v3', auth: authClient });

const q: string[] = [];
if (search?.parent) q.push(`'${search.parent}' in parents`);
if (search?.parent)
q.push(`'${escapeDriveQueryLiteral(search.parent)}' in parents`);
if (search?.createdTime)
q.push(
`createdTime ${search.createdTimeOp ?? '>'} '${dayjs(
search.createdTime
).format()}'`
);
if (search?.changedSince) {
const changedSince = dayjs(search.changedSince).format();
q.push(
`(modifiedTime > '${changedSince}' or createdTime > '${changedSince}')`
);
}
if (search?.mimeTypes?.length)
q.push(
`(${search.mimeTypes
.map(
(mimeType) => `mimeType='${escapeDriveQueryLiteral(mimeType)}'`
)
.join(' or ')})`
);
if (search?.excludeShortcuts)
q.push(`mimeType!='application/vnd.google-apps.shortcut'`);
q.push(`trashed = false`);
const allFiles: any[] = [];
let pageToken: string | undefined = undefined;
let pagesFetched = 0;
do {
const listParams: Record<string, any> = {
q: q.concat("mimeType!='application/vnd.google-apps.folder'").join(' and '),
fields: 'nextPageToken, files(id, name, mimeType, webViewLink, kind, createdTime)',
fields:
'nextPageToken, files(id, name, mimeType, webViewLink, kind, createdTime, modifiedTime)',
orderBy: order ?? 'createdTime desc',
supportsAllDrives: true,
includeItemsFromAllDrives: search?.includeTeamDrive,
Expand All @@ -148,6 +171,8 @@ export const common = {
const response = await drive.files.list(listParams);
allFiles.push(...(response.data.files ?? []));
pageToken = response.data.nextPageToken ?? undefined;
pagesFetched += 1;
if (search?.maxPages && pagesFetched >= search.maxPages) break;
} while (pageToken);

return allFiles;
Expand All @@ -168,7 +193,8 @@ export const common = {
const drive = googleDrive({ version: 'v3', auth: authClient });

const q: string[] = [`mimeType='application/vnd.google-apps.folder'`];
if (search?.parent) q.push(`'${search.parent}' in parents`);
if (search?.parent)
q.push(`'${escapeDriveQueryLiteral(search.parent)}' in parents`);
if (search?.createdTime)
q.push(
`createdTime ${search.createdTimeOp ?? '>'} '${dayjs(
Expand Down
49 changes: 49 additions & 0 deletions packages/pieces/community/google-drive/src/lib/output-schemas.ts
Original file line number Diff line number Diff line change
Expand Up @@ -456,6 +456,11 @@ export const newFileTriggerOutputSchema: OutputSchema = {
key: 'name',
label: 'File Name',
},
{
key: 'modifiedTime',
label: 'Modified Time',
format: 'datetime',
},
{
key: 'webViewLink',
label: 'View Link',
Expand All @@ -477,6 +482,50 @@ export const newFileTriggerOutputSchema: OutputSchema = {
],
};

export const newOrUpdatedFileTriggerOutputSchema: OutputSchema = {
fields: [
{
key: 'changeType',
label: 'Change Type',
},
{
key: 'content',
label: 'File Content',
},
{
key: 'contentError',
label: 'File Content Error',
},
{
key: 'name',
label: 'File Name',
},
{
key: 'webViewLink',
label: 'View Link',
format: 'url',
},
{
key: 'mimeType',
label: 'MIME Type',
},
{
key: 'createdTime',
label: 'Created Time',
format: 'datetime',
},
{
key: 'modifiedTime',
label: 'Modified Time',
format: 'datetime',
},
{
key: 'id',
label: 'File ID',
},
],
};

export const newFolderTriggerOutputSchema: OutputSchema = {
fields: [
{
Expand Down
Loading
Loading