From d82e8998ddbfe73a85ff40a054e5db3647ac8b56 Mon Sep 17 00:00:00 2001 From: Kishan Parmar <135701940+kishanprmr@users.noreply.github.com> Date: Fri, 4 Sep 2026 15:37:45 +0530 Subject: [PATCH 1/6] feat(pieces): add outputSchema for monday.com, fillout-forms, and line (#15263) --- .../community/fillout-forms/package.json | 2 +- .../src/lib/actions/find-form-by-title.ts | 2 + .../src/lib/actions/get-form-responses.ts | 2 + .../src/lib/actions/get-single-response.ts | 2 + .../fillout-forms/src/lib/output-schemas.ts | 75 +++++++++++++ .../src/lib/triggers/new-form-response.ts | 2 + packages/pieces/community/line/package.json | 2 +- .../community/line/src/lib/output-schemas.ts | 35 ++++++ .../line/src/lib/trigger/new-message.ts | 2 + packages/pieces/community/monday/package.json | 2 +- .../monday/src/lib/actions/create-column.ts | 2 + .../monday/src/lib/actions/create-group.ts | 2 + .../monday/src/lib/actions/create-item.ts | 2 + .../monday/src/lib/actions/create-update.ts | 2 + .../actions/update-column-values-of-item.ts | 2 + .../src/lib/actions/update-item-name.ts | 2 + .../src/lib/actions/upload-file-to-column.ts | 2 + .../monday/src/lib/output-schemas.ts | 105 ++++++++++++++++++ .../src/lib/triggers/new-item-in-board.ts | 2 + .../lib/triggers/specific-column-updated.ts | 2 + 20 files changed, 246 insertions(+), 3 deletions(-) create mode 100644 packages/pieces/community/fillout-forms/src/lib/output-schemas.ts create mode 100644 packages/pieces/community/line/src/lib/output-schemas.ts create mode 100644 packages/pieces/community/monday/src/lib/output-schemas.ts diff --git a/packages/pieces/community/fillout-forms/package.json b/packages/pieces/community/fillout-forms/package.json index 7a7ca7188814..b1a24b036710 100644 --- a/packages/pieces/community/fillout-forms/package.json +++ b/packages/pieces/community/fillout-forms/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-fillout-forms", - "version": "0.1.9", + "version": "0.1.10", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { diff --git a/packages/pieces/community/fillout-forms/src/lib/actions/find-form-by-title.ts b/packages/pieces/community/fillout-forms/src/lib/actions/find-form-by-title.ts index 170251587730..1ecb476dce57 100644 --- a/packages/pieces/community/fillout-forms/src/lib/actions/find-form-by-title.ts +++ b/packages/pieces/community/fillout-forms/src/lib/actions/find-form-by-title.ts @@ -2,6 +2,7 @@ import { createAction, Property } from '@activepieces/pieces-framework'; import { HttpMethod } from '@activepieces/pieces-common'; import { makeRequest } from '../common'; import { filloutFormsAuth } from '../auth'; +import { findFormByTitleActionOutputSchema } from '../output-schemas'; export const findFormByTitle = createAction({ auth: filloutFormsAuth, @@ -11,6 +12,7 @@ export const findFormByTitle = createAction({ description: 'Finds an existing forms by title.', audience: 'both', aiMetadata: { description: 'Searches the account\'s Fillout forms for those whose title contains the given text (case-insensitive partial match) and returns the matches. Use to resolve a form name to its form ID before calling response-fetching actions. Read-only and idempotent.', idempotent: true }, + outputSchema: findFormByTitleActionOutputSchema, props: { title: Property.ShortText({ displayName: 'Form Title', diff --git a/packages/pieces/community/fillout-forms/src/lib/actions/get-form-responses.ts b/packages/pieces/community/fillout-forms/src/lib/actions/get-form-responses.ts index 372a3dd505f2..d9d37761c252 100644 --- a/packages/pieces/community/fillout-forms/src/lib/actions/get-form-responses.ts +++ b/packages/pieces/community/fillout-forms/src/lib/actions/get-form-responses.ts @@ -3,6 +3,7 @@ import { HttpMethod } from '@activepieces/pieces-common'; import { makeRequest } from '../common'; import { formIdDropdown } from '../common/props'; import { filloutFormsAuth } from '../auth'; +import { getFormResponsesActionOutputSchema } from '../output-schemas'; export const getFormResponses = createAction({ auth: filloutFormsAuth, @@ -12,6 +13,7 @@ export const getFormResponses = createAction({ description: 'Fetch all responses for a Fillout form, with optional filters.', audience: 'both', aiMetadata: { description: 'Lists submissions for a specific Fillout form (identified by form ID), optionally narrowed by date range, search text, completion status (finished by default, or in-progress), sort order, limit, and offset for pagination. With no filters it returns all submissions; supply filters to fetch only matching ones. Use to retrieve or page through collected form responses. Read-only and idempotent.', idempotent: true }, + outputSchema: getFormResponsesActionOutputSchema, props: { formId: formIdDropdown, limit: Property.Number({ diff --git a/packages/pieces/community/fillout-forms/src/lib/actions/get-single-response.ts b/packages/pieces/community/fillout-forms/src/lib/actions/get-single-response.ts index eef070347469..eb2dd3f1046f 100644 --- a/packages/pieces/community/fillout-forms/src/lib/actions/get-single-response.ts +++ b/packages/pieces/community/fillout-forms/src/lib/actions/get-single-response.ts @@ -3,6 +3,7 @@ import { HttpMethod } from '@activepieces/pieces-common'; import { makeRequest } from '../common'; import { formIdDropdown, submissionIdDropdown } from '../common/props'; import { filloutFormsAuth } from '../auth'; +import { getSingleResponseActionOutputSchema } from '../output-schemas'; export const getSingleResponse = createAction({ auth: filloutFormsAuth, @@ -12,6 +13,7 @@ export const getSingleResponse = createAction({ description: 'Retrieves a specific submission from a form.', audience: 'both', aiMetadata: { description: 'Retrieves one specific submission from a Fillout form, keyed by form ID and submission ID. Use when you already know the submission ID and need its full details, rather than listing all responses. Read-only and idempotent.', idempotent: true }, + outputSchema: getSingleResponseActionOutputSchema, props: { formId: formIdDropdown, submissionId: submissionIdDropdown, diff --git a/packages/pieces/community/fillout-forms/src/lib/output-schemas.ts b/packages/pieces/community/fillout-forms/src/lib/output-schemas.ts new file mode 100644 index 000000000000..3225a1ce83fb --- /dev/null +++ b/packages/pieces/community/fillout-forms/src/lib/output-schemas.ts @@ -0,0 +1,75 @@ +import { OutputSchema } from '@activepieces/pieces-framework'; + +const formFields: OutputSchema['fields'] = [ + { key: 'name', label: 'Name' }, + { key: 'formId', label: 'Form ID' }, + { key: 'id', label: 'Numeric ID' }, + { key: 'isPublished', label: 'Is Published', format: 'boolean' }, + { key: 'tags', label: 'Tags' }, +]; + +export const findFormByTitleActionOutputSchema: OutputSchema = { + fields: [ + { key: 'found', label: 'Found', format: 'boolean' }, + { key: 'result', label: 'Matching Forms', labelKey: 'name', listItems: formFields }, + ], +}; + +const submissionCoreFields: OutputSchema['fields'] = [ + { key: 'submissionId', label: 'Submission ID' }, + { key: 'submissionTime', label: 'Submission Time', format: 'datetime' }, + { + key: 'questions', label: 'Questions', labelKey: 'name', + listItems: [ + { key: 'id', label: 'Question ID' }, + { key: 'name', label: 'Question Name' }, + { key: 'type', label: 'Question Type' }, + { key: 'value', label: 'Answer' }, + ], + }, + { + key: 'calculations', label: 'Calculations', labelKey: 'name', + listItems: [ + { key: 'id', label: 'Calculation ID' }, + { key: 'name', label: 'Calculation Name' }, + { key: 'type', label: 'Calculation Type' }, + { key: 'value', label: 'Value' }, + ], + }, + { + key: 'urlParameters', label: 'URL Parameters', labelKey: 'name', + listItems: [ + { key: 'id', label: 'Parameter ID' }, + { key: 'name', label: 'Parameter Name' }, + { key: 'value', label: 'Value' }, + ], + }, +]; + +const submissionFields: OutputSchema['fields'] = [ + ...submissionCoreFields, + { key: 'lastUpdatedAt', label: 'Last Updated At', format: 'datetime' }, + { key: 'startedAt', label: 'Started At', format: 'datetime' }, + { key: 'editLink', label: 'Edit Link', format: 'url' }, +]; + +export const getFormResponsesActionOutputSchema: OutputSchema = { + fields: [ + { key: 'totalResponses', label: 'Total Responses', format: 'number' }, + { key: 'pageCount', label: 'Page Count', format: 'number' }, + { key: 'responses', label: 'Responses', labelKey: 'submissionId', listItems: submissionFields }, + ], +}; + +export const getSingleResponseActionOutputSchema: OutputSchema = { + fields: [ + { key: 'submission', label: 'Submission', children: submissionFields }, + ], +}; + +export const newFormResponseTriggerOutputSchema: OutputSchema = { + itemLabel: 'Submission {submissionId}', + fields: [ + { key: 'submissions', label: 'Submissions', value: '', listItems: submissionCoreFields }, + ], +}; diff --git a/packages/pieces/community/fillout-forms/src/lib/triggers/new-form-response.ts b/packages/pieces/community/fillout-forms/src/lib/triggers/new-form-response.ts index 8baf2716f8a4..ec6aac3cf18f 100644 --- a/packages/pieces/community/fillout-forms/src/lib/triggers/new-form-response.ts +++ b/packages/pieces/community/fillout-forms/src/lib/triggers/new-form-response.ts @@ -4,6 +4,7 @@ import { filloutFormsAuth } from '../auth'; import { makeRequest } from '../common'; import { HttpMethod } from '@activepieces/pieces-common'; import { isNil } from '@activepieces/pieces-framework'; +import { newFormResponseTriggerOutputSchema } from '../output-schemas'; const TRIGGER_KEY = 'new-form-response-trigger'; @@ -17,6 +18,7 @@ export const newFormResponse = createTrigger({ aiMetadata: { description: 'Fires when a new submission is received for the selected Fillout form, delivering the submitted answers, calculations, and metadata. Use to start a workflow whenever someone completes the form.', }, + outputSchema: newFormResponseTriggerOutputSchema, props: { formId: formIdDropdown, }, diff --git a/packages/pieces/community/line/package.json b/packages/pieces/community/line/package.json index ebd1c3db817d..7f090c930024 100644 --- a/packages/pieces/community/line/package.json +++ b/packages/pieces/community/line/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-line", - "version": "0.1.8", + "version": "0.1.9", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { diff --git a/packages/pieces/community/line/src/lib/output-schemas.ts b/packages/pieces/community/line/src/lib/output-schemas.ts new file mode 100644 index 000000000000..b24fd39e71f7 --- /dev/null +++ b/packages/pieces/community/line/src/lib/output-schemas.ts @@ -0,0 +1,35 @@ +import { OutputSchema } from '@activepieces/pieces-framework'; + +export const newMessageTriggerOutputSchema: OutputSchema = { + itemLabel: 'Message from {source.userId}', + fields: [ + { + key: 'events', + label: 'Events', + value: '', + listItems: [ + { key: 'type', label: 'Event Type' }, + { key: 'mode', label: 'Mode' }, + { key: 'timestamp', label: 'Timestamp', format: 'datetime' }, + { key: 'replyToken', label: 'Reply Token' }, + { key: 'webhookEventId', label: 'Webhook Event ID' }, + { + key: 'source', label: 'Source', + children: [ + { key: 'type', label: 'Source Type' }, + { key: 'userId', label: 'User ID' }, + { key: 'groupId', label: 'Group ID' }, + { key: 'roomId', label: 'Room ID' }, + ], + }, + { + key: 'deliveryContext', label: 'Delivery Context', + children: [ + { key: 'isRedelivery', label: 'Is Redelivery', format: 'boolean' }, + ], + }, + { key: 'message', label: 'Message' }, + ], + }, + ], +}; diff --git a/packages/pieces/community/line/src/lib/trigger/new-message.ts b/packages/pieces/community/line/src/lib/trigger/new-message.ts index 00bbd44c7025..ed3d87fd2715 100644 --- a/packages/pieces/community/line/src/lib/trigger/new-message.ts +++ b/packages/pieces/community/line/src/lib/trigger/new-message.ts @@ -4,6 +4,7 @@ import { Property, TriggerStrategy, } from '@activepieces/pieces-framework'; +import { newMessageTriggerOutputSchema } from '../output-schemas'; const markdown = ` - Create Line bot account from Developer Console @@ -22,6 +23,7 @@ export const newMessage = createTrigger({ aiMetadata: { description: 'Fires when the LINE bot receives an inbound message event from a user via the Messaging API webhook. Each emitted item represents one active message event, carrying the sender details and message content needed to react or reply.', }, + outputSchema: newMessageTriggerOutputSchema, props: { md: Property.MarkDown({ value: markdown, diff --git a/packages/pieces/community/monday/package.json b/packages/pieces/community/monday/package.json index b2b406377931..0f54f1c2da37 100644 --- a/packages/pieces/community/monday/package.json +++ b/packages/pieces/community/monday/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-monday", - "version": "0.3.7", + "version": "0.3.8", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "dependencies": { diff --git a/packages/pieces/community/monday/src/lib/actions/create-column.ts b/packages/pieces/community/monday/src/lib/actions/create-column.ts index 51c928d17dc3..1af2f98ff211 100644 --- a/packages/pieces/community/monday/src/lib/actions/create-column.ts +++ b/packages/pieces/community/monday/src/lib/actions/create-column.ts @@ -2,6 +2,7 @@ import { Property, createAction } from '@activepieces/pieces-framework'; import { mondayAuth } from '../auth'; import { makeClient, mondayCommon } from '../common'; import { COLUMN_TYPE_OPTIONS } from '../common/constants'; +import { createColumnActionOutputSchema } from '../output-schemas'; export const createColumnAction = createAction({ auth: mondayAuth, @@ -11,6 +12,7 @@ export const createColumnAction = createAction({ description: 'Creates a new column in board.', audience: 'both', aiMetadata: { description: 'Adds a new column of a chosen type (text, status, date, number, etc.) to a monday.com board. Use to extend a board\'s structure before writing data into it. Not idempotent: each call creates a separate column even with the same title.', idempotent: false }, + outputSchema: createColumnActionOutputSchema, props: { workspace_id: mondayCommon.workspace_id(true), board_id: mondayCommon.board_id(true), diff --git a/packages/pieces/community/monday/src/lib/actions/create-group.ts b/packages/pieces/community/monday/src/lib/actions/create-group.ts index 7af8826ea090..69ccf1800b81 100644 --- a/packages/pieces/community/monday/src/lib/actions/create-group.ts +++ b/packages/pieces/community/monday/src/lib/actions/create-group.ts @@ -1,6 +1,7 @@ import { Property, createAction } from '@activepieces/pieces-framework'; import { mondayAuth } from '../auth'; import { makeClient, mondayCommon } from '../common'; +import { createGroupActionOutputSchema } from '../output-schemas'; export const createGroupAction = createAction({ auth: mondayAuth, @@ -10,6 +11,7 @@ export const createGroupAction = createAction({ description: 'Creates a new group in board.', audience: 'both', aiMetadata: { description: 'Creates a new group (section that holds items) on a monday.com board. Use to organize items under a named section before adding them. Not idempotent: each call creates a separate group even with the same name.', idempotent: false }, + outputSchema: createGroupActionOutputSchema, props: { workspace_id: mondayCommon.workspace_id(true), board_id: mondayCommon.board_id(true), diff --git a/packages/pieces/community/monday/src/lib/actions/create-item.ts b/packages/pieces/community/monday/src/lib/actions/create-item.ts index 481415af8593..43261e87dc53 100644 --- a/packages/pieces/community/monday/src/lib/actions/create-item.ts +++ b/packages/pieces/community/monday/src/lib/actions/create-item.ts @@ -9,6 +9,7 @@ import { convertPropValueToMondayColumnValue, generateColumnIdTypeMap, } from '../common/helper'; +import { createItemActionOutputSchema } from '../output-schemas'; export const createItemAction = createAction({ auth: mondayAuth, @@ -18,6 +19,7 @@ export const createItemAction = createAction({ description: 'Creates a new item inside a board.', audience: 'both', aiMetadata: { description: 'Creates a new item (row) on a monday.com board, optionally placed in a group and pre-populated with column values that are auto-coerced to each column\'s type. Use to add a record to a board. Requires the board id and an item name; enable create-labels-if-missing only when allowed to modify board structure. Not idempotent: each call creates a separate item.', idempotent: false }, + outputSchema: createItemActionOutputSchema, props: { workspace_id: mondayCommon.workspace_id(true), board_id: mondayCommon.board_id(true), diff --git a/packages/pieces/community/monday/src/lib/actions/create-update.ts b/packages/pieces/community/monday/src/lib/actions/create-update.ts index d43eb75629a0..7837d67e0342 100644 --- a/packages/pieces/community/monday/src/lib/actions/create-update.ts +++ b/packages/pieces/community/monday/src/lib/actions/create-update.ts @@ -1,6 +1,7 @@ import { Property, createAction } from '@activepieces/pieces-framework'; import { mondayAuth } from '../auth'; import { makeClient } from '../common'; +import { createUpdateActionOutputSchema } from '../output-schemas'; export const createUpdateAction = createAction({ auth: mondayAuth, @@ -10,6 +11,7 @@ export const createUpdateAction = createAction({ description: 'Creates a new update.', audience: 'both', aiMetadata: { description: 'Posts an update (a comment/note in the item\'s update feed) to a monday.com item identified by item id. Use to add a message or log to an item. Not idempotent: each call appends a new update.', idempotent: false }, + outputSchema: createUpdateActionOutputSchema, props: { item_id: Property.ShortText({ displayName: 'Item ID', diff --git a/packages/pieces/community/monday/src/lib/actions/update-column-values-of-item.ts b/packages/pieces/community/monday/src/lib/actions/update-column-values-of-item.ts index 78413f0adf08..d51f73843c36 100644 --- a/packages/pieces/community/monday/src/lib/actions/update-column-values-of-item.ts +++ b/packages/pieces/community/monday/src/lib/actions/update-column-values-of-item.ts @@ -8,6 +8,7 @@ import { convertPropValueToMondayColumnValue, generateColumnIdTypeMap, } from '../common/helper'; +import { updateColumnValuesOfItemActionOutputSchema } from '../output-schemas'; export const updateColumnValuesOfItemAction = createAction({ auth: mondayAuth, @@ -17,6 +18,7 @@ export const updateColumnValuesOfItemAction = createAction({ description: 'Updates multiple columns values of specific item.', audience: 'both', aiMetadata: { description: 'Updates one or more column values on an existing monday.com item identified by board and item id; provided values are auto-coerced to each column\'s type. Use to edit an item\'s fields. Idempotent: re-applying the same values leaves the item in the same state.', idempotent: true }, + outputSchema: updateColumnValuesOfItemActionOutputSchema, props: { workspace_id: mondayCommon.workspace_id(true), board_id: mondayCommon.board_id(true), diff --git a/packages/pieces/community/monday/src/lib/actions/update-item-name.ts b/packages/pieces/community/monday/src/lib/actions/update-item-name.ts index 229364d51081..19668a41fe1e 100644 --- a/packages/pieces/community/monday/src/lib/actions/update-item-name.ts +++ b/packages/pieces/community/monday/src/lib/actions/update-item-name.ts @@ -1,6 +1,7 @@ import { Property, createAction } from '@activepieces/pieces-framework'; import { mondayAuth } from '../auth'; import { makeClient, mondayCommon } from '../common'; +import { updateItemNameActionOutputSchema } from '../output-schemas'; export const updateItemNameAction = createAction({ auth: mondayAuth, @@ -10,6 +11,7 @@ export const updateItemNameAction = createAction({ description: 'Updates an item name.', audience: 'both', aiMetadata: { description: 'Renames an existing monday.com item identified by board and item id. Use to change an item\'s title. Idempotent: re-applying the same name leaves the item unchanged.', idempotent: true }, + outputSchema: updateItemNameActionOutputSchema, props: { workspace_id: mondayCommon.workspace_id(true), board_id: mondayCommon.board_id(true), diff --git a/packages/pieces/community/monday/src/lib/actions/upload-file-to-column.ts b/packages/pieces/community/monday/src/lib/actions/upload-file-to-column.ts index 5410b57a629f..2f5402daf1e2 100644 --- a/packages/pieces/community/monday/src/lib/actions/upload-file-to-column.ts +++ b/packages/pieces/community/monday/src/lib/actions/upload-file-to-column.ts @@ -4,6 +4,7 @@ import { MondayColumnType } from '../common/constants'; import { httpClient, HttpMethod } from '@activepieces/pieces-common'; import FormData from 'form-data'; import { mondayAuth } from '../auth'; +import { uploadFileToColumnActionOutputSchema } from '../output-schemas'; export const uploadFileToColumnAction = createAction({ auth: mondayAuth, @@ -13,6 +14,7 @@ export const uploadFileToColumnAction = createAction({ description: 'Upload a file to a column in Monday.', audience: 'both', aiMetadata: { description: 'Uploads a file (from URL or base64) and attaches it to a file-type column on a monday.com item, identified by board, item, and file column id. Use to add an attachment to an item. Not idempotent: each call uploads and attaches another copy.', idempotent: false }, + outputSchema: uploadFileToColumnActionOutputSchema, props: { workspace_id: mondayCommon.workspace_id(true), board_id: mondayCommon.board_id(true), diff --git a/packages/pieces/community/monday/src/lib/output-schemas.ts b/packages/pieces/community/monday/src/lib/output-schemas.ts new file mode 100644 index 000000000000..ead24ef8c359 --- /dev/null +++ b/packages/pieces/community/monday/src/lib/output-schemas.ts @@ -0,0 +1,105 @@ +import { OutputSchema } from '@activepieces/pieces-framework'; + +const changeMultipleColumnValuesFields: OutputSchema['fields'] = [ + { key: 'id', label: 'Item ID', value: 'data.change_multiple_column_values.id' }, + { key: 'name', label: 'Item Name', value: 'data.change_multiple_column_values.name' }, +]; + +export const updateItemNameActionOutputSchema: OutputSchema = { + fields: changeMultipleColumnValuesFields, +}; + +export const updateColumnValuesOfItemActionOutputSchema: OutputSchema = { + fields: changeMultipleColumnValuesFields, +}; + +export const createItemActionOutputSchema: OutputSchema = { + fields: [{ key: 'id', label: 'Item ID', value: 'data.create_item.id' }], +}; + +export const createColumnActionOutputSchema: OutputSchema = { + fields: [{ key: 'id', label: 'Column ID', value: 'data.create_column.id' }], +}; + +export const createGroupActionOutputSchema: OutputSchema = { + fields: [{ key: 'id', label: 'Group ID', value: 'data.create_group.id' }], +}; + +export const createUpdateActionOutputSchema: OutputSchema = { + fields: [{ key: 'id', label: 'Update ID', value: 'data.create_update.id' }], +}; + +export const uploadFileToColumnActionOutputSchema: OutputSchema = { + fields: [ + { key: 'id', label: 'File ID', value: 'data.add_file_to_column.id' }, + { key: 'url', label: 'File URL', value: 'data.add_file_to_column.url', format: 'url' }, + { key: 'name', label: 'File Name', value: 'data.add_file_to_column.name' }, + { key: 'file_size', label: 'File Size', value: 'data.add_file_to_column.file_size', format: 'filesize' }, + { key: 'file_extension', label: 'File Extension', value: 'data.add_file_to_column.file_extension' }, + { key: 'created_at', label: 'Created At', value: 'data.add_file_to_column.created_at', format: 'datetime' }, + ], +}; + +const specificColumnUpdatedEventFields: OutputSchema['fields'] = [ + { key: 'app', label: 'App' }, + { key: 'type', label: 'Event Type' }, + { key: 'triggerTime', label: 'Trigger Time', format: 'datetime' }, + { key: 'subscriptionId', label: 'Subscription ID' }, + { key: 'userId', label: 'User ID' }, + { key: 'boardId', label: 'Board ID' }, + { key: 'groupId', label: 'Group ID' }, + { key: 'isTopGroup', label: 'Is Top Group', format: 'boolean' }, + { key: 'pulseId', label: 'Item ID' }, + { key: 'pulseName', label: 'Item Name' }, + { key: 'columnId', label: 'Column ID' }, + { key: 'columnType', label: 'Column Type' }, + { key: 'columnTitle', label: 'Column Title' }, + { key: 'value', label: 'New Value', dynamicKey: true }, + { key: 'previousValue', label: 'Previous Value', dynamicKey: true }, + { key: 'triggerUuid', label: 'Trigger UUID' }, +]; + +export const specificColumnUpdatedTriggerOutputSchema: OutputSchema = { + itemLabel: '{event.pulseName}', + fields: [ + { + key: 'items', + label: 'Events', + value: '', + listItems: [ + { key: 'event', label: 'Event', children: specificColumnUpdatedEventFields }, + ], + }, + ], +}; + +const newItemInBoardEventFields: OutputSchema['fields'] = [ + { key: 'userId', label: 'User ID' }, + { key: 'boardId', label: 'Board ID' }, + { key: 'pulseId', label: 'Item ID' }, + { key: 'pulseName', label: 'Item Name' }, + { key: 'groupId', label: 'Group ID' }, + { key: 'groupName', label: 'Group Name' }, + { key: 'groupColor', label: 'Group Color' }, + { key: 'isTopGroup', label: 'Is Top Group', format: 'boolean' }, + { key: 'app', label: 'App' }, + { key: 'type', label: 'Event Type' }, + { key: 'triggerTime', label: 'Trigger Time', format: 'datetime' }, + { key: 'subscriptionId', label: 'Subscription ID' }, + { key: 'triggerUuid', label: 'Trigger UUID' }, +]; + +export const newItemInBoardTriggerOutputSchema: OutputSchema = { + itemLabel: '{event.pulseName}', + fields: [ + { + key: 'items', + label: 'Items', + value: '', + listItems: [ + { key: 'event', label: 'Event', children: newItemInBoardEventFields }, + { key: 'columnValues', label: 'Column Values', dynamicKey: true }, + ], + }, + ], +}; diff --git a/packages/pieces/community/monday/src/lib/triggers/new-item-in-board.ts b/packages/pieces/community/monday/src/lib/triggers/new-item-in-board.ts index 4a9d4b84047c..f34f47ef671f 100644 --- a/packages/pieces/community/monday/src/lib/triggers/new-item-in-board.ts +++ b/packages/pieces/community/monday/src/lib/triggers/new-item-in-board.ts @@ -8,6 +8,7 @@ import { MondayWebhookEventType } from '../common/constants'; import { parseMondayColumnValue } from '../common/helper'; import { WebhookInformation } from '../common/models'; import { WebhookHandshakeStrategy } from '@activepieces/pieces-framework'; +import { newItemInBoardTriggerOutputSchema } from '../output-schemas'; export const newItemInBoardTrigger = createTrigger({ auth: mondayAuth, name: 'monday_new_item_in_board', @@ -17,6 +18,7 @@ export const newItemInBoardTrigger = createTrigger({ aiMetadata: { description: 'Fires when a new item (row) is created on the selected monday.com board, enriching the payload with the new item\'s column values. Represents an item-creation event scoped to one board.', }, + outputSchema: newItemInBoardTriggerOutputSchema, props: { workspace_id: mondayCommon.workspace_id(true), board_id: mondayCommon.board_id(true), diff --git a/packages/pieces/community/monday/src/lib/triggers/specific-column-updated.ts b/packages/pieces/community/monday/src/lib/triggers/specific-column-updated.ts index afb4253c9c88..009ef007c564 100644 --- a/packages/pieces/community/monday/src/lib/triggers/specific-column-updated.ts +++ b/packages/pieces/community/monday/src/lib/triggers/specific-column-updated.ts @@ -11,6 +11,7 @@ import { } from '../common/constants'; import { WebhookInformation } from '../common/models'; import { WebhookHandshakeStrategy } from '@activepieces/pieces-framework'; +import { specificColumnUpdatedTriggerOutputSchema } from '../output-schemas'; export const specificColumnValueUpdatedTrigger = createTrigger({ auth: mondayAuth, name: 'monday_specific_column_updated', @@ -20,6 +21,7 @@ export const specificColumnValueUpdatedTrigger = createTrigger({ aiMetadata: { description: 'Fires when the value of one chosen column changes on the selected monday.com board. Represents a single-column update event, carrying both the new and previous values for that column.', }, + outputSchema: specificColumnUpdatedTriggerOutputSchema, props: { workspace_id: mondayCommon.workspace_id(true), board_id: mondayCommon.board_id(true), From 6516852eefe20a7b76e6eae6bc17043e541af33b Mon Sep 17 00:00:00 2001 From: Mo AbuAboud Date: Fri, 4 Sep 2026 12:34:55 +0200 Subject: [PATCH 2/6] fix(worker): flows no longer fail with piece-not-found on newly installed pieces (#15262) --- brain/knowledge/execution-runtime/workers.md | 1 + .../server/sandbox/src/lib/cache/cache-paths.ts | 2 +- .../sandbox/src/lib/cache/pieces/piece-installer.ts | 7 +++++-- .../sandbox/test/lib/cache/piece-installer.test.ts | 13 +++++++++++++ 4 files changed, 20 insertions(+), 3 deletions(-) diff --git a/brain/knowledge/execution-runtime/workers.md b/brain/knowledge/execution-runtime/workers.md index b5a769f9f5bb..7c53b31725d2 100644 --- a/brain/knowledge/execution-runtime/workers.md +++ b/brain/knowledge/execution-runtime/workers.md @@ -20,6 +20,7 @@ The deep `Resolver`/`Runtime` concurrency and bundle-caching model lives on the - `concurrency === 1` primes the sandbox to the full container RAM (cgroup-aware) via `primeFullContainerMemory()`. ### Gotchas +- **The piece workspace pins `linker = "isolated"` in its own `bunfig.toml` — do not delete it, the engine's piece resolver depends on the layout bun chooses.** `pieceInstaller` generates a bun workspace at `//common` whose members are `pieces/-`, and the engine (`piece-loader.ts` → `resolveInstalledPieceEntry`) resolves a piece **only** at `pieces//node_modules/`, which just the isolated linker produces. The workspace used to carry no bunfig and inherited whatever ancestor bunfig bun discovered above the cache mount (`/usr/src/app/bunfig.toml`); when bun instead uses the **hoisted** linker it writes the package as a real directory at the workspace root `node_modules/@activepieces/` and leaves the member with no `node_modules` at all, so every run of that piece fails `PieceNotFoundError: Piece not found for package: -` while `bun install` still exits 0. It was silent and self-perpetuating — `markPiecesAsUsed` writes `ready` and `pieceCheckIfAlreadyInstalled` only looks for *a* `node_modules`, so the piece is reinstalled forever and never resolves. Sep 2026 production: 125/125 folders missing `node_modules` were exactly the 125 packages sitting hoisted at the workspace root; 7,400+ `EXECUTE_FLOW` jobs, 15+ platforms, ~60 official pieces, ~300/hr. Two things had to line up — bun 1.4.0 (Aug 31) changed the layout newly-installed members got, and removing the piece-upgrade Redis gate (#15236, deployed 2026-09-03 08:09 UTC) mass-upgraded every platform's flows to newer piece versions so almost every install was suddenly a *first-time* install. Diagnose on a worker with `for d in /v*/common/pieces/@activepieces/*/; do [ -d "$d/node_modules" ] || basename $d; done`. Fixed by pinning the linker plus a `LATEST_CACHE_VERSION` bump (v14 → v15) so the fleet rebuilds one clean workspace under the pinned layout rather than carrying a mixed one. - **No PM2 anymore — the container is the supervision unit.** `docker-entrypoint.sh` launches the bootstrap scripts with plain `node --enable-source-maps` (removed the `pm2-runtime` + `/tmp/ecosystem.config.js` machinery). `APP`/`WORKER` `exec` a single node as PID 1; `WORKER_AND_APP` runs both and, if either exits, kills the other and exits non-zero so the orchestrator restarts the whole container. This drops PM2's *in-container* crash/OOM restart: an OOM-kill no longer silently recycles a child every ~4 min behind a `RestartCount: 0` (the 2026-07-26 wedge's supply of retry attempts — see below); the container now dies and is rescheduled instead. The historical incident notes below still describe the old PM2 behavior as it happened. - **Version gate (rolling-deploy safety)**: dispatch requires an exact release match, enforced both sides via `versionsAreCompatible` (fail-closed — `undefined` or `UNKNOWN_VERSION` `'0.0.0'` is treated incompatible). App withholds jobs from a mismatched worker (`poll` returns null); worker pauses polling 10s. Ordinary mismatch self-heals on convergence; a read failure does not (cached for process life) and pages on-call once at startup via `assertReleaseReadable`. - Version source is `process.cwd()/package.json` (deploy-root), not a workspace file. Two failed reads are treated incompatible on purpose (not "same release"). diff --git a/packages/server/sandbox/src/lib/cache/cache-paths.ts b/packages/server/sandbox/src/lib/cache/cache-paths.ts index 28c1b18a7c30..73f3fbb93df8 100644 --- a/packages/server/sandbox/src/lib/cache/cache-paths.ts +++ b/packages/server/sandbox/src/lib/cache/cache-paths.ts @@ -75,7 +75,7 @@ async function inUseForMs(versionPath: string): Promise { return Date.now() - stats.mtimeMs } -export const LATEST_CACHE_VERSION = 'v14' +export const LATEST_CACHE_VERSION = 'v15' export const STALE_CACHE_GRACE_MS = 2 * 60 * 60 * 1000 diff --git a/packages/server/sandbox/src/lib/cache/pieces/piece-installer.ts b/packages/server/sandbox/src/lib/cache/pieces/piece-installer.ts index 604972b72e15..35df571e3927 100644 --- a/packages/server/sandbox/src/lib/cache/pieces/piece-installer.ts +++ b/packages/server/sandbox/src/lib/cache/pieces/piece-installer.ts @@ -76,7 +76,7 @@ async function installPieces(rootWorkspace: string, pieces: PiecePackage[], incl pieces: piecesToInstall.map(piece => `${piece.pieceName}-${piece.pieceVersion}`), }, '[pieceInstaller] acquired lock and starting to install pieces') - await createRootPackageJson({ + await createRootWorkspaceFiles({ path: rootWorkspace, }) @@ -207,7 +207,9 @@ function groupPiecesByPackagePath(pieces: PiecePackage[], basePath: string, getS }) } -async function createRootPackageJson({ path }: { path: string }): Promise { +const WORKSPACE_BUNFIG = '[install]\nlinker = "isolated"\nminimumReleaseAge = 259200\n' + +async function createRootWorkspaceFiles({ path }: { path: string }): Promise { const packageJsonPath = join(path, 'package.json') await fileSystemUtils.threadSafeMkdir(dirname(packageJsonPath)) await writeFileAtomic(packageJsonPath, JSON.stringify({ @@ -217,6 +219,7 @@ async function createRootPackageJson({ path }: { path: string }): Promise 'pieces/**', ], }, null, 2), 'utf8') + await writeFileAtomic(join(path, 'bunfig.toml'), WORKSPACE_BUNFIG, 'utf8') } async function createPiecePackageJson({ rootWorkspace, piecePackage }: { diff --git a/packages/server/sandbox/test/lib/cache/piece-installer.test.ts b/packages/server/sandbox/test/lib/cache/piece-installer.test.ts index 049e696cd2c0..d721c1c7778e 100644 --- a/packages/server/sandbox/test/lib/cache/piece-installer.test.ts +++ b/packages/server/sandbox/test/lib/cache/piece-installer.test.ts @@ -210,6 +210,19 @@ describe('pieceInstaller', () => { expect(manifest.dependencies['@acme/piece-sample']).toContain('bundle.tgz') }) + it('pins bun to the isolated linker in the generated workspace', async () => { + const piece = makePiece('@activepieces/piece-linked') + const installer = pieceInstaller(fakeLog, testWorkspace, fakeGetSettings) + + mockInstall.mockResolvedValueOnce({ output: '' }) + + await installer.install({ pieces: [piece], includeFilters: true, ...bundleSource }) + + const bunfig = await readFile(join(testWorkspace, 'bunfig.toml'), 'utf8') + expect(bunfig).toContain('linker = "isolated"') + expect(bunfig).toContain('minimumReleaseAge = 259200') + }) + it('skips pieces whose name is a relative path — they never reach the shared bun workspace', async () => { const good = makePiece('@activepieces/piece-good') // Stale `usedPieces` data from a since-reverted build can carry a relative path as the From 760e304a2d1b0969765b2dc0ebcc5ca335190591 Mon Sep 17 00:00:00 2001 From: Chaker Atallah <74781393+MrChaker@users.noreply.github.com> Date: Fri, 4 Sep 2026 12:02:44 +0100 Subject: [PATCH 3/6] chore: release 0.90.2 (#15271) --- docker-compose.yml | 4 ++-- package.json | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index 14e746aa5c59..1147c92fd7de 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,6 +1,6 @@ services: app: - image: ghcr.io/activepieces/activepieces:0.90.1 + image: ghcr.io/activepieces/activepieces:0.90.2 container_name: activepieces-app restart: unless-stopped ports: @@ -16,7 +16,7 @@ services: networks: - activepieces worker: - image: ghcr.io/activepieces/activepieces:0.90.1 + image: ghcr.io/activepieces/activepieces:0.90.2 restart: unless-stopped depends_on: - app diff --git a/package.json b/package.json index 5c771a74bc0c..6830de3c58a2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "activepieces", - "version": "0.90.1", + "version": "0.90.2", "packageManager": "bun@1.4.0", "trustedDependencies": [ "sqlite3", From ed45313a2510a8f00a2e33135f29a0fdf6d0f996 Mon Sep 17 00:00:00 2001 From: Odai Ahmad Date: Fri, 4 Sep 2026 14:25:10 +0300 Subject: [PATCH 4/6] feat(clay): send rows to a table and trigger on rows Clay sends back (#15222) --- bun.lock | 6 +- packages/pieces/community/clay/.eslintrc.json | 19 +- packages/pieces/community/clay/package.json | 9 +- packages/pieces/community/clay/src/index.ts | 12 +- .../clay/src/lib/actions/send-row-to-table.ts | 45 ++ .../clay/src/lib/common/output-schemas.ts | 19 + .../community/clay/src/lib/common/webhook.ts | 196 +++++++++ .../clay/src/lib/triggers/row-received.ts | 82 ++++ .../community/clay/test/webhook.test.ts | 391 ++++++++++++++++++ .../pieces/community/clay/vitest.config.ts | 17 + 10 files changed, 785 insertions(+), 11 deletions(-) create mode 100644 packages/pieces/community/clay/src/lib/actions/send-row-to-table.ts create mode 100644 packages/pieces/community/clay/src/lib/common/output-schemas.ts create mode 100644 packages/pieces/community/clay/src/lib/common/webhook.ts create mode 100644 packages/pieces/community/clay/src/lib/triggers/row-received.ts create mode 100644 packages/pieces/community/clay/test/webhook.test.ts create mode 100644 packages/pieces/community/clay/vitest.config.ts diff --git a/bun.lock b/bun.lock index 88f4b48df05a..84593ec8af2a 100644 --- a/bun.lock +++ b/bun.lock @@ -1890,13 +1890,15 @@ }, "packages/pieces/community/clay": { "name": "@activepieces/piece-clay", - "version": "0.0.1", + "version": "0.1.0", "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", - "@activepieces/shared": "workspace:*", "tslib": "2.6.2", }, + "devDependencies": { + "vitest": "3.2.6", + }, }, "packages/pieces/community/clearout": { "name": "@activepieces/piece-clearout", diff --git a/packages/pieces/community/clay/.eslintrc.json b/packages/pieces/community/clay/.eslintrc.json index a86bd8287d5a..9ee1a0770e89 100644 --- a/packages/pieces/community/clay/.eslintrc.json +++ b/packages/pieces/community/clay/.eslintrc.json @@ -3,7 +3,24 @@ "ignorePatterns": ["!**/*"], "overrides": [ { "files": ["*.ts", "*.tsx", "*.js", "*.jsx"], "rules": {} }, - { "files": ["*.ts", "*.tsx"], "rules": {} }, + { + "files": ["*.ts", "*.tsx"], + "rules": { + "no-restricted-imports": [ + "error", + { + "patterns": [ + "lodash", + "lodash/*", + "@activepieces/core-*", + "@activepieces/server*", + "@activepieces/engine", + "@activepieces/shared" + ] + } + ] + } + }, { "files": ["*.js", "*.jsx"], "rules": {} } ] } diff --git a/packages/pieces/community/clay/package.json b/packages/pieces/community/clay/package.json index 29681602bd19..501ac1edb1bb 100644 --- a/packages/pieces/community/clay/package.json +++ b/packages/pieces/community/clay/package.json @@ -1,16 +1,19 @@ { "name": "@activepieces/piece-clay", - "version": "0.0.1", + "version": "0.1.0", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { "build": "tsc -p tsconfig.lib.json && cp package.json dist/", - "lint": "eslint 'src/**/*.ts'" + "lint": "eslint 'src/**/*.ts'", + "test": "vitest run" }, "dependencies": { "@activepieces/pieces-common": "workspace:*", "@activepieces/pieces-framework": "workspace:*", - "@activepieces/shared": "workspace:*", "tslib": "2.6.2" + }, + "devDependencies": { + "vitest": "3.2.6" } } diff --git a/packages/pieces/community/clay/src/index.ts b/packages/pieces/community/clay/src/index.ts index 1b3c50b79ebc..5efb740a2561 100644 --- a/packages/pieces/community/clay/src/index.ts +++ b/packages/pieces/community/clay/src/index.ts @@ -1,21 +1,23 @@ -import { createPiece } from '@activepieces/pieces-framework'; +import { createPiece, PieceCategory } from '@activepieces/pieces-framework'; import { createCustomApiCallAction } from '@activepieces/pieces-common'; -import { PieceCategory } from '@activepieces/shared'; import { clayAuth } from './lib/auth'; import { searchCompaniesAction } from './lib/actions/search-companies'; import { searchPeopleAction } from './lib/actions/search-people'; +import { sendRowToTableAction } from './lib/actions/send-row-to-table'; +import { rowReceivedTrigger } from './lib/triggers/row-received'; export const clay = createPiece({ displayName: 'Clay', - description: 'Search Clay\'s GTM database for people and companies.', + description: 'Search Clay\'s GTM database, and move table rows in and out of Clay.', minimumSupportedRelease: '0.36.1', logoUrl: 'https://cdn.activepieces.com/pieces/clay.png', categories: [PieceCategory.SALES_AND_CRM], auth: clayAuth, - authors: ['kishanprmr'], + authors: ['kishanprmr', 'OdaiAhmed99'], actions: [ searchCompaniesAction, searchPeopleAction, + sendRowToTableAction, createCustomApiCallAction({ baseUrl: () => 'https://api.clay.com/public/v0', auth: clayAuth, @@ -24,5 +26,5 @@ export const clay = createPiece({ }), }), ], - triggers: [], + triggers: [rowReceivedTrigger], }); diff --git a/packages/pieces/community/clay/src/lib/actions/send-row-to-table.ts b/packages/pieces/community/clay/src/lib/actions/send-row-to-table.ts new file mode 100644 index 000000000000..ce922310ab49 --- /dev/null +++ b/packages/pieces/community/clay/src/lib/actions/send-row-to-table.ts @@ -0,0 +1,45 @@ +import { createAction, Property } from '@activepieces/pieces-framework'; +import { clayWebhook } from '../common/webhook'; +import { sendRowOutputSchema } from '../common/output-schemas'; + +export const sendRowToTableAction = createAction({ + name: 'send_row_to_table', + displayName: 'Send Row to Clay Table', + description: 'Sends a row to a Clay table through its webhook source.', + classification: 'WRITE', + audience: 'both', + aiMetadata: { + description: + 'Sends one row of data into a Clay table through that table\'s webhook source, where it runs the table\'s enrichment columns. Requires the table\'s webhook URL, which is generated in Clay on the table itself and must be an https address on clay.com, plus that source\'s authentication token when it has one. Clay only acknowledges receipt, so a successful call means the row was accepted, not that enrichment has finished. Whether re-sending the same data updates the existing row or appends another one depends on the table\'s own configuration, so treat a retry as capable of adding a duplicate row.', + idempotent: false, + }, + outputSchema: sendRowOutputSchema, + requireAuth: false, + props: { + webhookUrl: Property.ShortText({ + displayName: 'Webhook URL', + description: + 'From the table\'s webhook source in Clay, the value in its Webhook URL panel. Only https addresses on clay.com are accepted.', + required: true, + }), + authToken: Property.ShortText({ + displayName: 'Authentication Token', + description: + 'The token Clay showed when the source was created. Leave empty only if the source has none. Clay shows it once, so use Refresh auth token on the source if it was not saved.', + required: false, + }), + row: Property.Object({ + displayName: 'Row', + description: + 'The fields to send, as key-value pairs. Keys should match the source\'s Setup mapping panel.', + required: true, + }), + }, + async run({ propsValue }) { + return await clayWebhook.sendRow({ + webhookUrl: propsValue.webhookUrl, + authToken: propsValue.authToken, + row: propsValue.row, + }); + }, +}); diff --git a/packages/pieces/community/clay/src/lib/common/output-schemas.ts b/packages/pieces/community/clay/src/lib/common/output-schemas.ts new file mode 100644 index 000000000000..f94357e8f37c --- /dev/null +++ b/packages/pieces/community/clay/src/lib/common/output-schemas.ts @@ -0,0 +1,19 @@ +import { OutputSchema } from '@activepieces/pieces-framework'; + +export const sendRowOutputSchema: OutputSchema = { + fields: [ + { + key: 'success', + label: 'Accepted', + format: 'boolean', + description: + 'True when Clay accepted the row. Clay acknowledges receipt only, and the table\'s enrichment columns run afterwards, so this does not mean enrichment has finished.', + }, + { + key: 'response', + label: 'Clay Response', + description: + 'Clay\'s own acknowledgement, passed through unchanged. Its shape follows the Send response as setting on the webhook source: an object such as {"success": true} for JSON, or the text OK for Plaintext.', + }, + ], +}; diff --git a/packages/pieces/community/clay/src/lib/common/webhook.ts b/packages/pieces/community/clay/src/lib/common/webhook.ts new file mode 100644 index 000000000000..b6818309a313 --- /dev/null +++ b/packages/pieces/community/clay/src/lib/common/webhook.ts @@ -0,0 +1,196 @@ +import { httpClient, HttpMethod } from '@activepieces/pieces-common'; +import { isNil, tryCatch } from '@activepieces/pieces-framework'; +import crypto from 'crypto'; + +async function sendRow({ + webhookUrl, + authToken, + row, +}: { + webhookUrl: string; + authToken?: string; + row: Record; +}): Promise { + const url = normalizeSourceUrl(webhookUrl); + const token = trimmedSecretOf(authToken); + const result = await tryCatch(() => + httpClient.sendRequest({ + method: HttpMethod.POST, + url, + headers: isNil(token) + ? undefined + : { [WEBHOOK_AUTH_HEADER]: token }, + body: row, + }), + ); + + if (result.error !== null) { + throw new Error(sendFailureMessage({ error: result.error, authToken: token })); + } + + return { success: true, response: result.data.body }; +} + +function verifySignature({ + signingSecret, + rawBody, + signatureHeader, +}: { + signingSecret: string; + rawBody: unknown; + signatureHeader: string | undefined; +}): boolean { + const signedBytes = signedPayloadOf(rawBody); + if (isNil(signatureHeader) || isNil(signedBytes)) { + return false; + } + + const provided = signatureHeader.startsWith(SIGNATURE_PREFIX) + ? signatureHeader.slice(SIGNATURE_PREFIX.length) + : signatureHeader; + const expected = crypto + .createHmac('sha256', signingSecret) + .update(signedBytes) + .digest('hex'); + + const providedBytes = Buffer.from(provided, 'hex'); + const expectedBytes = Buffer.from(expected, 'hex'); + if ( + providedBytes.length === 0 || + providedBytes.length !== expectedBytes.length + ) { + return false; + } + + return crypto.timingSafeEqual(providedBytes, expectedBytes); +} + +function signatureHeaderOf(headers: Record | undefined): string | undefined { + if (isNil(headers)) { + return undefined; + } + const match = Object.keys(headers).find( + (name) => name.toLowerCase() === SIGNATURE_HEADER, + ); + return isNil(match) ? undefined : headers[match]; +} + +function isVerificationPing(body: unknown): boolean { + if (typeof body !== 'object' || isNil(body)) { + return false; + } + if (isNil(Reflect.get(body, 'webhookId'))) { + return false; + } + const row = Reflect.get(body, 'data'); + if (isNil(row)) { + return true; + } + return typeof row === 'object' && Object.keys(row).length === 0; +} + +function signedPayloadOf(rawBody: unknown): string | Buffer | undefined { + if (typeof rawBody === 'string' || Buffer.isBuffer(rawBody)) { + return rawBody; + } + return undefined; +} + +function normalizeSourceUrl(webhookUrl: string): string { + const trimmed = webhookUrl.trim(); + if (trimmed.length === 0) { + throw new Error('Webhook URL is required'); + } + + const parsed = parsedUrlOf(trimmed); + if (isNil(parsed)) { + throw new Error( + `Webhook URL is not a valid URL. ${COPY_FROM_CLAY}`, + ); + } + if (parsed.protocol !== 'https:') { + throw new Error( + `Webhook URL must use https, because the row and the source token are sent to it. ${COPY_FROM_CLAY}`, + ); + } + if (!isClayHost(parsed.hostname)) { + throw new Error( + `Webhook URL must point at Clay, but this one points at ${parsed.hostname}. The row and the source token are sent to this address, so anywhere else is refused. ${COPY_FROM_CLAY}`, + ); + } + + return trimmed; +} + +function trimmedSecretOf(secret: string | undefined): string | undefined { + const trimmed = secret?.trim(); + return isNil(trimmed) || trimmed.length === 0 ? undefined : trimmed; +} + +function parsedUrlOf(value: string): URL | undefined { + try { + return new URL(value); + } + catch { + return undefined; + } +} + +function isClayHost(hostname: string): boolean { + const host = hostname.toLowerCase(); + return host === CLAY_DOMAIN || host.endsWith(`.${CLAY_DOMAIN}`); +} + +function sendFailureMessage({ + error, + authToken, +}: { + error: Error; + authToken?: string; +}): string { + const status = statusOf(error); + + if (status === 401) { + return isNil(authToken) + ? 'Clay rejected the row with 401 Unauthorized. This webhook source has an authentication token, so fill in Authentication Token with the value Clay showed when the source was created.' + : 'Clay rejected the row with 401 Unauthorized. The authentication token does not match the one on this Clay webhook source. Note that a newly refreshed token can take up to a minute to become active.'; + } + if (status === 404) { + return 'Clay returned 404 for this webhook URL. Check the URL against the Webhook URL panel on the table source, and that the source has not been deleted.'; + } + + return `Clay rejected the row${isNil(status) ? '' : ` with ${status}`}: ${error.message}`; +} + +function statusOf(error: unknown): number | undefined { + if (typeof error !== 'object' || isNil(error)) { + return undefined; + } + const response = Reflect.get(error, 'response'); + if (typeof response !== 'object' || isNil(response)) { + return undefined; + } + const status = Reflect.get(response, 'status'); + return typeof status === 'number' ? status : undefined; +} + +export const clayWebhook = { + sendRow, + verifySignature, + signatureHeaderOf, + isVerificationPing, + normalizeSourceUrl, + trimmedSecretOf, +}; + +export const WEBHOOK_AUTH_HEADER = 'x-clay-webhook-auth'; +export const SIGNATURE_HEADER = 'x-clay-signature'; +export const SIGNATURE_PREFIX = 'sha256='; +export const CLAY_DOMAIN = 'clay.com'; +export const COPY_FROM_CLAY = + 'Copy it from the Webhook URL panel on your Clay table source.'; + +export type ClayWebhookResult = { + success: boolean; + response: unknown; +}; diff --git a/packages/pieces/community/clay/src/lib/triggers/row-received.ts b/packages/pieces/community/clay/src/lib/triggers/row-received.ts new file mode 100644 index 000000000000..edf54dc341d0 --- /dev/null +++ b/packages/pieces/community/clay/src/lib/triggers/row-received.ts @@ -0,0 +1,82 @@ +import { + createTrigger, + Property, + TriggerStrategy, +} from '@activepieces/pieces-framework'; +import { clayWebhook } from '../common/webhook'; + +export const rowReceivedTrigger = createTrigger({ + name: 'row_received', + displayName: 'Row Received from Clay', + description: 'Triggers when Clay sends a row to this flow.', + aiMetadata: { + description: + 'Fires when Clay posts a row to this flow, carrying whatever fields the Clay side was configured to send. Clay has no API for registering webhooks, so somebody has to point a Clay webhook or an HTTP API enrichment column at this trigger\'s URL by hand before it receives anything.', + }, + type: TriggerStrategy.WEBHOOK, + requireAuth: false, + props: { + setupInstructions: Property.MarkDown({ + value: ` +Clay cannot register a webhook for you, so paste this URL into Clay yourself: + +\`\`\`text +{{webhookUrl}} +\`\`\` + +**Sending rows from a table** is the usual route. Add an **HTTP API** column, then: + +1. Set the method to \`POST\` and paste the URL into **Endpoint**. Paste only the URL - typing \`/\` there opens Clay's column picker and corrupts it. +2. Set the **Body** to the columns you want, for example \`{"Domain": "/Domain"}\`. +3. Leave **Signing Secret** below empty. An HTTP API column cannot sign a request, so a secret there rejects every delivery. + +**For workspace events**, create a webhook in Clay's settings and paste the URL into its **Webhook URL** field. Its first verification request carries an empty row and is ignored. Optionally put that webhook's \`whsec_...\` secret in **Signing Secret** below. + `, + }), + signingSecret: Property.ShortText({ + displayName: 'Signing Secret', + description: + 'The whsec_... secret of a webhook created in Clay\'s settings, used to verify every delivery. Leave empty for an HTTP API column, which cannot sign requests.', + required: false, + }), + }, + sampleData: { + Domain: 'activepieces.com', + Company: 'Activepieces', + }, + + async onEnable() { + return; + }, + + async onDisable() { + return; + }, + + async run(context) { + const signingSecret = clayWebhook.trimmedSecretOf( + context.propsValue.signingSecret, + ); + + if (signingSecret) { + const verified = clayWebhook.verifySignature({ + signingSecret, + rawBody: context.payload.rawBody, + signatureHeader: clayWebhook.signatureHeaderOf( + context.payload.headers, + ), + }); + if (!verified) { + throw new Error( + 'The x-clay-signature header did not match the signing secret, so this request was not accepted. Check that Signing Secret matches the secret Clay showed for this webhook, including its whsec_ prefix.', + ); + } + } + + if (clayWebhook.isVerificationPing(context.payload.body)) { + return []; + } + + return [context.payload.body]; + }, +}); diff --git a/packages/pieces/community/clay/test/webhook.test.ts b/packages/pieces/community/clay/test/webhook.test.ts new file mode 100644 index 000000000000..299a97102b79 --- /dev/null +++ b/packages/pieces/community/clay/test/webhook.test.ts @@ -0,0 +1,391 @@ +/// + +import { + HttpMethod, + HttpRequest, + HttpResponse, + httpClient, +} from '@activepieces/pieces-common'; +import { clayWebhook } from '../src/lib/common/webhook'; + +const FIXTURE_SECRET = + 'whsec_testonlyfixture000000000000000000000000000000000000000000000'; +const FIXTURE_BODY = + '{"webhookId":"wh_fixture","createdAt":"2026-09-02T13:25:15.924Z","data":{"Domain":"activepieces.com"}}'; +const FIXTURE_SIGNATURE = + 'sha256=f787d8f6ca1d5ea61ac6ae74dceb201cafc5b41ccd1593522c1344837d102c19'; + +let sendRequest: ReturnType; +let lastRequest: HttpRequest | undefined; + +beforeEach(() => { + lastRequest = undefined; + sendRequest = vi.spyOn(httpClient, 'sendRequest'); +}); + +afterEach(() => { + vi.restoreAllMocks(); +}); + +function acknowledgeWith(body: unknown) { + sendRequest.mockImplementation(async (request: HttpRequest): Promise => { + lastRequest = request; + return { status: 200, headers: {}, body }; + }); +} + +function httpError(status: number) { + return Object.assign(new Error(`Request failed with status ${status}`), { + response: { status, body: { message: 'nope' } }, + }); +} + +describe('signature verification', () => { + test('a signature Clay would send is accepted', () => { + expect( + clayWebhook.verifySignature({ + signingSecret: FIXTURE_SECRET, + rawBody: FIXTURE_BODY, + signatureHeader: FIXTURE_SIGNATURE, + }), + ).toBe(true); + }); + + test('the whsec_ prefix is part of the key, not stripped like Standard Webhooks does', () => { + const standardWebhooksSignature = + 'sha256=b0169390732d1cb7ae61f1927bd7c5ac181d1c6262331cabf5a28545d0719efa'; + + expect( + clayWebhook.verifySignature({ + signingSecret: FIXTURE_SECRET, + rawBody: FIXTURE_BODY, + signatureHeader: standardWebhooksSignature, + }), + ).toBe(false); + }); + + test('a tampered body is rejected', () => { + expect( + clayWebhook.verifySignature({ + signingSecret: FIXTURE_SECRET, + rawBody: FIXTURE_BODY.replace('activepieces.com', 'evil.com'), + signatureHeader: FIXTURE_SIGNATURE, + }), + ).toBe(false); + }); + + test('a different secret is rejected', () => { + expect( + clayWebhook.verifySignature({ + signingSecret: 'whsec_someoneelsessecret', + rawBody: FIXTURE_BODY, + signatureHeader: FIXTURE_SIGNATURE, + }), + ).toBe(false); + }); + + test('a signature without the sha256= prefix still verifies', () => { + expect( + clayWebhook.verifySignature({ + signingSecret: FIXTURE_SECRET, + rawBody: FIXTURE_BODY, + signatureHeader: FIXTURE_SIGNATURE.replace('sha256=', ''), + }), + ).toBe(true); + }); + + test('a parsed body is rejected, since re-serialising changes the bytes', () => { + expect( + clayWebhook.verifySignature({ + signingSecret: FIXTURE_SECRET, + rawBody: JSON.parse(FIXTURE_BODY), + signatureHeader: FIXTURE_SIGNATURE, + }), + ).toBe(false); + }); + + test('a missing signature header is rejected', () => { + expect( + clayWebhook.verifySignature({ + signingSecret: FIXTURE_SECRET, + rawBody: FIXTURE_BODY, + signatureHeader: undefined, + }), + ).toBe(false); + }); + + test('an empty signature is rejected rather than throwing', () => { + expect( + clayWebhook.verifySignature({ + signingSecret: FIXTURE_SECRET, + rawBody: FIXTURE_BODY, + signatureHeader: 'sha256=', + }), + ).toBe(false); + }); + + test('the header is found whatever its casing', () => { + expect( + clayWebhook.signatureHeaderOf({ 'X-Clay-Signature': 'sha256=abc' }), + ).toBe('sha256=abc'); + expect( + clayWebhook.signatureHeaderOf({ 'x-clay-signature': 'sha256=abc' }), + ).toBe('sha256=abc'); + expect(clayWebhook.signatureHeaderOf({ other: 'x' })).toBeUndefined(); + expect(clayWebhook.signatureHeaderOf(undefined)).toBeUndefined(); + }); +}); + +describe('the verification ping Clay sends when a webhook is created', () => { + test('an enveloped delivery with an empty row is a ping', () => { + expect( + clayWebhook.isVerificationPing({ + webhookId: 'wh_x', + createdAt: '2026-09-02', + data: {}, + }), + ).toBe(true); + }); + + test('an enveloped delivery carrying a row is not a ping', () => { + expect( + clayWebhook.isVerificationPing({ + webhookId: 'wh_x', + createdAt: '2026-09-02', + data: { Domain: 'activepieces.com' }, + }), + ).toBe(false); + }); + + test('a flat payload from an HTTP API column is never a ping', () => { + expect( + clayWebhook.isVerificationPing({ Domain: 'activepieces.com' }), + ).toBe(false); + expect( + clayWebhook.isVerificationPing({ Domain: 'stripe.com', Company: 'Stripe' }), + ).toBe(false); + expect(clayWebhook.isVerificationPing({})).toBe(false); + }); +}); + +describe('a secret pasted into a field, whitespace and all', () => { + test('surrounding whitespace is trimmed, so a pasted secret still verifies', () => { + expect( + clayWebhook.verifySignature({ + signingSecret: + clayWebhook.trimmedSecretOf(` ${FIXTURE_SECRET}\n`) ?? '', + rawBody: FIXTURE_BODY, + signatureHeader: FIXTURE_SIGNATURE, + }), + ).toBe(true); + }); + + test('whitespace alone counts as empty, so it never enables verification', () => { + expect(clayWebhook.trimmedSecretOf(' ')).toBeUndefined(); + expect(clayWebhook.trimmedSecretOf('')).toBeUndefined(); + expect(clayWebhook.trimmedSecretOf(undefined)).toBeUndefined(); + }); +}); + +describe('webhook URL handling', () => { + test('surrounding whitespace is trimmed', () => { + expect( + clayWebhook.normalizeSourceUrl(' https://api.clay.com/v3/sources/webhook/x '), + ).toBe('https://api.clay.com/v3/sources/webhook/x'); + }); + + test('a URL without a scheme is refused rather than guessed at', () => { + expect(() => + clayWebhook.normalizeSourceUrl('api.clay.com/v3/sources/webhook/x'), + ).toThrow(/not a valid URL/i); + }); + + test('a blank URL is refused', () => { + expect(() => clayWebhook.normalizeSourceUrl(' ')).toThrow(/required/i); + }); + + test('a Clay subdomain is accepted, so their infrastructure can move', () => { + expect( + clayWebhook.normalizeSourceUrl('https://eu.api.clay.com/v3/sources/webhook/x'), + ).toBe('https://eu.api.clay.com/v3/sources/webhook/x'); + }); +}); + +describe('the destination is constrained to Clay, because the token travels with the row', () => { + test('a non-Clay host is refused and named in the error', () => { + expect(() => + clayWebhook.normalizeSourceUrl('https://evil.example.com/collect'), + ).toThrow(/evil\.example\.com/); + }); + + test('a lookalike domain is refused', () => { + expect(() => + clayWebhook.normalizeSourceUrl('https://notclay.com/v3/sources/webhook/x'), + ).toThrow(/must point at Clay/i); + }); + + test('a domain that merely ends in the brand name is refused', () => { + expect(() => + clayWebhook.normalizeSourceUrl('https://evil-clay.com/v3/sources/webhook/x'), + ).toThrow(/must point at Clay/i); + }); + + test('Clay as a prefix of another domain is refused', () => { + expect(() => + clayWebhook.normalizeSourceUrl('https://clay.com.evil.example/collect'), + ).toThrow(/must point at Clay/i); + }); + + test('plain http is refused even on a Clay host', () => { + expect(() => + clayWebhook.normalizeSourceUrl('http://api.clay.com/v3/sources/webhook/x'), + ).toThrow(/https/i); + }); + + test('a loopback address is refused, so the token cannot be pointed inward', () => { + expect(() => + clayWebhook.normalizeSourceUrl('https://127.0.0.1/collect'), + ).toThrow(/must point at Clay/i); + }); + + test('no request is attempted when the destination is refused', async () => { + await expect( + clayWebhook.sendRow({ + webhookUrl: 'https://evil.example.com/collect', + authToken: 'a-token', + row: { Domain: 'activepieces.com' }, + }), + ).rejects.toThrow(/must point at Clay/i); + + expect(sendRequest).not.toHaveBeenCalled(); + }); +}); + +describe('sending a row', () => { + test('the token travels in the x-clay-webhook-auth header', async () => { + acknowledgeWith({ success: true }); + await clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + authToken: 'a-token', + row: { Domain: 'activepieces.com' }, + }); + + expect(lastRequest?.headers).toEqual({ 'x-clay-webhook-auth': 'a-token' }); + }); + + test('no header is sent when the source has no token', async () => { + acknowledgeWith({ success: true }); + await clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + row: { Domain: 'activepieces.com' }, + }); + + expect(lastRequest?.headers).toBeUndefined(); + }); + + test('the row is sent as the request body, flat', async () => { + acknowledgeWith({ success: true }); + await clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + row: { Domain: 'activepieces.com', Company: 'Activepieces' }, + }); + + expect(lastRequest?.method).toBe(HttpMethod.POST); + expect(lastRequest?.body).toEqual({ + Domain: 'activepieces.com', + Company: 'Activepieces', + }); + }); + + test('a JSON acknowledgement is reported alongside a stable success flag', async () => { + acknowledgeWith({ success: true }); + + await expect( + clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + row: {}, + }), + ).resolves.toEqual({ success: true, response: { success: true } }); + }); + + test('a plaintext acknowledgement keeps the same success flag', async () => { + acknowledgeWith('OK'); + + await expect( + clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + row: {}, + }), + ).resolves.toEqual({ success: true, response: 'OK' }); + }); + + test('a 401 with no token names the field to fill in', async () => { + sendRequest.mockRejectedValueOnce(httpError(401)); + + await expect( + clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + row: {}, + }), + ).rejects.toThrow(/Authentication Token/); + }); + + test('a 401 with a token says the token does not match', async () => { + sendRequest.mockRejectedValueOnce(httpError(401)); + + await expect( + clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + authToken: 'wrong', + row: {}, + }), + ).rejects.toThrow(/does not match/); + }); + + test('a 404 points at the source rather than the token', async () => { + sendRequest.mockRejectedValueOnce(httpError(404)); + + await expect( + clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + row: {}, + }), + ).rejects.toThrow(/404/); + }); + + test('an unrecognised failure still surfaces Clay\'s own message', async () => { + sendRequest.mockRejectedValueOnce(httpError(500)); + + await expect( + clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + row: {}, + }), + ).rejects.toThrow(/500/); + }); +}); + +describe('a token pasted into a field, whitespace and all', () => { + test('surrounding whitespace is trimmed before the token reaches Clay', async () => { + acknowledgeWith({ success: true }); + await clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + authToken: ' a-token\n', + row: { Domain: 'activepieces.com' }, + }); + + expect(lastRequest?.headers).toEqual({ 'x-clay-webhook-auth': 'a-token' }); + }); + + test('a whitespace-only token is treated as absent, so the 401 names the field to fill in', async () => { + sendRequest.mockRejectedValueOnce(httpError(401)); + + await expect( + clayWebhook.sendRow({ + webhookUrl: 'https://api.clay.com/v3/sources/webhook/x', + authToken: ' ', + row: {}, + }), + ).rejects.toThrow(/Authentication Token/); + }); +}); diff --git a/packages/pieces/community/clay/vitest.config.ts b/packages/pieces/community/clay/vitest.config.ts new file mode 100644 index 000000000000..ba8ade4a1780 --- /dev/null +++ b/packages/pieces/community/clay/vitest.config.ts @@ -0,0 +1,17 @@ +import path from 'path' +import { defineConfig } from 'vitest/config' + +const repoRoot = path.resolve(__dirname, '../../../..') + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, + resolve: { + alias: { + '@activepieces/pieces-framework': path.resolve(repoRoot, 'packages/pieces/framework/src/index.ts'), + '@activepieces/pieces-common': path.resolve(repoRoot, 'packages/pieces/common/src/index.ts'), + }, + }, +}) From 9f7f282d5bb943a1d5462ffd7feccf2d3e02b705 Mon Sep 17 00:00:00 2001 From: Bartosz Majewski <30874844+majewskibartosz@users.noreply.github.com> Date: Fri, 4 Sep 2026 14:10:07 +0200 Subject: [PATCH 5/6] fix(ai): Ask AI and Summarize Text no longer fail on models that reject the temperature setting (#15269) --- bun.lock | 5 +- packages/pieces/community/ai/package.json | 8 ++- .../ai/src/lib/actions/text/ask-ai.ts | 9 +-- .../ai/src/lib/actions/text/summarize-text.ts | 3 +- .../src/lib/actions/text/temperature.test.ts | 69 +++++++++++++++++++ .../pieces/community/ai/tsconfig.lib.json | 1 + packages/pieces/community/ai/vitest.config.ts | 8 +++ 7 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 packages/pieces/community/ai/src/lib/actions/text/temperature.test.ts create mode 100644 packages/pieces/community/ai/vitest.config.ts diff --git a/bun.lock b/bun.lock index 84593ec8af2a..46c783b9962e 100644 --- a/bun.lock +++ b/bun.lock @@ -336,7 +336,7 @@ }, "packages/pieces/community/ai": { "name": "@activepieces/piece-ai", - "version": "0.10.0", + "version": "0.10.1", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", @@ -362,6 +362,7 @@ "devDependencies": { "@types/mime-types": "2.1.1", "tslib": "2.6.2", + "vitest": "3.2.6", }, }, "packages/pieces/community/aianswer": { @@ -1127,7 +1128,7 @@ }, "packages/pieces/community/bettermode": { "name": "@activepieces/piece-bettermode", - "version": "0.1.8", + "version": "0.1.9", "dependencies": { "@activepieces/core-piece-types": "workspace:*", "@activepieces/core-utils": "workspace:*", diff --git a/packages/pieces/community/ai/package.json b/packages/pieces/community/ai/package.json index 1be4ddf111ce..75a07c20bdcc 100644 --- a/packages/pieces/community/ai/package.json +++ b/packages/pieces/community/ai/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-ai", - "version": "0.10.0", + "version": "0.10.1", "type": "commonjs", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", @@ -29,10 +29,12 @@ "scripts": { "build": "tsc -p tsconfig.lib.json && cp package.json dist/", "bundle": "node ../../../../dist/packages/cli/src/index.js pieces bundle", - "lint": "eslint 'src/**/*.ts'" + "lint": "eslint 'src/**/*.ts'", + "test": "vitest run" }, "devDependencies": { "@types/mime-types": "2.1.1", - "tslib": "2.6.2" + "tslib": "2.6.2", + "vitest": "3.2.6" } } diff --git a/packages/pieces/community/ai/src/lib/actions/text/ask-ai.ts b/packages/pieces/community/ai/src/lib/actions/text/ask-ai.ts index 3ca7e01bc1e6..03de83ac5095 100644 --- a/packages/pieces/community/ai/src/lib/actions/text/ask-ai.ts +++ b/packages/pieces/community/ai/src/lib/actions/text/ask-ai.ts @@ -1,9 +1,5 @@ -import { - createAction, - Property, -} from '@activepieces/pieces-framework'; import { ModelMessage, generateText, stepCountIs } from 'ai'; -import { AIProviderName, getEffectiveProviderAndModel, spreadIfDefined } from '@activepieces/pieces-framework'; +import { AIProviderName, createAction, getEffectiveProviderAndModel, isNil, Property, spreadIfDefined } from '@activepieces/pieces-framework'; import { aiProps, aiProviderSelection } from '../../common/props'; import { createAIModel } from '../../common/ai-sdk'; import { buildWebSearchOptionsProperty, buildWebSearchConfig, WebSearchOptions } from '../../common/web-search'; @@ -29,7 +25,6 @@ export const askAI = createAction({ creativity: Property.Number({ displayName: 'Creativity', required: false, - defaultValue: 100, description: 'Controls the creativity of the AI response. A higher value will make the AI more creative and a lower value will make it more deterministic.', }), @@ -109,7 +104,7 @@ export const askAI = createAction({ }, ], maxOutputTokens: context.propsValue.maxOutputTokens, - temperature: (context.propsValue.creativity ?? 100) / 100, + ...spreadIfDefined('temperature', isNil(context.propsValue.creativity) ? undefined : context.propsValue.creativity / 100), tools: webSearchTools, stopWhen, providerOptions, diff --git a/packages/pieces/community/ai/src/lib/actions/text/summarize-text.ts b/packages/pieces/community/ai/src/lib/actions/text/summarize-text.ts index 0f58398a5034..e60eb26c65ef 100644 --- a/packages/pieces/community/ai/src/lib/actions/text/summarize-text.ts +++ b/packages/pieces/community/ai/src/lib/actions/text/summarize-text.ts @@ -10,7 +10,7 @@ export const summarizeText = createAction({ classification: 'READ', displayName: 'Summarize Text', description: 'Summarize long emails, articles, or documents into what matters.', - aiMetadata: { description: 'Condenses one block of supplied text into a shorter summary using a chosen text model. Pick it when the goal is a shorter version of text you already have; use extractStructuredData for specific typed fields, classifyText for a label, or askAi for open-ended questions. Requires a provider/model, the text inline (it fetches no URLs and reads no files) and the Prompt prop, which carries a default guide instruction but is still required; not idempotent, as generation runs at temperature 1, so identical text returns differently worded summaries.', idempotent: false }, + aiMetadata: { description: 'Condenses one block of supplied text into a shorter summary using a chosen text model. Pick it when the goal is a shorter version of text you already have; use extractStructuredData for specific typed fields, classifyText for a label, or askAi for open-ended questions. Requires a provider/model, the text inline (it fetches no URLs and reads no files) and the Prompt prop, which carries a default guide instruction but is still required; not idempotent, as generation is non-deterministic, so identical text returns differently worded summaries.', idempotent: false }, props: { provider: aiProps({ modelType: 'text' }).provider, model: aiProps({ modelType: 'text' }).model, @@ -54,7 +54,6 @@ export const summarizeText = createAction({ }, ], maxOutputTokens: context.propsValue.maxOutputTokens, - temperature: 1, providerOptions: { [provider]: { ...(provider === AIProviderName.OPENAI ? { reasoning_effort: 'minimal' } : {}), diff --git a/packages/pieces/community/ai/src/lib/actions/text/temperature.test.ts b/packages/pieces/community/ai/src/lib/actions/text/temperature.test.ts new file mode 100644 index 000000000000..7bd72926d737 --- /dev/null +++ b/packages/pieces/community/ai/src/lib/actions/text/temperature.test.ts @@ -0,0 +1,69 @@ +import { beforeEach, describe, expect, it, vi } from 'vitest'; +import { createMockActionContext } from '@activepieces/pieces-framework'; +import { generateText } from 'ai'; +import { askAI } from './ask-ai'; +import { summarizeText } from './summarize-text'; + +vi.mock('ai', () => ({ + generateText: vi.fn(async () => ({ text: 'ok', sources: [] })), + stepCountIs: vi.fn(), +})); + +vi.mock('../../common/ai-sdk', () => ({ + createAIModel: vi.fn(async () => ({})), +})); + +const generateTextMock = vi.mocked(generateText); + +const baseProps = { + provider: { provider: 'openai', configId: 'config1' }, + model: 'gpt-test', + maxOutputTokens: 2000, +}; + +async function askAiGenerateTextArgs({ creativity }: { creativity: number | null | undefined }) { + await askAI.run(createMockActionContext({ + propsValue: { ...baseProps, prompt: 'hello', webSearch: false, creativity }, + })); + return generateTextMock.mock.calls[0][0]; +} + +beforeEach(() => { + generateTextMock.mockClear(); +}); + +describe('askAI temperature', () => { + it('omits temperature when creativity is not set', async () => { + const args = await askAiGenerateTextArgs({ creativity: undefined }); + expect(args).not.toHaveProperty('temperature'); + }); + + it('omits temperature when creativity is null', async () => { + const args = await askAiGenerateTextArgs({ creativity: null }); + expect(args).not.toHaveProperty('temperature'); + }); + + it('sends temperature scaled from an explicit creativity', async () => { + const args = await askAiGenerateTextArgs({ creativity: 50 }); + expect(args.temperature).toBe(0.5); + }); + + it('sends temperature 1 for the previously seeded default of 100', async () => { + const args = await askAiGenerateTextArgs({ creativity: 100 }); + expect(args.temperature).toBe(1); + }); + + it('sends temperature 0 when creativity is 0', async () => { + const args = await askAiGenerateTextArgs({ creativity: 0 }); + expect(args.temperature).toBe(0); + }); +}); + +describe('summarizeText temperature', () => { + it('sends no temperature', async () => { + await summarizeText.run(createMockActionContext({ + propsValue: { ...baseProps, text: 'long text', prompt: 'Summarize' }, + })); + expect(generateTextMock.mock.calls[0][0]).not.toHaveProperty('temperature'); + }); +}); diff --git a/packages/pieces/community/ai/tsconfig.lib.json b/packages/pieces/community/ai/tsconfig.lib.json index 86bdeddd7a64..26599beee9bf 100644 --- a/packages/pieces/community/ai/tsconfig.lib.json +++ b/packages/pieces/community/ai/tsconfig.lib.json @@ -16,5 +16,6 @@ "declarationMap": true, "types": ["node"] }, + "exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"], "include": ["src/**/*.ts"] } diff --git a/packages/pieces/community/ai/vitest.config.ts b/packages/pieces/community/ai/vitest.config.ts new file mode 100644 index 000000000000..d87fc4a69542 --- /dev/null +++ b/packages/pieces/community/ai/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from 'vitest/config' + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + }, +}) From 35d82a5cdda46a00d6c0c9329cdc82f8a721711b Mon Sep 17 00:00:00 2001 From: Kishan Parmar <135701940+kishanprmr@users.noreply.github.com> Date: Fri, 4 Sep 2026 18:32:26 +0530 Subject: [PATCH 6/6] fix(stripe): triggers no longer fail to enable with "Invalid array" (#15274) --- packages/pieces/community/stripe/package.json | 2 +- packages/pieces/community/stripe/src/lib/common/index.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/pieces/community/stripe/package.json b/packages/pieces/community/stripe/package.json index e779e5300f2e..5a6e08711134 100644 --- a/packages/pieces/community/stripe/package.json +++ b/packages/pieces/community/stripe/package.json @@ -1,6 +1,6 @@ { "name": "@activepieces/piece-stripe", - "version": "0.6.13", + "version": "0.6.14", "main": "./dist/src/index.js", "types": "./dist/src/index.d.ts", "scripts": { diff --git a/packages/pieces/community/stripe/src/lib/common/index.ts b/packages/pieces/community/stripe/src/lib/common/index.ts index c5ac3d6d6d43..384e79b33491 100644 --- a/packages/pieces/community/stripe/src/lib/common/index.ts +++ b/packages/pieces/community/stripe/src/lib/common/index.ts @@ -40,7 +40,7 @@ export const stripeCommon = { 'Content-Type': 'application/x-www-form-urlencoded', }, body: { - enabled_events: [eventName], + 'enabled_events[]': eventName, url: webhookUrl, }, authentication: {