Skip to main content

The unidirectional file synchronization interface allows you to implement 1-way sync in your integration to import files from an external service to Botpress.

Filesystem abstraction

The file synchronization interface provides a filesystem-like abstraction that works with any kind of data source. The external service doesn’t need to provide an actual filesystem - your integration just needs to represent the external data as files and folders. For example:
  • If you are building a website crawler, individual pages could be folders and HTML contents and assets like images or stylesheets could be files.
  • For a note-taking platform, notebooks could be folders with individual notes being files.
  • For an email provider, mailboxes or labels could be folders and individual emails could be files.
This abstraction allows the interface to work consistently regardless of what type of data is being synchronized from your external service.

Terminology

Throughout this document, we will use the following terms:

External service requirements

The providing file synchronization functionality must support the following:
  • An API that allows listing all files and folders in a folder.
    • Must support pagination. This means that the API should return a limited number of items at a time, along with a token that can be used to retrieve the next set of items.
  • An API that allows downloading files.
The may also support the following in order to provide :
  • Webhooks that can notify your of the following events:
    • A was created.
    • A was updated.
    • A was deleted.
    • A was deleted.

Updating your package.json file

Finding the current interface version

The current version of the files-readonly interface is: You will need this version number for the next steps.

Adding the interface as a dependency

Once you have the version, you can add it as a dependency to your :
1

Open the package.json file

Open your ‘s package.json file.
2

Add the dependencies section

If there is no bpDependencies section in your ‘s package.json file, create one:
package.json
{
  "bpDependencies": {}
}
3

Add the interface as a dependency

In the bpDependencies section, add the as a dependency. For example, for version 0.2.0, you would add the following:
package.json
{
  "bpDependencies": {
    "files-readonly": "interface:files-readonly@0.2.0"
  }
}
It’s very important to follow this syntax:
"<interface-name>": "interface:<interface-name>@<version>".
4

Save the package.json file

Save the package.json file.
5

Install the interface

Now that you have added the as a dependency, you can run the bp add command to install it. This command will:
  • Download the interface from Botpress.
  • Install it in a directory named bp_modules in your ‘s root directory.

Adding a helper build script

To keep your up to date, we recommend adding a helper build script to your package.json file:
1

Open the package.json file

Open your ‘s package.json file.
2

Add the build script

In the scripts section, add the following script:
package.json
{
  "scripts": {
    "build": "bp add -y && bp build"
  }
}
If the build script already exists in your package.json file, please replace it.
3

Save the package.json file

Save the package.json file.
Now, whenever you run npm run build, it will automatically install the and build your .

Editing your integration definition file

Adding the interface to your integration definition file

Now that the is installed, you must add it your integration definition file in order to implement it.
1

Open the integration.definition.ts file

Open your ‘s integration.definition.ts file.
2

Import the interface

At the top of the file, import the :
integration.definition.ts
import filesReadonly from './bp_modules/files-readonly'
3

Extend your definition

Use the .extend() function at the end of your new IntegrationDefinition() statement:
integration.definition.ts
export default new sdk.IntegrationDefinition({
  ...
})
  .extend(files-readonly, () => ({
    entities: {},
  }))
The exact syntax of .extend() will be explained in the next section.

Configuring the interface

The .extend() function takes two arguments:
  • The first argument is a reference to the interface you want to implement. In this case, it’s filesReadonly.
  • The second argument is a configuration object. Using this object, you can override interface defaults with custom names, titles, and descriptions.
Whilst renaming actions, events and channels is optional, it’s highly recommended to rename these to match the terminology of the . This will help you avoid confusion and make your easier to understand.

Renaming actions

The defines two actions that are used to interact with the :
  • listItemsInFolder - Used by the to request a list of all files and folders in a folder.
  • transferFileToBotpress - Used by the to request that a file be downloaded from the and uploaded to Botpress.
If you want to rename these actions, you can do so in the configuration object. For example, if you want to rename listItemsInFolder to crawlFolder, you can do it like this:
integration.definition.ts
.extend(filesReadonly, () => ({
  actions: {
    listItemsInFolder: {
      name: 'crawlFolder',
    },
  },
}))
For example, if you’re using a note-taking platform such as Microsoft OneNote, you might rename listItemsInFolder to listNotebooksAndPages and transferFileToBotpress to downloadPage. This way, the action names reflect the specific context of the note-taking platform, making your clearer and easier to understand.

Renaming events

The interface defines these events to notify the plugin of changes in the :
  • fileCreated - Emitted by your to notify the that a new has been created in the .
  • fileUpdated - Emitted by your to notify the that a has been updated in the .
  • fileDeleted - Emitted by your to notify the that a has been deleted in the .
  • folderDeletedRecursive - Emitted by your to notify the that a and all of its contents have been deleted in the .
If the emits several filesystem changes at once, it’s also possible for your integration to emit a aggregateFileChanges event, which contains all the changes in a single event.
If you want to rename these events, you can do so in the configuration object. For example, if you want to rename fileCreated to pageCreated, you can do it like this:
integration.definition.ts
.extend(filesReadonly, () => ({
  events: {
    fileCreated: {
      name: 'pageCreated',
    },
  },
}))

Implementing the interface

Implementing the actions

Implementing listItemsInFolder

The listItemsInFolder action is used by the to request a list of all files and folders in a folder.
If you opted to rename the action to something else to listItemsInFolder in the “Configuring the interface” section, please use the new name instead of listItemsInFolder.
Please refer to the expected input and output schemas for the action: interface.definition.ts line 52. This action should implement the following logic:
1

Get the folder ID

Get the folder identifier from input.folderId. When this value is undefined, it means the is requesting a list of all items in the root directory of the . For root directory requests, please refer to the documentation of the to determine the correct root identifier - this is typically an empty string, a slash character (/), or a special value defined by the service.
2

Get the list of items

Use the ‘s API to get the list of items in the folder. If the supports filtering by item type (file or folder), by maximum file size, or by modification date, please use these filters to limit the number of items returned. This will help reduce the amount of data transferred and improve performance.
If a pagination token is provided (input.nextToken), use it to get the next page of items. The should return a new pagination token in the response, which you should return with the action’s response.
Don’t list items recursively. The is responsible for handling recursion. Your should only return the items in the specified folder.
3

Map each items to the expected schema

Map each item to the expected schema. The expects the following schemas:
4

Yield control back to the plugin

Yield control back to the by returning the list of items. The will then handle the rest of the synchronization process.
return {
  items: [...mappedFolders, ...mappedFiles],
  meta: { nextToken: hasMoreItems ? nextToken : undefined },
}
If the indicates it has more items, return the pagination token in the nextToken field. The will use this token to request the next page of items. Otherwise, return undefined.
As reference, here’s how this logic is implemented in the Dropbox integration:
src/index.ts
export default new bp.Integration({
  actions: {
    async listItemsInFolder({ ctx, input, client }) {
      // Extract input parameters:
      const { folderId, filters, nextToken: prevToken } = input

      // Get the folder ID:
      //    (Dropbox expects an empty string for the root directory)
      const parentId = folderId ?? ''

      // Get the list of items in the folder
      const { items, nextToken, hasMore } = await dropboxClient.listItemsInFolder({
        path: parentId,
        recursive: false, // <= The integration shouldn't list recursively
        nextToken: prevToken, // <= Use the pagination token if provided
      })

      const mappedItems = items
        .filter((item) => item.itemType !== 'deleted')
        // Call utility functions to handle the mapping:
        .map((item) =>
          item.itemType === 'file' ?
            filesReadonlyMapping.mapFile(item) :
            filesReadonlyMapping.mapFolder(item)
        )

      // Yield control back to the plugin and return the items:
      return {
        items: mappedItems,
        meta: { nextToken: hasMore ? nextToken : undefined },
      }
    },
  },
})

Implementing transferFileToBotpress

The transferFileToBotpress action is used by the to request that a file be downloaded from the and uploaded to Botpress.
If you opted to rename the action to something else to transferFileToBotpress in the “Configuring the interface” section, please use the new name instead of transferFileToBotpress.
Please refer to the expected input and output schemas for the action: interface.definition.ts line 88. This action should implement the following logic:
1

Get the file ID

Get the file identifier from input.file.id. This is the identifier of the file to be downloaded from the .
2

Download the file from the external service

Use the ‘s API to download the file’s content.
3

Upload the file to Botpress

Upload the file to Botpress using the client.uploadFile method. This method expects both the file’s content and a file key, which is provided by the as input.fileKey.
4

Yield control back to the plugin

Yield control back to the by returning the ID of the file that was uploaded to Botpress.
As reference, here’s how this logic is implemented in the Dropbox integration:
src/index.ts
export default new bp.Integration({
  actions: {
    async transferFileToBotpress({ ctx, input, client }) {
      // Extract input parameters:
      const { file: fileToTransfer, fileKey } = input

      // Use Dropbox's SDK to download the file:
      const fileBuffer = await dropboxClient.getFileContents({ path: fileToTransfer.id })

      // Upload the file to Botpress:
      const { file: uploadedFile } = await client.uploadFile({
        key: fileKey,
        content: fileBuffer,
      })

      // Yield control back to the plugin:
      return {
        botpressFileId: uploadedFile.id
      }
    },
  },
})

Implementing real-time sync

The can be configured to use real-time synchronization. This means that changes in the are immediately reflected in Botpress. To enable this functionality, the must support webhooks that can notify your of changes in the filesystem.

Implementing fileCreated

1

Add a webhook handler

In your , add a webhook handler that can receive file change notifications from the .
2

Map the file to the expected schema

In your handler, map the file to the expected schema. The expects the following schema:
3

Emit the event

Emit the fileCreated event with the mapped file as the payload. The will then handle the rest of the synchronization process.
await client.createEvent({
  name: 'fileCreated',
  payload: { file: mappedFile },
})

Implementing fileUpdated

The logic is identical to the fileCreated event, but you should emit the fileUpdated event instead.

Implementing fileDeleted

The logic is identical to the fileCreated event, but you should emit the fileDeleted event instead.

Implementing folderDeletedRecursive

1

Add a webhook handler

In your , add a webhook handler that can receive file change notifications from the .
2

Map the folder to the expected schema

In your handler, map the folder to the expected schema. The expects the following schema:
3

Emit the event

Emit the folderDeletedRecursive event with the mapped folder as the payload. The will then handle the rest of the synchronization process.
await client.createEvent({
  name: 'folderDeletedRecursive',
  payload: { folder: mappedFolder },
})

Implementing aggregateFileChanges

The logic is identical to the fileCreated, fileUpdated, fileDeleted, or folderDeletedRecursive events, but you should emit the aggregateFileChanges event instead:
await client.createEvent({
  name: 'aggregateFileChanges',
  payload: {
    modifiedItems: {
      created: [...mappedCreatedFiles],
      updated: [...mappedUpdatedFiles],
      deleted: [...mappedDeletedFilesOrFolders],
    },
  },
})
If your needs to emit more than one filesystem change event, you should combine them into a single aggregateFileChanges event. This is more efficient and faster to process for the .
Last modified on March 9, 2026