Environment
Twenty version: v1.21.0
STORAGE_TYPE: s3 (MinIO)
LOGIC_FUNCTION_TYPE: LOCAL
Deployment: self-hosted Docker Compose
Bug description
When creating a Code step in a workflow with S3 storage and LOGIC_FUNCTION_TYPE=LOCAL, the sourceHandlerPath and builtHandlerPath stored in core.logicFunction are missing the workspace and datasource prefix. This causes the server to construct an incorrect S3 path when trying to read or execute the function, resulting in a FILE_NOT_FOUND error on every Test and workflow execution.
Steps to reproduce
- Self-host Twenty v1.21.0 with STORAGE_TYPE=s3 and LOGIC_FUNCTION_TYPE=LOCAL
- Create a workflow with a Code step
- Save the code and click Test
Expected behaviour
Paths stored in DB should include the full prefix:
sourceHandlerPath: <workspaceId>/<datasourceId>/source/<functionId>/src/index.ts
builtHandlerPath: <workspaceId>/<datasourceId>/built-logic-function/<functionId>/src/index.mjs
Actual behaviour
Paths stored without prefix:
sourceHandlerPath: <functionId>/src/index.ts
builtHandlerPath: <functionId>/src/index.mjs
Error in logs
FileStorageException [Error]: File not found
at S3Driver.readFile (s3.driver.js:43)
at S3Driver.downloadFile (s3.driver.js:64)
at LogicFunctionResourceService.copyDependenciesInMemory (logic-function-resource.service.js:189)
at LocalDriver.createLayerIfNotExist (local.driver.js:50)
at LocalDriver.execute (local.driver.js:148)
at LogicFunctionExecutorService.execute
at LogicFunctionResolver.executeOneLogicFunction
Additional issue
copyDependenciesInMemory also looks for a dependencies/package.json in S3 that is never created during function setup, causing a second FILE_NOT_FOUND failure even when the built function file exists. The core.logicFunctionLayer table remains empty and no layer file is ever written to S3.
Workaround
- Manually update
core.logicFunction paths in the DB to use short-form paths (without prefix)
- Ensure built files exist in S3 at the short-form path the server constructs
- Manually create
dependencies/package.json in the S3 bucket with minimal content: {"name":"logic-function-layer","version":"1.0.0","dependencies":{}}
Related
Similar path tracking fix was shipped in v1.19.0 (#18230 — "logicFunction sourceHandlerPath and builtHandlerPath manifest updates are not saved") but the prefix omission and missing layer file issues persist in v1.21.0 with S3 storage.
Bug 1 — copyDependenciesInMemory crashes if no layer exists
File: packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.ts
The function unconditionally tries to download package.json from S3. If no layer was ever uploaded it throws FILE_NOT_FOUND. Fix — check existence first:
async copyDependenciesInMemory({
applicationUniversalIdentifier,
workspaceId,
inMemoryFolderPath,
}: {
applicationUniversalIdentifier: string;
workspaceId: string;
inMemoryFolderPath: string;
}): Promise<void> {
// ✅ FIX: check before downloading — layer may not exist
const packageJsonExists = await this.fileStorageService.checkFileExists({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'package.json',
});
if (!packageJsonExists) {
// Write a default empty package.json locally so execution can proceed
await fs.promises.writeFile(
path.join(inMemoryFolderPath, 'package.json'),
JSON.stringify({
name: 'logic-function-layer',
version: '1.0.0',
dependencies: {},
}),
);
return;
}
const yarnLockExists = await this.fileStorageService.checkFileExists({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'yarn.lock',
});
const promises: Promise<void>[] = [];
promises.push(
this.fileStorageService.downloadFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'package.json',
localPath: path.join(inMemoryFolderPath, 'package.json'),
}),
);
if (yarnLockExists) {
promises.push(
this.fileStorageService.downloadFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'yarn.lock',
localPath: path.join(inMemoryFolderPath, 'yarn.lock'),
}),
);
}
await Promise.all(promises);
}
Bug 2 — Layer never initialized in S3 on workspace creation
File: packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.ts
Add a method to initialize the default layer in S3 when a workspace is first set up, and call it from workspace initialization:
async initializeDefaultLayer({
workspaceId,
applicationUniversalIdentifier,
}: {
workspaceId: string;
applicationUniversalIdentifier: string;
}): Promise<void> {
const exists = await this.fileStorageService.checkFileExists({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'package.json',
});
// ✅ FIX: only initialize if not already present
if (!exists) {
const defaultPackageJson = JSON.stringify({
name: 'logic-function-layer',
version: '1.0.0',
dependencies: {},
});
await this.fileStorageService.writeFile({
workspaceId,
applicationUniversalIdentifier,
fileFolder: FileFolder.Dependencies,
resourcePath: 'package.json',
sourceFile: Buffer.from(defaultPackageJson),
mimeType: 'application/json',
settings: { isTemporaryFile: false, toDelete: false },
});
}
}
Call this from the workspace initialization service (wherever the workspace schema is seeded):
// In workspace initialization
await this.logicFunctionResourceService.initializeDefaultLayer({
workspaceId,
applicationUniversalIdentifier: datasourceId,
});
Bug 3 — sourceHandlerPath / builtHandlerPath double-prefix on activation
File: packages/twenty-server/src/engine/metadata-modules/logic-function/services/logic-function-from-source.service.ts
When duplicateOneWithSource copies a function for a new workflow version, it passes the already-prefixed sourceHandlerPath through copyResources, which prepends the prefix again. Fix — strip any existing prefix before passing to copyResources:
async duplicateOneWithSource(...): Promise<LogicFunction> {
const sourceHandlerPath = logicFunction.sourceHandlerPath;
const builtHandlerPath = logicFunction.builtHandlerPath;
// ✅ FIX: extract just the relative resource path (after fileFolder segment)
const sourceResourcePath = sourceHandlerPath.includes('/source/')
? sourceHandlerPath.split('/source/')[1]
: sourceHandlerPath;
const builtResourcePath = builtHandlerPath.includes('/built-logic-function/')
? builtHandlerPath.split('/built-logic-function/')[1]
: builtHandlerPath;
await this.logicFunctionResourceService.copyResources({
workspaceId,
applicationUniversalIdentifier,
fromSourceHandlerPath: sourceResourcePath,
fromBuiltHandlerPath: builtResourcePath,
toSourceHandlerPath: newSourceHandlerPath,
toBuiltHandlerPath: newBuiltHandlerPath,
});
}
| Bug |
File |
Fix |
package.json download crashes if layer missing |
logic-function-resource.service.ts` |
Check existence, fall back to in-memory default |
| Layer never written to S3 on workspace init |
logic-function-resource.service.ts |
Add initializeDefaultLayer called at workspace creation |
Double prefix on duplicateOneWithSource |
logic-function-from-source.service.ts |
Strip existing prefix before passing to copyResources |
Environment
Bug description
When creating a Code step in a workflow with S3 storage and LOGIC_FUNCTION_TYPE=LOCAL, the sourceHandlerPath and builtHandlerPath stored in core.logicFunction are missing the workspace and datasource prefix. This causes the server to construct an incorrect S3 path when trying to read or execute the function, resulting in a FILE_NOT_FOUND error on every Test and workflow execution.
Steps to reproduce
Expected behaviour
Paths stored in DB should include the full prefix:
Actual behaviour
Paths stored without prefix:
Error in logs
Additional issue
copyDependenciesInMemoryalso looks for adependencies/package.jsonin S3 that is never created during function setup, causing a second FILE_NOT_FOUND failure even when the built function file exists. Thecore.logicFunctionLayertable remains empty and no layer file is ever written to S3.Workaround
core.logicFunctionpaths in the DB to use short-form paths (without prefix)dependencies/package.jsonin the S3 bucket with minimal content:{"name":"logic-function-layer","version":"1.0.0","dependencies":{}}Related
Similar path tracking fix was shipped in v1.19.0 (#18230 — "logicFunction sourceHandlerPath and builtHandlerPath manifest updates are not saved") but the prefix omission and missing layer file issues persist in v1.21.0 with S3 storage.
Bug 1 —
copyDependenciesInMemorycrashes if no layer existsFile:
packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.tsThe function unconditionally tries to download
package.jsonfrom S3. If no layer was ever uploaded it throwsFILE_NOT_FOUND. Fix — check existence first:Bug 2 — Layer never initialized in S3 on workspace creation
File:
packages/twenty-server/src/engine/core-modules/logic-function/logic-function-resource/logic-function-resource.service.tsAdd a method to initialize the default layer in S3 when a workspace is first set up, and call it from workspace initialization:
Call this from the workspace initialization service (wherever the workspace schema is seeded):
Bug 3 —
sourceHandlerPath/builtHandlerPathdouble-prefix on activationFile:
packages/twenty-server/src/engine/metadata-modules/logic-function/services/logic-function-from-source.service.tsWhen
duplicateOneWithSourcecopies a function for a new workflow version, it passes the already-prefixedsourceHandlerPaththroughcopyResources, which prepends the prefix again. Fix — strip any existing prefix before passing tocopyResources:package.jsondownload crashes if layer missinglogic-function-resource.service.tsinitializeDefaultLayercalled at workspace creationduplicateOneWithSourcelogic-function-from-source.service.tscopyResources