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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions brain/knowledge/execution-runtime/workers.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 `<cache>/<version>/common` whose members are `pieces/<pieceName>-<pieceVersion>`, and the engine (`piece-loader.ts` → `resolveInstalledPieceEntry`) resolves a piece **only** at `pieces/<alias>/node_modules/<pieceName>`, 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/<name>` and leaves the member with no `node_modules` at all, so every run of that piece fails `PieceNotFoundError: Piece not found for package: <name>-<version>` 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 <cache>/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").
Expand Down
11 changes: 7 additions & 4 deletions bun.lock

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

4 changes: 2 additions & 2 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand All @@ -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
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "activepieces",
"version": "0.90.1",
"version": "0.90.2",
"packageManager": "bun@1.4.0",
"trustedDependencies": [
"sqlite3",
Expand Down
8 changes: 5 additions & 3 deletions packages/pieces/community/ai/package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down Expand Up @@ -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"
}
}
9 changes: 2 additions & 7 deletions packages/pieces/community/ai/src/lib/actions/text/ask-ai.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand All @@ -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.',
}),
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -54,7 +54,6 @@ export const summarizeText = createAction({
},
],
maxOutputTokens: context.propsValue.maxOutputTokens,
temperature: 1,
providerOptions: {
[provider]: {
...(provider === AIProviderName.OPENAI ? { reasoning_effort: 'minimal' } : {}),
Expand Down
Original file line number Diff line number Diff line change
@@ -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');
});
});
1 change: 1 addition & 0 deletions packages/pieces/community/ai/tsconfig.lib.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,5 +16,6 @@
"declarationMap": true,
"types": ["node"]
},
"exclude": ["src/**/*.spec.ts", "src/**/*.test.ts"],
"include": ["src/**/*.ts"]
}
8 changes: 8 additions & 0 deletions packages/pieces/community/ai/vitest.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { defineConfig } from 'vitest/config'

export default defineConfig({
test: {
globals: true,
environment: 'node',
},
})
19 changes: 18 additions & 1 deletion packages/pieces/community/clay/.eslintrc.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {} }
]
}
9 changes: 6 additions & 3 deletions packages/pieces/community/clay/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
12 changes: 7 additions & 5 deletions packages/pieces/community/clay/src/index.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -24,5 +26,5 @@ export const clay = createPiece({
}),
}),
],
triggers: [],
triggers: [rowReceivedTrigger],
});
Original file line number Diff line number Diff line change
@@ -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,
});
},
});
19 changes: 19 additions & 0 deletions packages/pieces/community/clay/src/lib/common/output-schemas.ts
Original file line number Diff line number Diff line change
@@ -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.',
},
],
};
Loading
Loading