The @cap-js/attachments package is a CDS plugin that provides out-of-the box asset storage and handling by using an aspect called Attachments. It also provides a CAP-level, easy-to-use integration of the SAP Object Store.
For a quick local development setup with in-memory storage:
sh
npm add @cap-js/attachments
The attachments plugin needs to be referenced in the package.json of the consuming CAP NodeJS application:
cds
"dependencies": {
"@cap-js/attachments": "<latest-version>",
// (...)
}
In addition, different profiles can be found in package.json as well, such as:
json
"cds": {
"requires": {
// (...)
"[hybrid]": {
"attachments": {
"kind": "standard"
// (...)
}
}
}
}
```cds using { Attachments } from '@cap-js/attachments';
entity Incidents { // (...) attachments: Composition of many Attachments; } ```
In this guide, we use the Incidents Management reference sample app as the base application to provide a demonstration how to use this plugin. A miniature version of this app can be found within the tests directory for local testing.
For productive use, a valid object store binding is required, see Object Stores and Storage Targets.
With the steps above, we have successfully set up asset handling for our reference application. To test the application locally, use the following steps.
[!NOTE] For local testing, the attachment objects are stored in a local database.
Start the server:
Default scenario (In memory database):
sh
cds watch
Navigate to the object page of the incident Solar panel broken:
Go to object page for incident Solar panel broken
The Attachments type has generated an out-of-the-box Attachments table (see 1) at the bottom of the Object page:

Upload a file by going into Edit mode and either using the Upload button on the Attachments table or by drag/drop. Then click the Save button to have that file stored in the dedicated resource (database, S3 bucket, etc.). We demonstrate this by uploading the PDF file from tests/integration/content/sample.pdf:

Delete a file by going into Edit mode, selecting the file, and pressing the Delete button above the Attachments table. Clicking the Save button will then delete that file from the resource (database, S3 bucket, etc.).

To use the aspect Attachments on an existing entity, the corresponding entity needs to either include attachments as an element in the model definition or be extended in a CDS file in the srv module. In the quick start, the former was done, adding an element to the model definition:
using { Attachments } from '@cap-js/attachments';
entity Incidents {
// ...
attachments: Composition of many Attachments;
}
The entity Incidents can also be extended in the srv module, as seen in the following example:
using { Attachments } from '@cap-js/attachments';
extend my.Incidents with {
attachments: Composition of many Attachments;
}
service ProcessorService {
entity Incidents as projection on my.Incidents
}
Both methods directly add the respective UI Facet. To use the plugin with an SAP Fiori elements UI, be sure that draft is enabled for the entity using @odata.draft.enabled. For example:
annotate service.Incidents with @odata.draft.enabled;
If you are not using SAP Fiori elements, draft enablement is not required. For more information, see non-draft upload for an alternative upload flow.
It is also possible to allow for only 1 attachment by defining the attachments field as a single attachment. This displays a different UI that more clearly shows the single attachment rather than in a list.
using { Attachment } from '@cap-js/attachments';
entity Incidents {
...
attachment: Attachment;
}

When testing locally, the plugin operates without a dedicated storage target, storing attachments directly in the underlying database. In a hybrid setup, a dedicated storage target is preferred. You can bind it by using the cds bind command as described in the CAP documentation for hybrid testing.
Meanwhile, with a dedicated storage target the attachment is not stored in the underlying database; instead, it is saved on the specified storage target and only a reference to the file including metadata is kept in the database, as defined in the CDS model.
For using an Object Store in BTP, you must already have an SAP Object Store service instance on the appropriate landscape created. To bind it in a hybrid setup, follow this setup:
cf login -a <CF-API> -o <ORG-NAME> -s <SPACE-NAME> --sso
cds bind <HybridObjectStoreName> --to <RemoteObjectStoreName>
Where HybridObjectStoreName can be any name given by the user here and RemoteObjectStoreName is the name of your object store instance in SAP BTP.
cds watch --profile hybrid
See Object Stores for further information on SAP Object Store.
The BTP malware scanning service is used in the AttachmentService to scan attachments for vulnerabilities.
For using SAP Malware Scanning Service, you must already have a service instance which you can access. To bind it, run the following command:
cds bind <HybridMalwareScannerName> --to <RemoteMalwareScannerName>
By default, malware scanning is enabled for all profiles if a storage provider has been specified. You can configure malware scanning by setting:
{
"cds": {
// (...)
"attachments": {
"scan": true
}
}
}
If there is no malware scanner available and the scanner is not disabled, then the upload will fail.
Scan status codes:
Unscanned: Attachment is still unscanned.Scanning: Immediately after upload, the attachment is marked as Scanning. Depending on processing speed, it may already appear as Clean when the page is reloaded.Clean: Only attachments with the status Clean are accessible.Infected: The attachment is infected.Failed: Scanning failed.[!Note] The malware scanner supports mTLS authentication which requires an annual renewal of the certificate. Previously, basic authentication was used which has now been deprecated.
[!Note] If the malware scanner reports a file size larger than the limit specified via @Validation.Maximum it removes the file and sets the status of the attachment metadata to failed.
The SAP Malware Scanning Service enforces a rate limit of 30 concurrent requests per subaccount. When this limit is exceeded, the service responds with HTTP 429 Too Many Requests. By default, the plugin automatically retries scan requests that receive a 429 response using exponential backoff with jitter.
You can configure the retry behavior in package.json or .cdsrc.json:
{
"cds": {
"requires": {
"malwareScanner": {
"retry": {
"maxAttempts": 5,
"initialDelay": 1000,
"maxDelay": 30000
}
}
}
}
}
| Option | Default | Description |
|---|---|---|
retry.maxAttempts |
5 |
Total number of attempts including the initial request |
retry.initialDelay |
1000 |
Base delay in milliseconds before the first retry |
retry.maxDelay |
30000 |
Maximum delay in milliseconds between retries |
When a 429 response includes a Retry-After header, the plugin respects that value (capped at maxDelay). Only 429 responses trigger retries — other errors fail immediately.
To disable retry and restore the previous behavior (immediate failure on 429), set retry to false.
To reduce pressure on the shared rate limit, the plugin limits how many scan requests run concurrently within a single process. Excess scans are queued and processed as slots become available.
{
"cds": {
"requires": {
"malwareScanner": {
"maxConcurrentScans": 10
}
}
}
}
| Option | Default | Description |
|---|---|---|
maxConcurrentScans |
30 |
Maximum number of concurrent scan requests per process. Set to 0 to disable (unbounded parallelism). |
A scan that is retrying due to a 429 response holds its concurrency slot during the backoff wait, preventing retry storms from competing with new scans.
According to the recommendation of the Malware Scanning Service, attachments should be rescanned automatically if the last scan is older than 3 days. This behavior can be configured in the attachments settings by specifying the scanExpiryMs property:
{
"cds": {
"requires": {
"attachments": {
"scanExpiryMs": 259200000
}
}
}
}
By default, scanExpiryMs is set to 259200000 milliseconds (3 days). Downloading an attachment is not permitted unless its status is Clean.
The attachment service emits the following three events:
When `@cap-js
$ claude mcp add attachments \
-- python -m otcore.mcp_server <graph>