Skip to content

bug: logicFunction paths missing workspace/datasource prefix when using S3 storage with LOGIC_FUNCTION_TYPE=LOCAL #19616

@usama-nrk

Description

@usama-nrk

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

  1. Self-host Twenty v1.21.0 with STORAGE_TYPE=s3 and LOGIC_FUNCTION_TYPE=LOCAL
  2. Create a workflow with a Code step
  3. 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

  1. Manually update core.logicFunction paths in the DB to use short-form paths (without prefix)
  2. Ensure built files exist in S3 at the short-form path the server constructs
  3. 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 1copyDependenciesInMemory 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 3sourceHandlerPath / 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

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

Status

✅ Done

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions