From ca62fc940870a44d825994b276fed69f17c3a768 Mon Sep 17 00:00:00 2001 From: Talal Jaber Date: Tue, 22 Sep 2026 23:41:11 +0300 Subject: [PATCH 1/3] feat(wordpress): step form (#15680) --- .../pieces/community/wordpress/package.json | 2 +- .../pieces/community/wordpress/src/index.ts | 31 +++++----- .../src/lib/actions/create-page.action.ts | 45 +++++++++----- .../src/lib/actions/create-post.action.ts | 50 +++++++++------- .../src/lib/actions/get-post.action.ts | 4 +- .../src/lib/actions/update-post.action.ts | 52 +++++++++------- .../wordpress/src/lib/common/index.ts | 60 +++++++++++-------- 7 files changed, 144 insertions(+), 100 deletions(-) diff --git a/packages/pieces/community/wordpress/package.json b/packages/pieces/community/wordpress/package.json index 682ade7343c0..d5eeb06c8a6e 100644 --- a/packages/pieces/community/wordpress/package.json +++ b/packages/pieces/community/wordpress/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-wordpress", - "version": "0.5.0", + "version": "0.6.0", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { diff --git a/packages/pieces/community/wordpress/src/index.ts b/packages/pieces/community/wordpress/src/index.ts index f3efd1bfba40..512bd6881c36 100644 --- a/packages/pieces/community/wordpress/src/index.ts +++ b/packages/pieces/community/wordpress/src/index.ts @@ -19,14 +19,13 @@ import { wordpressNewPost } from './lib/trigger/new-post.trigger'; import { updateWordPressPost } from './lib/actions/update-post.action'; const markdownPropertyDescription = ` -**Enable Basic Authentication:** +Connect with a WordPress **application password**: -1. Download the plugin from: https://github.com/WP-API/Basic-Auth (Click on Code -> Download Zip) -2. Log in to your WordPress dashboard. -3. Go to "Plugins" and click "Add New." -4. Choose "Upload Plugin" and select the downloaded file. -5. Install and activate the plugin. +1. In your WordPress admin, open **Users → Profile**. +2. Under **Application Passwords**, type a name and click **Add New Application Password**. +3. Copy the generated password and paste it below together with your username. +Your site must use HTTPS: the password travels with every request. On WordPress older than 5.6, install the [Basic Auth plugin](https://github.com/WP-API/Basic-Auth) and use your login password instead, still over HTTPS. `; export const wordpressAuth = PieceAuth.CustomAuth({ @@ -36,16 +35,19 @@ export const wordpressAuth = PieceAuth.CustomAuth({ username: Property.ShortText({ displayName: 'Username', required: true, + description: 'The WordPress user the flow acts as.', }), password: PieceAuth.SecretText({ displayName: 'Password', required: true, + description: + 'Application password, or the login password with the Basic Auth plugin.', }), website_url: Property.ShortText({ displayName: 'Website URL', required: true, - description: - 'URL of the wordpress url i.e https://www.example-website.com', + description: 'Address of your WordPress site.', + placeholder: 'https://example.com', }), }, validate: async ({ auth }) => { @@ -53,7 +55,7 @@ export const wordpressAuth = PieceAuth.CustomAuth({ if (!username || !password || !website_url) { return { valid: false, - error: 'please fill all the fields [username, password, website_url] ', + error: 'Please fill in the username, password and website URL.', }; } if (!wordpressCommon.isBaseUrl(website_url.trim())) { @@ -69,13 +71,13 @@ export const wordpressAuth = PieceAuth.CustomAuth({ if (!apiEnabled) { return { valid: false, - error: `REST API is not reachable, visit ${website_url.trim()}/wp-json" \n make sure your settings (Settings -> Permalinks) are set to "Post name" (or any option other than "Plain") and disable any security plugins that might block the REST API `, + error: `REST API is not reachable at ${website_url.trim()}/wp-json. In Settings → Permalinks pick any option other than "Plain", and disable security plugins that block the REST API.`, }; } try { const request: HttpRequest = { method: HttpMethod.GET, - url: `${website_url}/wp-json/wp/v2/categories`, + url: `${website_url.trim()}/wp-json/wp/v2/categories`, authentication: { type: AuthenticationType.BASIC, username: username, @@ -86,10 +88,11 @@ export const wordpressAuth = PieceAuth.CustomAuth({ return { valid: true, }; - } catch (e: any) { + } catch (e) { + const message = e instanceof Error ? e.message : String(e); return { valid: false, - error: 'Credentials are invalid. ' + e?.message, + error: 'Credentials are invalid. ' + message, }; } }, @@ -99,7 +102,7 @@ export const wordpress = createPiece({ displayName: 'WordPress', description: 'Open-source website creation software', - minimumSupportedRelease: '0.30.0', + minimumSupportedRelease: '0.88.2', logoUrl: 'https://cdn.activepieces.com/pieces/wordpress.png', categories: [PieceCategory.MARKETING], auth: wordpressAuth, diff --git a/packages/pieces/community/wordpress/src/lib/actions/create-page.action.ts b/packages/pieces/community/wordpress/src/lib/actions/create-page.action.ts index df969abc4b3a..c8a69adb6bef 100644 --- a/packages/pieces/community/wordpress/src/lib/actions/create-page.action.ts +++ b/packages/pieces/community/wordpress/src/lib/actions/create-page.action.ts @@ -13,33 +13,23 @@ export const createWordPressPage = createAction({ auth: wordpressAuth, name: 'create_page', classification: 'WRITE', - description: 'Create new page on WordPress', + description: 'Add a new page to your WordPress site', audience: 'both', aiMetadata: { description: 'Publishes a new static page (not a blog post) on a WordPress site via the REST API, with optional status, slug, excerpt, and comment settings. Choose this for standalone pages like About or Contact rather than dated posts. Requires a title and HTML content; not idempotent — each call creates a separate page.', idempotent: false }, displayName: 'Create Page', outputSchema: createPageActionOutputSchema, props: { title: Property.ShortText({ - description: 'Title of the page about to be added', displayName: 'Title', required: true, }), content: Property.LongText({ - description: 'Uses the WordPress Text Editor which supports HTML', + description: 'Body of the page. HTML is allowed.', displayName: 'Content', required: true, }), - slug: Property.ShortText({ - displayName: 'Slug', - required: false, - }), - date: Property.ShortText({ - description: 'Page publish date (ISO-8601)', - displayName: 'Date', - required: false, - }), status: Property.StaticDropdown({ - description: 'Choose status', + description: 'Publish now, schedule it, or save as a draft.', displayName: 'Status', required: false, options: { @@ -53,17 +43,35 @@ export const createWordPressPage = createAction({ ], }, }), + date: Property.ShortText({ + description: "Publish date and time in the site's timezone.", + displayName: 'Publish Date', + placeholder: '2026-09-21T09:00:00', + required: false, + }), + slug: Property.ShortText({ + advanced: true, + description: 'Last part of the URL.', + displayName: 'Slug', + placeholder: 'my-first-post', + required: false, + }), excerpt: Property.LongText({ - description: 'Uses the WordPress Text Editor which supports HTML', + advanced: true, + description: 'Short summary shown in listings. HTML is allowed.', displayName: 'Excerpt', required: false, }), comment_status: Property.Checkbox({ - displayName: 'Enable Comments', + advanced: true, + description: 'On lets readers comment. Off leaves the setting as is.', + displayName: 'Allow Comments', required: false, }), ping_status: Property.Checkbox({ - displayName: 'Open to Pinging', + advanced: true, + description: 'On accepts pingbacks and trackbacks. Off leaves it as is.', + displayName: 'Allow Pingbacks', required: false, }), }, @@ -80,6 +88,11 @@ export const createWordPressPage = createAction({ ? 'open' : 'closed'; } + if (context.propsValue.ping_status) { + requestBody['ping_status'] = context.propsValue.ping_status + ? 'open' + : 'closed'; + } if (context.propsValue.slug) { requestBody['slug'] = context.propsValue.slug; } diff --git a/packages/pieces/community/wordpress/src/lib/actions/create-post.action.ts b/packages/pieces/community/wordpress/src/lib/actions/create-post.action.ts index 75332f0c8bb2..862fab239f14 100644 --- a/packages/pieces/community/wordpress/src/lib/actions/create-post.action.ts +++ b/packages/pieces/community/wordpress/src/lib/actions/create-post.action.ts @@ -17,53 +17,61 @@ export const createWordPressPost = createAction({ auth: wordpressAuth, name: 'create_post', classification: 'WRITE', - description: 'Create new post on WordPress', + description: 'Add a new post to your WordPress site', audience: 'both', aiMetadata: { description: 'Publishes a new blog post on a WordPress site via the REST API, with optional status (draft/publish/etc.), categories, tags, excerpt, featured image, and custom ACF fields. Choose this to add fresh content; for editing an existing post use Update Post. Requires a title and HTML content; not idempotent — each call creates a separate post.', idempotent: false }, displayName: 'Create Post', outputSchema: createPostActionOutputSchema, props: { title: Property.ShortText({ - description: 'Title of the post about to be added', displayName: 'Title', required: true, }), content: Property.LongText({ - description: 'Uses the WordPress Text Editor which supports HTML', + description: 'Body of the post. HTML is allowed.', displayName: 'Content', required: true, }), - slug: Property.ShortText({ - displayName: 'Slug', - required: false, - }), + status: wordpressCommon.status, date: Property.ShortText({ - description: 'Post publish date (ISO-8601)', - displayName: 'Date', - required: false, - }), - featured_media_file: wordpressCommon.featured_media_file, - tags: wordpressCommon.tags, - acfFields: Property.Object({ - displayName: 'Custom ACF fields', - description: - 'Provide field name with value.You can find out field name from ACF plugin menu.', + description: "Publish date and time in the site's timezone.", + displayName: 'Publish Date', + placeholder: '2026-09-21T09:00:00', required: false, }), categories: wordpressCommon.categories, + tags: wordpressCommon.tags, featured_media: wordpressCommon.featured_media, - status: wordpressCommon.status, + featured_media_file: wordpressCommon.featured_media_file, excerpt: Property.LongText({ - description: 'Uses the WordPress Text Editor which supports HTML', + description: 'Short summary shown in listings. HTML is allowed.', displayName: 'Excerpt', required: false, }), + slug: Property.ShortText({ + advanced: true, + description: 'Last part of the URL.', + displayName: 'Slug', + placeholder: 'my-first-post', + required: false, + }), + acfFields: Property.Object({ + advanced: true, + displayName: 'ACF Fields', + description: + 'Field names and values for the Advanced Custom Fields plugin.', + required: false, + }), comment_status: Property.Checkbox({ - displayName: 'Enable Comments', + advanced: true, + description: 'On lets readers comment. Off leaves the setting as is.', + displayName: 'Allow Comments', required: false, }), ping_status: Property.Checkbox({ - displayName: 'Open to Pinging', + advanced: true, + description: 'On accepts pingbacks and trackbacks. Off leaves it as is.', + displayName: 'Allow Pingbacks', required: false, }), }, diff --git a/packages/pieces/community/wordpress/src/lib/actions/get-post.action.ts b/packages/pieces/community/wordpress/src/lib/actions/get-post.action.ts index 4e53cd42752d..6afe953a9345 100644 --- a/packages/pieces/community/wordpress/src/lib/actions/get-post.action.ts +++ b/packages/pieces/community/wordpress/src/lib/actions/get-post.action.ts @@ -17,14 +17,14 @@ export const getWordPressPost = createAction({ auth: wordpressAuth, name: 'get_post', classification: 'READ', - description: 'Get a post from WordPress', + description: 'Fetch one post by its ID', audience: 'both', aiMetadata: { description: 'Fetches a single WordPress post by its numeric post ID via the REST API. Use it to read the current content, status, or metadata of a known post before acting on it. Requires the post ID; read-only and idempotent.', idempotent: true }, displayName: 'Get Post Details', outputSchema: getPostActionOutputSchema, props: { id: Property.Number({ - description: 'The ID of the post to get', + description: "Number after post= in the post's edit URL.", displayName: 'Post ID', required: true, }), diff --git a/packages/pieces/community/wordpress/src/lib/actions/update-post.action.ts b/packages/pieces/community/wordpress/src/lib/actions/update-post.action.ts index a4e4a138e63b..b5203a7c2319 100644 --- a/packages/pieces/community/wordpress/src/lib/actions/update-post.action.ts +++ b/packages/pieces/community/wordpress/src/lib/actions/update-post.action.ts @@ -13,7 +13,7 @@ export const updateWordPressPost = createAction({ auth: wordpressAuth, name: 'update_post', classification: 'WRITE', - description: 'Update an existing post on WordPress.', + description: 'Change the title, content or settings of an existing post', audience: 'both', aiMetadata: { description: 'Updates an existing WordPress post identified by its post ID, changing only the fields you supply (title, content, status, categories, tags, excerpt, featured image, or ACF fields). Choose this to edit or republish a known post rather than create a new one. Requires the target post ID; idempotent — repeating with the same input leaves the post in the same final state.', idempotent: true }, displayName: 'Update Post', @@ -21,46 +21,56 @@ export const updateWordPressPost = createAction({ props: { post: wordpressCommon.post, title: Property.ShortText({ - description: 'Title of the post about to be added', + description: 'New title. Empty keeps the current one.', displayName: 'Title', required: false, }), content: Property.LongText({ - description: 'Uses the WordPress Text Editor which supports HTML', + description: 'New body, HTML allowed. Empty keeps the current one.', displayName: 'Content', required: false, }), - slug: Property.ShortText({ - displayName: 'Slug', + status: wordpressCommon.status, + categories: wordpressCommon.categories, + tags: wordpressCommon.tags, + featured_media: wordpressCommon.featured_media, + featured_media_file: wordpressCommon.featured_media_file, + excerpt: Property.LongText({ + description: 'Short summary shown in listings. HTML is allowed.', + displayName: 'Excerpt', required: false, }), date: Property.ShortText({ - description: 'Post publish date (ISO-8601)', - displayName: 'Date', + advanced: true, + description: "Publish date and time in the site's timezone.", + displayName: 'Publish Date', + placeholder: '2026-09-21T09:00:00', required: false, }), - featured_media_file: wordpressCommon.featured_media_file, - tags: wordpressCommon.tags, - acfFields: Property.Object({ - displayName: 'Custom ACF fields', - description: - 'Provide field name with value.You can find out field name from ACF plugin menu.', + slug: Property.ShortText({ + advanced: true, + description: 'Last part of the URL.', + displayName: 'Slug', + placeholder: 'my-first-post', required: false, }), - categories: wordpressCommon.categories, - featured_media: wordpressCommon.featured_media, - status: wordpressCommon.status, - excerpt: Property.LongText({ - description: 'Uses the WordPress Text Editor which supports HTML', - displayName: 'Excerpt', + acfFields: Property.Object({ + advanced: true, + displayName: 'ACF Fields', + description: + 'Field names and values for the Advanced Custom Fields plugin.', required: false, }), comment_status: Property.Checkbox({ - displayName: 'Enable Comments', + advanced: true, + description: 'On lets readers comment. Off leaves the setting as is.', + displayName: 'Allow Comments', required: false, }), ping_status: Property.Checkbox({ - displayName: 'Open to Pinging', + advanced: true, + description: 'On accepts pingbacks and trackbacks. Off leaves it as is.', + displayName: 'Allow Pingbacks', required: false, }), }, diff --git a/packages/pieces/community/wordpress/src/lib/common/index.ts b/packages/pieces/community/wordpress/src/lib/common/index.ts index e58929db6f9e..96875c24bf55 100644 --- a/packages/pieces/community/wordpress/src/lib/common/index.ts +++ b/packages/pieces/community/wordpress/src/lib/common/index.ts @@ -16,13 +16,14 @@ const PAGE_HEADER = 'x-wp-totalpages'; export const wordpressCommon = { featured_media_file: Property.File({ - displayName: 'Featured Media (URL)', + displayName: 'Featured Image File', required: false, - description: 'URL of featured media', + description: 'Upload an image or paste its URL. Overrides Featured Image.', }), authors: Property.Dropdown({ auth: wordpressAuth, - displayName: 'Authors', + displayName: 'Author', + description: 'Only posts by this author. Empty means any author.', required: false, refreshers: [], options: async ({ auth }) => { @@ -38,7 +39,7 @@ export const wordpressCommon = { if (!connection?.username || !connection?.password || !websiteUrl) { return { disabled: true, - placeholder: 'Connect your account first', + placeholder: 'Please connect your account first', options: [], }; } @@ -50,6 +51,7 @@ export const wordpressCommon = { username: connection.username, password: connection.password, }, + queryParams: { per_page: '100' }, }; const response = await httpClient.sendRequest< { id: string; name: string }[] @@ -63,7 +65,7 @@ export const wordpressCommon = { }), tags: Property.MultiSelectDropdown({ auth: wordpressAuth, - description: 'Post tags', + description: 'Pick existing tags. Create new ones in WordPress.', displayName: 'Tags', required: false, refreshers: [], @@ -80,7 +82,7 @@ export const wordpressCommon = { if (!(await wordpressCommon.urlExists(connection.website_url.trim()))) { return { disabled: true, - placeholder: 'Incorrect website url', + placeholder: 'Website URL is not reachable', options: [], }; } @@ -97,7 +99,7 @@ export const wordpressCommon = { let tags = await wordpressCommon.getTags(getTagsParams); while (hasNext) { result.push(...tags.tags); - hasNext = pageCursor <= tags.totalPages; + hasNext = pageCursor < tags.totalPages; if (hasNext) { pageCursor++; tags = await wordpressCommon.getTags({ @@ -110,7 +112,7 @@ export const wordpressCommon = { return { disabled: true, options: [], - placeholder: 'Please add tags from your admin dashboard', + placeholder: 'No tags found. Create some in WordPress first.', }; } const options = result.map((res) => { @@ -127,7 +129,8 @@ export const wordpressCommon = { }), categories: Property.MultiSelectDropdown({ auth: wordpressAuth, - description: 'Post categories', + description: + 'Pick existing categories. Create new ones in WordPress.', displayName: 'Categories', required: false, refreshers: [], @@ -143,7 +146,7 @@ export const wordpressCommon = { if (!(await wordpressCommon.urlExists(connection.website_url.trim()))) { return { disabled: true, - placeholder: 'Incorrect website url', + placeholder: 'Website URL is not reachable', options: [], }; } @@ -153,7 +156,6 @@ export const wordpressCommon = { websiteUrl: connection.website_url, username: connection.username, password: connection.password, - perPage: 10, page: pageCursor, }; const result: { id: string; name: string }[] = []; @@ -161,7 +163,7 @@ export const wordpressCommon = { let hasNext = true; while (hasNext) { result.push(...categories.categories); - hasNext = pageCursor <= categories.totalPages; + hasNext = pageCursor < categories.totalPages; if (hasNext) { pageCursor++; categories = await wordpressCommon.getCategories({ @@ -174,7 +176,7 @@ export const wordpressCommon = { return { disabled: true, options: [], - placeholder: 'Please add categories from your admin dashboard', + placeholder: 'No categories found. Create some in WordPress first.', }; } const options = result.map((res) => { @@ -191,8 +193,8 @@ export const wordpressCommon = { }), featured_media: Property.Dropdown({ auth: wordpressAuth, - description: 'Choose from one of your uploaded media files', - displayName: 'Featured Media (image)', + description: 'Pick an image already in your media library.', + displayName: 'Featured Image', required: false, refreshers: [], options: async ({ auth }) => { @@ -207,7 +209,7 @@ export const wordpressCommon = { if (!(await wordpressCommon.urlExists(connection.website_url.trim()))) { return { disabled: true, - placeholder: 'Incorrect website url', + placeholder: 'Website URL is not reachable', options: [], }; } @@ -220,21 +222,25 @@ export const wordpressCommon = { page: pageCursor, }; const result: WordPressMedia[] = []; + let hasNext = true; let media = await wordpressCommon.getMedia(getMediaParams); - if (media.totalPages === 0) { - result.push(...media.media); - } - while (media.media.length > 0 && pageCursor <= media.totalPages) { + while (hasNext) { result.push(...media.media); - pageCursor++; - media = await wordpressCommon.getMedia(getMediaParams); + hasNext = pageCursor < media.totalPages; + if (hasNext) { + pageCursor++; + media = await wordpressCommon.getMedia({ + ...getMediaParams, + page: pageCursor, + }); + } } if (result.length === 0) { return { disabled: true, options: [], placeholder: - 'Please add an image to your media from your admin dashboard', + 'No images found. Upload some to the media library first.', }; } const options = result.map((res) => { @@ -250,7 +256,7 @@ export const wordpressCommon = { }, }), status: Property.StaticDropdown({ - description: 'Choose post status', + description: 'Publish now, schedule it, or save as a draft.', displayName: 'Status', required: false, options: { @@ -283,7 +289,7 @@ export const wordpressCommon = { if (!connection?.username || !connection?.password || !websiteUrl) { return { disabled: true, - placeholder: 'Connect your account first', + placeholder: 'Please connect your account first', options: [], }; } @@ -348,6 +354,7 @@ export const wordpressCommon = { order: 'desc', before: new Date().toISOString(), after: params.afterDate, + per_page: '100', page: params.page.toString(), }; if (params.authors) { @@ -382,6 +389,7 @@ export const wordpressCommon = { method: HttpMethod.GET, url: `${params.websiteUrl}/wp-json/wp/v2/media`, queryParams: { + per_page: '100', page: params.page.toString(), }, authentication: { @@ -409,6 +417,7 @@ export const wordpressCommon = { method: HttpMethod.GET, url: `${params.websiteUrl}/wp-json/wp/v2/tags`, queryParams: { + per_page: '100', page: params.page.toString(), }, authentication: { @@ -443,6 +452,7 @@ export const wordpressCommon = { password: params.password, }, queryParams: { + per_page: '100', page: params.page.toString(), }, }; From 0ba4be0d1a3ef0b9354ade81b6f97e407f020a33 Mon Sep 17 00:00:00 2001 From: Talal Jaber Date: Tue, 22 Sep 2026 23:58:05 +0300 Subject: [PATCH 2/3] feat(tables): improve the step forms (#15287) --- packages/pieces/core/tables/package.json | 2 +- .../core/tables/src/i18n/translation.json | 48 +++++++++---------- packages/pieces/core/tables/src/index.ts | 2 +- .../tables/src/lib/actions/clear-table.ts | 2 +- .../tables/src/lib/actions/create-records.ts | 27 ++++++----- .../tables/src/lib/actions/create-table.ts | 5 +- .../tables/src/lib/actions/delete-record.ts | 6 +-- .../tables/src/lib/actions/download-table.ts | 5 +- .../tables/src/lib/actions/find-records.ts | 36 +++++++------- .../core/tables/src/lib/actions/get-record.ts | 4 +- .../tables/src/lib/actions/update-record.ts | 19 +++++--- .../core/tables/src/lib/common/index.ts | 9 ++-- 12 files changed, 89 insertions(+), 76 deletions(-) diff --git a/packages/pieces/core/tables/package.json b/packages/pieces/core/tables/package.json index 868a0da3ca15..7e3c0cc0537c 100644 --- a/packages/pieces/core/tables/package.json +++ b/packages/pieces/core/tables/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-tables", - "version": "0.5.1", + "version": "0.5.2", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { diff --git a/packages/pieces/core/tables/src/i18n/translation.json b/packages/pieces/core/tables/src/i18n/translation.json index 0d5a7002af5a..6c17e8bfa13a 100644 --- a/packages/pieces/core/tables/src/i18n/translation.json +++ b/packages/pieces/core/tables/src/i18n/translation.json @@ -7,35 +7,35 @@ "Get Record": "Get Record", "Find Records": "Find Records", "Clear Table": "Clear Table", - "Insert one or more new records to a table.": "Insert one or more new records to a table.", - "Delete record(s) from a table": "Delete record(s) from a table", - "Update values in an existing record": "Update values in an existing record", - "Get single record by its id.": "Get single record by its id.", - "Find records in a table with filters.": "Find records in a table with filters.", - "Delete all records from a table": "Delete all records from a table", + "Download Table": "Download Table", "Create a new table, optionally with fields.": "Create a new table, optionally with fields.", "Delete a table and all of its records.": "Delete a table and all of its records.", - "Fields": "Fields", - "The columns to create. Leave empty to create a table with no fields.": "The columns to create. Leave empty to create a table with no fields.", - "Field Name": "Field Name", - "Type": "Type", - "Options": "Options", - "Comma-separated options, used only when Type is Single Select.": "Comma-separated options, used only when Type is Single Select.", - "Records (Raw)": "Records (Raw)", - "Advanced: provide records as a JSON array of objects keyed by field name, e.g. [{\"Name\":\"John\",\"Age\":30}]. Use this when the table is selected dynamically (its columns are unknown at build time). When set, this overrides the Records form above.": "Advanced: provide records as a JSON array of objects keyed by field name, e.g. [{\"Name\":\"John\",\"Age\":30}]. Use this when the table is selected dynamically (its columns are unknown at build time). When set, this overrides the Records form above.", + "Insert one or more new records into a table.": "Insert one or more new records into a table.", + "Delete record(s) from a table.": "Delete record(s) from a table.", + "Update values in an existing record.": "Update values in an existing record.", + "Get a single record by its ID.": "Get a single record by its ID.", + "Find records in a table with filters.": "Find records in a table with filters.", + "Delete all records from a table.": "Delete all records from a table.", + "Export a table as a CSV file.": "Export a table as a CSV file.", "Table Name": "Table Name", - "Records": "Records", - "Records IDs": "Records IDs", - "Record ID": "Record ID", + "Fields": "Fields", + "Table": "Table", "Values": "Values", - "Limit": "Limit", + "Records as JSON": "Records as JSON", + "Record IDs": "Record IDs", + "Record ID": "Record ID", "Filters": "Filters", - "The records to create.": "The records to create.", - "The IDs of the records to delete": "The IDs of the records to delete", - "The ID of the record to do the action on.": "The ID of the record to do the action on.", - "The values to update. Leave empty to keep current value.": "The values to update. Leave empty to keep current value.", - "Maximum number of records to return (default no limit).": "Maximum number of records to return (default no limit).", - "Filter conditions to apply": "Filter conditions to apply", + "Limit": "Limit", + "Include Headers": "Include Headers", + "The columns to create. Leave empty to create a table with no fields.": "The columns to create. Leave empty to create a table with no fields.", + "One entry per record to insert.": "One entry per record to insert.", + "Array of objects keyed by field name. Overrides the form above.": "Array of objects keyed by field name. Overrides the form above.", + "Usually mapped from a Find Records step or a table trigger.": "Usually mapped from a Find Records step or a table trigger.", + "Usually comes from a Find Records step or a table trigger.": "Usually comes from a Find Records step or a table trigger.", + "Leave a field empty to keep its current value.": "Leave a field empty to keep its current value.", + "All conditions must match. Empty returns every record.": "All conditions must match. Empty returns every record.", + "Maximum records to return. Empty returns all records.": "Maximum records to return. Empty returns all records.", + "Adds the column names as the first row of the CSV.": "Adds the column names as the first row of the CSV.", "New Record Created": "New Record Created", "Record Updated": "Record Updated", "Record Deleted": "Record Deleted", diff --git a/packages/pieces/core/tables/src/index.ts b/packages/pieces/core/tables/src/index.ts index ec7cf27281c0..5e71702df78f 100644 --- a/packages/pieces/core/tables/src/index.ts +++ b/packages/pieces/core/tables/src/index.ts @@ -17,7 +17,7 @@ export const tables = createPiece({ displayName: 'Tables', logoUrl: 'https://cdn.activepieces.com/pieces/new-core/tables.svg', categories: [PieceCategory.CORE], - minimumSupportedRelease: '0.80.0', + minimumSupportedRelease: '0.88.2', authors: ['amrdb'], auth: PieceAuth.None(), actions: [createTable, deleteTable, createRecords, deleteRecord, updateRecord, getRecord, findRecords, clearTable, downloadTable], diff --git a/packages/pieces/core/tables/src/lib/actions/clear-table.ts b/packages/pieces/core/tables/src/lib/actions/clear-table.ts index c3d21935311c..7649ed047f31 100644 --- a/packages/pieces/core/tables/src/lib/actions/clear-table.ts +++ b/packages/pieces/core/tables/src/lib/actions/clear-table.ts @@ -8,7 +8,7 @@ export const clearTable = createAction({ name: 'tables-clear-table', classification: 'DESTRUCTIVE', displayName: 'Clear Table', - description: 'Delete all records from a table', + description: 'Delete all records from a table.', aiMetadata: { description: 'Removes every record from an Activepieces Table while leaving the table and its field/column definitions intact. Pick this to empty a table before reloading it; use Delete Record(s) to remove specific rows by ID, or Delete Table to drop the table itself. Requires the table ID and deletes all rows with no filter or confirmation step; idempotent, since the table ends up empty no matter how many times it runs.', idempotent: true }, auth: PieceAuth.None(), props: { diff --git a/packages/pieces/core/tables/src/lib/actions/create-records.ts b/packages/pieces/core/tables/src/lib/actions/create-records.ts index bc204d537579..23623fb1e0b8 100644 --- a/packages/pieces/core/tables/src/lib/actions/create-records.ts +++ b/packages/pieces/core/tables/src/lib/actions/create-records.ts @@ -1,4 +1,4 @@ -import { createAction, MarkdownVariant, PieceAuth, Property } from '@activepieces/pieces-framework'; +import { createAction, DynamicPropsValue, MarkdownVariant, PieceAuth, Property } from '@activepieces/pieces-framework'; import { AuthenticationType, httpClient, HttpMethod, propsValidation } from '@activepieces/pieces-common'; import { CreateRecordsRequest } from '@activepieces/pieces-framework'; import { tablesCommon } from '../common'; @@ -9,21 +9,26 @@ export const createRecords = createAction({ name: 'tables-create-records', classification: 'WRITE', displayName: 'Create Record(s)', - description: 'Insert one or more new records to a table.', + description: 'Insert one or more new records into a table.', aiMetadata: { description: 'Inserts one or more new rows into an Activepieces Table via either a per-field form built from the columns of the selected table, or a raw JSON array of objects keyed by column name that overrides the form, for when the table is chosen dynamically at runtime. Use Update Record instead to change a row that already exists. Requires a table ID, and any value whose key does not match a real column is silently dropped; not idempotent, since every call appends new records with new IDs.', idempotent: false }, auth: PieceAuth.None(), props: { table_id: tablesCommon.table_id, values: Property.DynamicProperties({ auth: PieceAuth.None(), - displayName: 'Records', - description: 'The records to create.', + displayName: 'Values', + description: 'One entry per record to insert.', required: true, refreshers: ['table_id'], - props: async ({ table_id }, context) => { + props: async ({ table_id }, context): Promise => { const tableExternalId = table_id as unknown as string; if ((tableExternalId ?? '').toString().length === 0) { - return {}; + return { + markdown: Property.MarkDown({ + value: 'Select a table to load its fields.', + variant: MarkdownVariant.INFO, + }), + }; } try { const tableId = await tablesCommon.convertTableExternalIdToId(tableExternalId, context); @@ -36,7 +41,7 @@ export const createRecords = createAction({ return { values: Property.Array({ displayName: 'Records', - description: 'Add one or more records to insert', + description: 'Add one or more records to insert.', required: true, properties: fields, }), @@ -45,7 +50,7 @@ export const createRecords = createAction({ return { markdown: Property.MarkDown({ value: - "Couldn't load the fields for this table. If it's selected dynamically (e.g. from a previous step), use the \"Records (Raw)\" field below to provide records as JSON.", + "Couldn't load the fields for this table. If the table is chosen dynamically (e.g. from a previous step), open Advanced and fill in Records as JSON.", variant: MarkdownVariant.INFO, }), }; @@ -53,10 +58,10 @@ export const createRecords = createAction({ }, }), records: Property.Json({ - displayName: 'Records (Raw)', - description: - 'Advanced: provide records as a JSON array of objects keyed by field name, e.g. [{"Name":"John","Age":30}]. Use this when the table is selected dynamically (its columns are unknown at build time). When set, this overrides the Records form above.', + displayName: 'Records as JSON', + description: 'Array of objects keyed by field name. Overrides the form above.', required: false, + advanced: true, }), }, outputSchema: createRecordsActionOutputSchema, diff --git a/packages/pieces/core/tables/src/lib/actions/create-table.ts b/packages/pieces/core/tables/src/lib/actions/create-table.ts index 845d5d47b69b..70185d1eda96 100644 --- a/packages/pieces/core/tables/src/lib/actions/create-table.ts +++ b/packages/pieces/core/tables/src/lib/actions/create-table.ts @@ -15,6 +15,7 @@ export const createTable = createAction({ name: Property.ShortText({ displayName: 'Table Name', required: true, + placeholder: 'Customers', }), fields: Property.Array({ displayName: 'Fields', @@ -24,6 +25,7 @@ export const createTable = createAction({ name: Property.ShortText({ displayName: 'Field Name', required: true, + placeholder: 'Email', }), type: Property.StaticDropdown({ displayName: 'Type', @@ -41,8 +43,9 @@ export const createTable = createAction({ }), options: Property.ShortText({ displayName: 'Options', - description: 'Comma-separated options, used only when Type is Single Select.', + description: 'Only used when Type is Single Select.', required: false, + placeholder: 'Low, Medium, High', }), }, }), diff --git a/packages/pieces/core/tables/src/lib/actions/delete-record.ts b/packages/pieces/core/tables/src/lib/actions/delete-record.ts index 93e53d4618b7..6daaa9cea7c7 100644 --- a/packages/pieces/core/tables/src/lib/actions/delete-record.ts +++ b/packages/pieces/core/tables/src/lib/actions/delete-record.ts @@ -8,15 +8,15 @@ export const deleteRecord = createAction({ name: 'tables-delete-record', classification: 'DESTRUCTIVE', displayName: 'Delete Record(s)', - description: 'Delete record(s) from a table', + description: 'Delete record(s) from a table.', aiMetadata: { description: 'Deletes one or more rows from an Activepieces Table by record ID, accepting a list of IDs in a single call. Pick this to remove specific known rows; use Clear Table to empty a table wholesale, or Delete Table to drop the table along with its schema. Requires the table ID plus the record IDs, which come from Find Records or a table trigger payload, and cannot delete by filter or field value; idempotent.', idempotent: true }, auth: PieceAuth.None(), props: { table_id: tablesCommon.table_id, records_ids: Property.Array({ - displayName: 'Records IDs', + displayName: 'Record IDs', required: true, - description: 'The IDs of the records to delete' + description: 'Usually mapped from a Find Records step or a table trigger.' }), }, outputSchema: deleteRecordActionOutputSchema, diff --git a/packages/pieces/core/tables/src/lib/actions/download-table.ts b/packages/pieces/core/tables/src/lib/actions/download-table.ts index b2e2c3d04c31..10915f8d141a 100644 --- a/packages/pieces/core/tables/src/lib/actions/download-table.ts +++ b/packages/pieces/core/tables/src/lib/actions/download-table.ts @@ -23,9 +23,8 @@ export const downloadTable = createAction({ table_id: tablesCommon.table_id, include_headers: Property.Checkbox({ displayName: 'Include Headers', - description: - 'Whether to include column headers as the first row of the CSV.', - required: true, + description: 'Adds the column names as the first row of the CSV.', + required: false, defaultValue: true, }), }, diff --git a/packages/pieces/core/tables/src/lib/actions/find-records.ts b/packages/pieces/core/tables/src/lib/actions/find-records.ts index 9fa3a3ab5475..ba14cb9ac9ed 100644 --- a/packages/pieces/core/tables/src/lib/actions/find-records.ts +++ b/packages/pieces/core/tables/src/lib/actions/find-records.ts @@ -1,4 +1,4 @@ -import { createAction, PieceAuth, Property } from '@activepieces/pieces-framework'; +import { createAction, DynamicPropsValue, MarkdownVariant, PieceAuth, Property } from '@activepieces/pieces-framework'; import { tablesCommon } from '../common'; import { AuthenticationType, httpClient, HttpMethod, propsValidation } from '@activepieces/pieces-common'; import { FieldType, Filter, FilterOperator, ListRecordsRequest, PopulatedRecord, SeekPage } from '@activepieces/pieces-framework'; @@ -21,25 +21,19 @@ export const findRecords = createAction({ auth: PieceAuth.None(), props: { table_id: tablesCommon.table_id, - limit: Property.Number({ - displayName: 'Limit', - description: 'Maximum number of records to return (default no limit).', - required: false, - }), filters: Property.DynamicProperties({ auth: PieceAuth.None(), displayName: 'Filters', - description: 'Filter conditions to apply', + description: 'All conditions must match. Empty returns every record.', required: false, refreshers: ['table_id'], - props: async (propsValue, context) => { + props: async (propsValue, context): Promise => { const table_id = propsValue['table_id']; if (!table_id || typeof table_id !== 'string') { return { - filters: Property.Array({ - displayName: 'Filters', - required: false, - properties: {}, + markdown: Property.MarkDown({ + value: 'Select a table to load its fields.', + variant: MarkdownVariant.INFO, }), }; } @@ -53,6 +47,7 @@ export const findRecords = createAction({ return { filters: Property.Array({ displayName: 'Filters', + description: 'All conditions must match. Empty returns every record.', required: false, properties: { field: Property.StaticDropdown({ @@ -71,11 +66,11 @@ export const findRecords = createAction({ options: { options: [ { label: 'Equals', value: FilterOperator.EQ }, - { label: 'Not Equals', value: FilterOperator.NEQ }, - { label: 'Greater Than', value: FilterOperator.GT }, - { label: 'Greater Than or Equal', value: FilterOperator.GTE }, - { label: 'Less Than', value: FilterOperator.LT }, - { label: 'Less Than or Equal', value: FilterOperator.LTE }, + { label: 'Not equals', value: FilterOperator.NEQ }, + { label: 'Greater than', value: FilterOperator.GT }, + { label: 'Greater than or equal', value: FilterOperator.GTE }, + { label: 'Less than', value: FilterOperator.LT }, + { label: 'Less than or equal', value: FilterOperator.LTE }, { label: 'Contains', value: FilterOperator.CO }, { label: 'Exists', value: FilterOperator.EXISTS }, { label: 'Does not exist', value: FilterOperator.NOT_EXISTS }, @@ -84,6 +79,7 @@ export const findRecords = createAction({ }), value: Property.ShortText({ displayName: 'Value', + description: 'Ignored for Exists and Does not exist.', required: false, }), }, @@ -91,6 +87,12 @@ export const findRecords = createAction({ }; }, }), + limit: Property.Number({ + displayName: 'Limit', + description: 'Maximum records to return. Empty returns all records.', + required: false, + advanced: true, + }), }, outputSchema: findRecordsActionOutputSchema, async run(context) { diff --git a/packages/pieces/core/tables/src/lib/actions/get-record.ts b/packages/pieces/core/tables/src/lib/actions/get-record.ts index 337341854a55..8af7fbc98380 100644 --- a/packages/pieces/core/tables/src/lib/actions/get-record.ts +++ b/packages/pieces/core/tables/src/lib/actions/get-record.ts @@ -1,4 +1,4 @@ -import { createAction, PieceAuth, Property } from '@activepieces/pieces-framework'; +import { createAction, PieceAuth } from '@activepieces/pieces-framework'; import { tablesCommon } from '../common'; import { AuthenticationType, httpClient, HttpMethod } from '@activepieces/pieces-common'; import { PopulatedRecord } from '@activepieces/pieces-framework'; @@ -9,7 +9,7 @@ export const getRecord = createAction({ name: 'tables-get-record', classification: 'READ', displayName: 'Get Record', - description: 'Get single record by its id.', + description: 'Get a single record by its ID.', aiMetadata: { description: 'Fetches one Activepieces Table record by its record ID and returns its cell values. Pick this when the exact record ID is already in hand, for example from a table trigger payload or an earlier Find Records step; use Find Records to look rows up by column value instead. Requires both the table ID and the record ID, and cannot search by column content; read-only and idempotent.', idempotent: true }, auth: PieceAuth.None(), props: { diff --git a/packages/pieces/core/tables/src/lib/actions/update-record.ts b/packages/pieces/core/tables/src/lib/actions/update-record.ts index d847c253c6f1..3d35fa857d77 100644 --- a/packages/pieces/core/tables/src/lib/actions/update-record.ts +++ b/packages/pieces/core/tables/src/lib/actions/update-record.ts @@ -1,4 +1,4 @@ -import { createAction, PieceAuth, Property } from '@activepieces/pieces-framework'; +import { createAction, DynamicPropsValue, MarkdownVariant, PieceAuth, Property } from '@activepieces/pieces-framework'; import { tablesCommon } from '../common'; import { AuthenticationType, httpClient, HttpMethod, propsValidation } from '@activepieces/pieces-common'; import { PopulatedRecord, UpdateRecordRequest } from '@activepieces/pieces-framework'; @@ -9,7 +9,7 @@ export const updateRecord = createAction({ name: 'tables-update-record', classification: 'WRITE', displayName: 'Update Record', - description: 'Update values in an existing record', + description: 'Update values in an existing record.', aiMetadata: { description: 'Overwrites cell values on one existing Activepieces Table record addressed by its record ID, changing only the columns supplied - blank values are skipped and keep whatever the record already holds. Pick this to modify a known row; use Create Record(s) to insert new rows, since this action only edits a record that already exists. Requires both the table ID and the record ID, and each value is validated against the type of its column; idempotent, since re-applying the same values leaves the record unchanged.', idempotent: true }, auth: PieceAuth.None(), props: { @@ -18,15 +18,20 @@ export const updateRecord = createAction({ values: Property.DynamicProperties({ auth: PieceAuth.None(), displayName: 'Values', - description: 'The values to update. Leave empty to keep current value.', + description: 'Leave a field empty to keep its current value.', required: true, refreshers: ['table_id'], - props: async ({ table_id }, context) => { + props: async ({ table_id }, context): Promise => { const tableExternalId = table_id as unknown as string; - const tableId = await tablesCommon.convertTableExternalIdToId(tableExternalId, context); - if ((tableId ?? '').toString().length === 0) { - return {}; + if ((tableExternalId ?? '').toString().length === 0) { + return { + markdown: Property.MarkDown({ + value: 'Select a table to load its fields.', + variant: MarkdownVariant.INFO, + }), + }; } + const tableId = await tablesCommon.convertTableExternalIdToId(tableExternalId, context); return tablesCommon.createFieldProperties({ tableId, context }); }, diff --git a/packages/pieces/core/tables/src/lib/common/index.ts b/packages/pieces/core/tables/src/lib/common/index.ts index d6cce3acf727..0260560d2098 100644 --- a/packages/pieces/core/tables/src/lib/common/index.ts +++ b/packages/pieces/core/tables/src/lib/common/index.ts @@ -32,10 +32,9 @@ const getFieldTypeText = (fieldType: FieldType) => { export const tablesCommon = { table_id: Property.Dropdown({ auth: PieceAuth.None(), - displayName: 'Table Name', + displayName: 'Table', required: true, refreshers: [], - refreshOnSearch: true, options: async (_propsValue, context) => { try { const tables = await fetchAllTables(context); @@ -62,7 +61,7 @@ export const tablesCommon = { record_id: Property.ShortText({ displayName: 'Record ID', - description: 'The ID of the record to do the action on.', + description: 'Usually comes from a Find Records step or a table trigger.', required: true, }), @@ -115,7 +114,7 @@ export const tablesCommon = { const tableFields = await this.getTableFields({ tableId, context }); if (!Array.isArray(tableFields) || tableFields.length === 0) { fields['markdown'] = Property.MarkDown({ - value: `We couldn't find any fields in the selected table. Please add fields to the table first.`, + value: `This table has no fields yet. Add fields to the table first.`, variant: MarkdownVariant.INFO, }); return fields; @@ -166,7 +165,7 @@ export const tablesCommon = { } catch (e) { console.error('Error fetching fields:', e); fields['markdown'] = Property.MarkDown({ - value: `We couldn't find any fields in the selected table. Please add fields to the table first.`, + value: `Couldn't load the fields of this table. Try again.`, variant: MarkdownVariant.INFO, }); From 4b91d941567e73db82e51aef097ce04678112b18 Mon Sep 17 00:00:00 2001 From: Othman Abu Ajamieh <52608229+othmanemad@users.noreply.github.com> Date: Wed, 23 Sep 2026 00:05:07 +0300 Subject: [PATCH 3/3] =?UTF-8?q?feat(waitpoints):=20the=20barrier's=20exter?= =?UTF-8?q?nal=20edges=20=E2=80=94=20deadline,=20create=20endpoint,=20conf?= =?UTF-8?q?irm=20links=20(#14778)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...in-is-an-event-driven-waitpoint-barrier.md | 11 + .../engineering/ci-pr-review-hygiene.md | 1 + brain/knowledge/flows-execution/flow-runs.md | 16 +- bun.lock | 6 +- docs/install/architecture/waitpoints.mdx | 1 + packages/core/execution/package.json | 2 +- .../src/lib/engine/execution-errors.ts | 6 + .../src/lib/flow-run/waitpoint/index.ts | 3 + packages/server/api/src/app/app.ts | 16 + ...857000000000-AddWaitpointDeadLetteredAt.ts | 62 ++ .../src/app/database/postgres-connection.ts | 2 + .../api/src/app/helper/system-jobs/common.ts | 17 +- .../src/app/helper/system-jobs/system-job.ts | 55 +- .../api/src/app/waitpoints/barrier-service.ts | 113 ++- .../src/app/waitpoints/resume-controller.ts | 281 ++++++-- .../app/waitpoints/resume-delay-handler.ts | 16 + .../app/waitpoints/waitpoint-controller.ts | 71 +- .../waitpoints/waitpoint-deadline-sweep.ts | 384 ++++++++++ .../src/app/waitpoints/waitpoint-entity.ts | 15 +- .../src/app/waitpoints/waitpoint-service.ts | 74 +- .../app/waitpoints/waitpoint-signal-entity.ts | 1 + .../app/waitpoints/waitpoint-timeout-job.ts | 44 ++ .../api/src/app/waitpoints/waitpoint-types.ts | 1 + .../workers/migrations/refill-paused-jobs.ts | 20 +- .../ce/flows/flow-run/barrier.test.ts | 679 +++++++++++++++++- .../ce/flows/flow-run/waitpoint.test.ts | 91 ++- .../ce/workers/refill-paused-jobs.test.ts | 7 +- .../src/lib/piece-context/rejected-request.ts | 37 + .../src/lib/piece-context/waitpoint-client.ts | 35 +- .../piece-context/rejected-request.test.ts | 52 ++ 30 files changed, 1935 insertions(+), 184 deletions(-) create mode 100644 packages/server/api/src/app/database/migration/postgres/1857000000000-AddWaitpointDeadLetteredAt.ts create mode 100644 packages/server/api/src/app/waitpoints/waitpoint-deadline-sweep.ts create mode 100644 packages/server/api/src/app/waitpoints/waitpoint-timeout-job.ts create mode 100644 packages/server/engine/src/lib/piece-context/rejected-request.ts create mode 100644 packages/server/engine/test/piece-context/rejected-request.test.ts diff --git a/brain/knowledge/decisions/000015-fan-in-is-an-event-driven-waitpoint-barrier.md b/brain/knowledge/decisions/000015-fan-in-is-an-event-driven-waitpoint-barrier.md index 94095a8c881a..3018b1404f94 100644 --- a/brain/knowledge/decisions/000015-fan-in-is-an-event-driven-waitpoint-barrier.md +++ b/brain/knowledge/decisions/000015-fan-in-is-an-event-driven-waitpoint-barrier.md @@ -45,10 +45,21 @@ all, purely because child rows appear later than the dispatch that created them. - **Receiving is an upsert, last write wins.** It is not a state machine and does not reject a second write: a retried child reports its terminal status again — possibly a different one — and the barrier must reflect the latest truth, not the first. That also makes redelivery of the receive path free. +- **A human decision is the one exception: it is compare-and-set.** `receiveSignal` keeps the upsert above for + machine producers; the confirm link goes through `recordDecision`, whose `UPDATE` carries + `AND status = PENDING` and answers "already responded" when it matches nothing. A double-click, an email + client prefetching on hover, or two people on a shared inbox must not silently flip a recorded approve into + a reject. Both paths write with an `UPDATE … RETURNING "waitpointId"`, never read-then-`save()`, which + *re-inserted* a signal row the release had already deleted. `recordDecision` also takes `FOR SHARE` on the + barrier first, so `closeBarrier`'s `FOR UPDATE` waits for it: a decision is either counted in the summary or + refused, never dropped between the count and the signal delete. `receiveSignal` takes no lock; a machine + signal that commits during a close can miss the summary, which "last write wins" already accepts. - **The deadline is set at create and nothing moves it.** The floor rule needs an evaluation to fire; a barrier nobody ever signals gets none, so the deadline is the only thing between "the producer died hard" and a run paused until retention deletes it. Seal does not touch it — the clamp with no requested value already returns the ceiling, and a shorter deadline is a `policy` input, so both are known at create. + `defaultBarrierDeadline({ flowRunCreated })` anchors it to the run's start, not to the moment the barrier + opened, so a barrier inside a long-running run cannot outlive `AP_PAUSED_FLOW_TIMEOUT_DAYS`. - **Evaluation must be coalesced, and coalescing must not swallow its own last signal.** At 10 000 signals, evaluating per signal is ~100M row reads. BullMQ holds a deduplication key while the job is queued *and* active, so the handler's **first statement** is `clearEvaluationDedupKey`, and every producer commits diff --git a/brain/knowledge/engineering/ci-pr-review-hygiene.md b/brain/knowledge/engineering/ci-pr-review-hygiene.md index f8f33538c536..00a16b5ebcef 100644 --- a/brain/knowledge/engineering/ci-pr-review-hygiene.md +++ b/brain/knowledge/engineering/ci-pr-review-hygiene.md @@ -72,5 +72,6 @@ Enforcement is the **`Codeowners review` repository ruleset** (active on the def - **Fixing an infra failure can turn a branch from red to a different red, and that is progress, not a regression.** A runner killed mid-run (`shutdown signal` above `exited (137)`) reports no test results at all, so genuine failures underneath it are invisible. When the OOM is fixed the suites finally run to completion and those failures surface for the first time — on this stack, thirteen branches went from an OOM red to a real `barrier.test.ts` red the moment the runner survived. Check whether the *previous* run reached a test summary at all before blaming the change that unmasked it. - **A `beforeAll` that overrides the vitest timeout *downwards* is a slow-motion flake, and it fails in a way that blames your diff.** [`execute-flow-e2e.test.ts`](../../../packages/server/api/test/integration/ce/flows/flow-run/execute-flow-e2e.test.ts) passes `30_000` to `beforeAll` even though `packages/server/api/vitest.config.ts` sets `hookTimeout: 60000`, and spends 5s of that budget on a hardcoded `setTimeout`. Inside the remaining ~25s it boots a fresh app, runs the **whole migration chain**, starts queue consumers and a worker. That chain has grown 293 → 376 postgres migrations since the hook was written (`d2c42816013`, "feat: worker v2", #11608, 2026-03-15 — authored *after* the 60s default was already in place), so on a loaded runner it now times out. When it does, all 9 of its tests report **skipped**, not failed: the summary reads `1 failed | 84 passed` with no failing assertion anywhere, and `--bail 1` kills the rest of the CE task. Seen on [#15078](https://github.com/activepieces/activepieces/pull/15078), an MCP-OAuth-only PR that cannot reach that code. No single commit broke it, so there is nobody to blame and nothing to revert — the fix is to delete the override and poll for worker readiness instead of sleeping blind. When triaging a red CE task, check whether the failure is an assertion or a hook timeout **before** blaming the diff. - **Git hooks cannot run from a `git worktree`: `.husky/_/husky.sh` is an install artifact that only exists in the tree where `bun install` ran.** Every commit and push from a worktree dies on `.: cannot open .husky/_/husky.sh`, before the hook's own logic is reached. Don't reach for `--no-verify` — push the worktree's commit from the main checkout (`git push origin :`; worktrees share the object store), so `pre-push` actually bootstraps and its "no direct pushes to `main`" guard runs. `SKIP_CHECK=1` is the repo's own sanctioned way to skip the lint/test prompt. Note `pre-push` special-cases `CLAUDE_PUSH=yes` into running the **full** lint + unit + API suite, so an agent that silently bypasses the hook is skipping a check the repo added deliberately for it — say so out loud when you do. +- **Merging up a stacked chain, a parent's rename reaches a child's call sites as a clean auto-merge — only the typecheck finds it.** Git conflicts on lines both sides edited, so a symbol renamed on the parent merges silently into a child that merely *calls* the old name: `feat/barrier-service` renamed `barrierService.receive` → `receiveSignal`, and `feat/barrier-edges`' `resume-controller.ts` kept calling `.receive(...)` with no conflict marker. Worse, both sides can each be right and still merge wrong — the same commit renamed `findPreCompletedByFlowRunId` while the child had rewritten that function's *body* to fix a real bug, so the conflict exposed only the signature and taking either side whole silently dropped work. Two habits: typecheck **every** branch in the chain after its merge rather than trusting a conflict-free result, and before picking a side run `git log --oneline $(git merge-base HEAD MERGE_HEAD)..HEAD -- ` on both to read what each side was actually doing. Stacks here run deep (the waitpoints → process-in-batches chain was 17 PRs), so one missed rename propagates through every branch above it. - **Docs videos belong on `cdn.activepieces.com/videos/docs/`, never in the repo — and `docs/resources/` is the legacy path that suggests otherwise.** Every `