# OpusCustomFields : Store custom metadata for virtually any item

**URL:** <https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698>\
**Category:** Buttons/Scripts\
**Tags:** commands, column, jscript, user-interface\
**Created:** [October 15, 2026, 2:23am UTC](https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698 "2026-10-15T02:23:09Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![errante](https://resource.dopus.com/user_avatar/resource.dopus.com/errante/32/61692_2.png) [@errante](https://resource.dopus.com/u/errante)\
**Post date:** [October 15, 2026, 2:23am UTC](https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698/1 "2026-10-15T02:23:09Z")

</div>

`OpusCustomFields` lets you define and set custom fields accessible in Directory Opus for virtually any file, with support for a variety of data types. It's the first read/write script add-in for Directory Opus.

> BEFORE CONTINUING, please keep the following in mind:

- This is a beta version and may contain bugs.
- The whole point of this is for you to test it in real-world use and report any bugs and suggestions.

### Features

- Almost any item can have custom fields, including FTP, MTP, items inside archives, etc.
- Blazing fast!
- You can create fields of any data type supported by Opus. It also supports multivalue data types, with special support for searching them (arrays and sets).
- Manage custom fields through an easy-to-use interface.
- Support for exporting/importing custom fields.
- Support for advanced actions: PULL, PUSH, FLUSH, etc.
- Seamless data management using an SQLite database, with commands for database maintenance.
- Support for directly searching the database, including an advanced mode using SQL SELECT syntax.

And more!

![config](https://github.com/user-attachments/assets/80c578bb-1cff-4ab3-b4d8-0bda09d73d4f)

### **Requirements**

- Directory Opus 13.25.4 or higher.
- Windows 10 1803 or higher (since the script uses Unix sockets for faster communication with the helper).

* * *

### **Installation**

[Download the installer from here](https://github.com/departure05/OpusCustomFields/releases/download/latest/OpusCustomFieldsInstaller.exe)

Then run the installer. This will put the helper in place and launch the standard script installer.

> This script uses a Go executable as a helper, which is why it requires a special installer.

* * *

### **How it works**

The script stores all its data in a database in its configuration folder. It communicates with the database transparently through a helper to manage the data with zero delay.

> Data is stored unencrypted, so avoid storing sensitive information in it.

To make it easier to manage and locate registered files, their paths are stored using the disk's volume name instead of its drive letter, where applicable. This is particularly useful for files on removable or USB drives, which tend to change drive letters.

> This script cannot track files. If a file is moved, renamed, or deleted, its data may be lost.

To work around this limitation, the script uses several methods:

1. For files on NTFS drives, it can back up data to an Alternate Data Stream (ADS). If the file is moved or renamed, the next time you try to retrieve its data, the script can recover it from this source and automatically update the database. (Optional; must be enabled in Options.)
2. For files on NTFS drives, it also stores each item's unique file identifier, allowing an item to be relocated after being moved, provided its identifier hasn't changed.
3. For all files, the script can retrieve data from a sidecar file stored alongside the original file, using the same filename plus the `.dopcf` extension. (Optional; must be enabled in Options.)

> Automatic recovery methods are only used when the file in question has no existing entry in the database.

> If ADS support is enabled, the ADS is automatically updated after every change, so manual editing is not recommended.

> Sidecar files are never generated automatically, but they can be generated manually using the `PULL` command.

> For the ADS workaround to work, you must set `Copy Alternate Data Streams` to `Copy to new file` under `Preferences / File Operations / Copying Files / Metadata (Copying Files)`.

* * *

### **Configuring fields**

The first time you install the script, you'll be asked if you want to start configuring fields.

To configure which fields are enabled, open the configuration dialog in one of the following ways:

- From the Script Management window, click the Configure button.
- Run the `OpusCustomFields CONFIG` command.

![config](https://github.com/user-attachments/assets/80c578bb-1cff-4ab3-b4d8-0bda09d73d4f)

Checked fields will be enabled as columns in Opus once you save your changes and close the dialog.

- The **Category** control lets you choose where the column appears. By default, columns are added under Script \> OpusCustomFields. You can change this to any category you prefer. This doesn't affect the column's behavior or output.
- `Blur in secure screenshot` lets you specify which columns will be blurred when Secure Screenshot is active.
- You can export or import columns from this window using the provided buttons.
- You can also set a short description for each field (max. 255 characters).

Press the `Add` button to create a new field by entering a name and choosing a type.

- The **Type** control sets the data type of the column's content. Allowed types are: String (text), number (int), double, star (rating), datetime, date, time, duration, size, percentage, array, and set.

> For compatibility reasons, field names must be lowercase, cannot contain spaces, commas, or slashes, and cannot begin with `_`.

> Once a field's name and type have been set, they cannot be changed.

When selecting an existing field, press the `Delete` button to delete it.

> Deleting a field is a destructive operation. All data associated with that field will be deleted from the database, so use this feature with extreme caution.

Once enabled, fields can be accessed and used as regular Opus columns anywhere columns are available.

> There are two internal columns that are always enabled: `_count` (Number of fields) and `_last_update` (Last Saved Date).

* * *

### **Options**

You can also access the Options dialog from the Config window to configure settings such as the script log level, backup-related options, and more.

(This dialog is also accessible from the Script Management window by clicking the About button.)

![options](https://github.com/user-attachments/assets/e7312de9-50d9-42c2-9619-332a9412e296)

- **Enable ADS support as a recovery resource** : When this option is enabled, if a file has no entry in the database, its ADS can be used as a recovery source. When data is set, the ADS is also updated automatically. Only applies to files on NTFS drives.

- **Enable sidecar files support as a recovery resource (read only)**: When this option is enabled, data can also be recovered from a sidecar file stored alongside the original file, using the same filename plus the `.dopcf` extension. This is for read-only recovery purposes; these files will not be created automatically.

* * *

### **Command arguments**

| Argument | Type | Value | Description |
| --- | --- | --- | --- |
| **CONFIG** | /S | | Displays the field configuration dialog. |
| **EXPORT** | /O/K | | Exports fields to a file. Also accepts an output path. |
| **FILES** | /M | | Files to act upon. If no files are provided, the script attempts to use the selected files in the source file display. Can be used with `SET`, `FLUSH`, `PULL`, `PUSH`, and `SETUI`. When used with `PURGEDB`, it allows the selected files to be deleted from the database. |
| **FLUSH** | /O/K | | A comma-separated list of field names whose values should be flushed from the database. If omitted, all active fields will be flushed. If ADS support is enabled, their values will be flushed from the ADS as well. |
| **PULL** | /O/K | | Pulls all values from active fields into a `.dopcf` file in JSON format. You can also specify an output filename. |
| **PUSH** | /K | | Pushes values for active fields from a file. The file must contain a JSON object in the `field:value` format. |
| **SET** | /M | | Sets values for active fields. You can pass values for multiple fields, just like with `SET META`. |
| **FIND** | /O/R | | Searches the database. You can pass a raw query or enter one in the FIND dialog. |
| **IN** | /K | | Specifies a path for `FIND` to search within. Must be a valid filesystem-only path. |
| **ADVANCED** | /S | | Enables special SQL syntax for the `FIND` command. |
| **PURGEDB** | /O | | Performs database cleanup, if applicable. |
| | | _path/wildcard_ | If a text value is specified, it can be the full name of a specific file or a path containing `*` and `?` wildcards. If omitted, all entries for non-existing items are removed (`KEEPMISSINGDRIVES` can change this behavior). |
| **KEEPMISSINGDRIVES** | /S | | Used with `PURGEDB` without a specified value, this keeps entries located on currently unavailable drives, such as external disks. |
| **VACUUM** | /S | | Compacts the database. Use together with `PURGEDB`. |
| **SETUI** | /O/K | | Sets values through a user interface. |
| | | `autosave` | Automatically saves changes whenever the file selection changes. |
| | | `text` | Starts with all field controls set to accept text. Useful for referencing properties in non-text fields. |
| **NOONTOP** | /S | | Prevents the dialog from opening on top. Can be used with `CONFIG` and `SETUI`. |
| **NOEDIT** | /S | | When used with `SET`, disables the add/remove feature for array-based fields. |

#### Setting values (via `SET`, `PUSH`, or `SETUI`)

Valid values vary depending on the data type of each field.

- **For arrays/sets:**

- **For duration values, you can pass:**

- **For size values, you can pass:**

- **For date-related values, you can pass:**

To remove a field's value for a file, set it to `""` (empty), regardless of its data type.

You can also reference properties of the files themselves. These can be of any type (script, evaluator, etc.). To reference a property, enclose its internal name in `$_..._$`. For example, `$_created_$` retrieves the creation date.

> Avoid referencing properties known to take a long time to calculate, such as checksums for large files, as this will stall processing for that file and all subsequent files.

- With `SET`, you can pass values using the following format:

- `PUSH` accepts a file containing valid JSON in the same `field:value` format. The same value types accepted by `SET` are supported.

- `SETUI` provides an interface for setting values more dynamically. Since it retrieves each field's current value when opened, it's ideal for editing or adding to existing values.

![setui](https://github.com/user-attachments/assets/fddc6039-83cd-41f6-aa38-32df946a1848)

- Use the `Use item's properties` checkbox to switch the controls to text input. This is useful when referencing properties in fields that don't accept text directly.
- After each file selection change, you must save or discard your changes using the Save button or Ctrl + S. You can enable automatic saving from the Save dropdown or by using `SETUI=autosave`.

* * *

### FIND syntax

- **SIMPLE mode** is a small expression language parsed in situ and converted into parameterized SQL. The client's text never reaches SQLite.

- **ADVANCED mode** wraps the query as follows:

```sql
SELECT _file FROM entries WHERE ( <QUERY> ) [AND _file LIKE <IN>]

```

Here, `<QUERY>` is your actual query, and `<IN>` is the value passed through the `IN` argument, if used.

- This mode does NOT try to understand SQL. SQLite parses the expression when it runs and reports its own syntax errors. The script only lexes the text to handle strings, quoted names, and comments correctly, rejecting an empty expression and a bare `;` (which would either break out of the `WHERE` clause into a second statement or be a pointless no-op; either way, a single expression should never need one). Otherwise, any expression-level SQL is allowed, including subqueries, `CASE`, `json1` functions, and so on.

- `FIND` has its own dialog where you can enter queries and switch between modes.

![find](https://github.com/user-attachments/assets/490d04f4-6de8-4dd9-b486-fe97ba1eb8ef)

- When searching, you can access the internal columns `_count` (Number of fields) and `_last_update` (Last Saved Date).
- You can access a list of your active fields using the button or F6.
- You can also access your recent search history using the button or F4.

* * *

### **CLEANING THE DATABASE**

![purgedb](https://github.com/user-attachments/assets/f2846359-9248-4d45-8882-4bf2ed0c16b6)

You can maintain your database using the `PURGEDB` argument.

When used together with a list of `FILES`, this deletes those files from the database, along with their ADS entries, if any.

If you pass a wildcard to `PURGEDB`, or omit `FILES`, the script checks for orphaned entries. You can use `KEEPMISSINGDRIVES` to decide whether to keep entries from removable drives that are not currently available.

A list of files will then be displayed, allowing you to choose which entries to keep and which to delete. After you press OK, the selected entries will be deleted from the database.

> Due to limitations in the Opus scripting API, all files on MTP devices will be listed, even if they exist.

You can also include `VACUUM` to compact the database.

* * *

### **NOTES**

- TEXT fields have a maximum length of 1,000 characters. Any excess will be automatically truncated.
- When the database reaches 1 GB in size, certain features, such as `FIND`, will become unavailable.
- Fields are NOT indexed, to maintain decent performance when setting values or rearranging the database. Even so, searches should be fast enough.
- This script is NOT DESIGNED for indexing purposes, such as indexing file properties or content.
- Files from remote locations stored in the database may slow down some operations, such as `FIND` or `PURGEDB`.
- ADS reading/writing and sidecar file lookup are effectively supported only for files that report a valid drive and an NTFS filesystem through the Opus scripting API.
- Some operations cannot run simultaneously.
- At least for now, FTP support is disabled.

* * *

### **Acknowledgments**

- To the Opus team, as usual.
- To all the testers!

The Go helper uses the following third-party libraries:

- `github.com/goccy/go-json`
- `modernc.org/sqlite`
- `github.com/dustin/go-humanize`
- `github.com/google/uuid`
- `github.com/mattn/go-isatty`
- `github.com/ncruces/go-strftime`
- `github.com/remyoudompheng/bigfft`
- `golang.org/x/sys`
- `modernc.org/libc`
- `modernc.org/mathutil`
- `modernc.org/memory`

* * *

### **Changelog**

**v0.9.7b (Oct 15, 2026):**

- Initial public release.

---

<div class="post-metadata">

**Author:** ![Bennjamin](https://resource.dopus.com/user_avatar/resource.dopus.com/bennjamin/32/73048_2.png) [@Bennjamin](https://resource.dopus.com/u/Bennjamin)\
**Post date:** [October 15, 2026, 5:46pm UTC](https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698/2 "2026-10-15T17:46:00Z")

</div>

I'm an enthusiast user - be it slowly and carefully - because this is the most complex Opus extension I've seen so far. But it's very powerful. Yet one thing that must be emphasized, I think, is with regards to the following remark:

> [@errante](#):
>
> For the ADS workaround to work, you must set `Copy Alternate Data Streams` to `Copy to new file` under `Preferences / File Operations / Copying Files / Metadata (Copying Files)`.

For anyone who already saved data in NTFS/ADS via this mechanism, I suppose they can lose their previously saved comments when activating this backup mechanism (I didn't test this though). Correct me if I'm wrong, but if I'm right, then a warning should probably be added there, in all caps and with a red light sign, just in case.

Ps. This potential "calamity" could perhaps be prevented in the script itself? E.g. by implementing a small, additional step **on first first access** of a file's ADS comment: first appending any already existing ADS comment to the sql data (separated by a pipeline char, for instance) before doing the initial backup operation, so no "old" comments get lost. (Plus, giving a warning when exceeding the limit of 1000 chars in this case).

---

<div class="post-metadata">

**Author:** ![errante](https://resource.dopus.com/user_avatar/resource.dopus.com/errante/32/61692_2.png) [@errante](https://resource.dopus.com/u/errante)\
**Post date:** [October 15, 2026, 6:13pm UTC](https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698/3 "2026-10-15T18:13:49Z")

</div>

> [@Bennjamin](#):
>
> suppose they can lose their previously saved comments when activating this backup mechanism

Check out here ADS definition:

> **[ADS — Alternate Data Streams (Beginner friendly PoC)](https://dev.to/itsmegsg/ads-alternate-data-streams-beginner-friendly-poc-27m4)**
>
> One-line summary: NTFS Alternate Data Streams (ADS) let a file carry named hidden streams of data...

You can have more than one btw, so I'm not sure why your comments will be in danger (unless you also saved them in an ADS named \_OpusCustomFields). The script will not touch other streams.

---

<div class="post-metadata">

**Author:** ![Bennjamin](https://resource.dopus.com/user_avatar/resource.dopus.com/bennjamin/32/73048_2.png) [@Bennjamin](https://resource.dopus.com/u/Bennjamin)\
**Post date:** [October 15, 2026, 7:41pm UTC](https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698/4 "2026-10-15T19:41:50Z")

</div>

> [@errante](#):
>
> Preferences / File Operations / Copying Files / Metadata

Ah. So ... I was probably confusing

Preferences / File Operations / Copying Files / Metadata

with

Preferences / File Operations / Metadata

I'm still not sure, but I will check this. I use the Opus mechanism for storing comments in metadata.other.usercomment (a.k.a. userdesc, I think) and I thought you were using the same mechanism for your backup solution.

---

<div class="post-metadata">

**Author:** ![MG250](https://resource.dopus.com/letter_avatar_proxy/v4/letter/m/b487fb/32.png) [@MG250](https://resource.dopus.com/u/MG250)\
**Post date:** [October 20, 2026, 8:16pm UTC](https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698/5 "2026-10-20T20:16:22Z")

</div>

Here's why I find this script very useful:

- Metadata is stored externally. No changes are made to the files themselves, which is useful when you need to preserve their hash values.
- You can keep filenames short. Instead of cramming everything into a filename, you can move additional descriptive information into custom fields. This is especially useful for files that require lengthy descriptions, given the limits on filename and path lengths and the general best practice of keeping filenames concise.
- You can link files that share a common topic. For example, you can use an array/set field to tag files with `software` and then find them with a command such as `OpusCustomFields FIND tags CONTAINS "software"`. All other field types are searchable, too.
- Fields aren't tied to specific file types. Unlike files built-in metadata fields, custom fields can be used across different types of files.
- You can define as many custom fields as you need. You're not limited to a single field, and you can store text or other data types.

Of course, these are just a few of the script's many features. Many thanks again for creating and sharing it.

---

<div class="post-metadata">

**Author:** ![Bennjamin](https://resource.dopus.com/user_avatar/resource.dopus.com/bennjamin/32/73048_2.png) [@Bennjamin](https://resource.dopus.com/u/Bennjamin)\
**Post date:** [October 22, 2026, 3:56pm UTC](https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698/6 "2026-10-22T15:56:16Z")

</div>

I agree. I'm still not sure how exactly to use it, but with a little programming (outside errante's dialog, but using his scripted commands) I could put the _ **metadata.other.usercomment** _ data for thousands of files at once (in flatview mode) in the sqlite database, in a "comment" field. It's an instant backup, but I can now also search blazingly fast through that content. It is not necessarily faster than searching through thousands of files, if what you are searching for sits in the filename - but if you need to search through metadata, that's a whole different story, and having that data in sqlite makes all the difference.

Unfortunately, the sql logic does not allow a simple "get" (e.g. to edit the data of 1 file in a DOpus dialog), but this can quite easily be done via a little round-trip, using a temporary text file.

I'm still in the process of considering how to proceed from here. Data in sql is far better (faster) approachable than file metadata, but _ **only** _ using sql would create a huge dependency on sqlite. So it seems like storing it both ways is the better option. Yet dual storage is overhead - and the worries about that data not being updated consistently in both places. But this seems like a price one can pay for greatly improving searching through metadata. I remember Bill Gates once said he wanted sql integrated in NTFS, and when he left Microsoft he regretted not having managed to do it. I can now understand why.

---

<div class="post-metadata">

**Author:** ![errante](https://resource.dopus.com/user_avatar/resource.dopus.com/errante/32/61692_2.png) [@errante](https://resource.dopus.com/u/errante)\
**Post date:** [October 24, 2026, 12:40am UTC](https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698/7 "2026-10-24T00:40:34Z")

</div>

> [@Bennjamin](#):
>
> I could put the _ **metadata.other.usercomment** _ data for thousands of files at once

I'm not sure what you're talking about, but FWIW, you can copy the `userdesc` value, with a one liner. 🙂  
`OpusCustomFields SET "mydesc:$_userdesc_$"`

In fact you can copy almost all properties that way (including checksums but that would also depend of your Opus settings).

Also note that unless you've found a way to explicitly write to that place, the description could be stored in other properties, depending on the filetype.

---

<div class="post-metadata">

**Author:** ![Bennjamin](https://resource.dopus.com/user_avatar/resource.dopus.com/bennjamin/32/73048_2.png) [@Bennjamin](https://resource.dopus.com/u/Bennjamin)\
**Post date:** [October 26, 2026, 10:39am UTC](https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698/8 "2026-10-26T10:39:34Z")

</div>

I don't know, @errante - you have the programming expertise. I guess "usercomment" is one of those older handles, which I should in my own code probably replace by "userdesc" (there seems to be [some confusion about this](https://resource.dopus.com/t/playground-using-the-evaluator-in-the-filter-bar/49510/27) \<= even @lxp found it "odd"). The shortcut you provide is nice, but it works either way.

PS. (somewhat off-topic) In my own scripts I only make an explicit distinction for pdf's: in that case I try to store data explicitly under _ **doc.subject** _ because I noticed (long ago) that my pdf's sometimes have no _description_ field (DO calls it "_Comment_" but it is the "userdesc" handler). And in those cases I could not always create that pdf field either - but the "_subject_" field always seemed to be available, so I went for that one. Whether or not this is still good practice I have no idea, but it solved my dilemma.

I wished Opus shared the part of their code (or some pseudocode, or scheme) which reveals how exactly their decision tree works for this magic userdesc field, across the jungle of file types. Some more insight wouldn't hurt.

---

<div class="post-metadata">

**Author:** ![errante](https://resource.dopus.com/user_avatar/resource.dopus.com/errante/32/61692_2.png) [@errante](https://resource.dopus.com/u/errante)\
**Post date:** [October 27, 2026, 3:36am UTC](https://resource.dopus.com/t/opuscustomfields-store-custom-metadata-for-virtually-any-item/60698/9 "2026-10-27T03:36:14Z")

</div>

> [@Bennjamin](#):
>
> how exactly their decision tree works for this magic userdesc field

It's not overly complicated IMHO. But you can start a new thread about your questions on that topic, and hopefully you'll get the answers you need.

Staying on THIS topic, in THIS context, what matters is how you end up writing the info. If you use something like `SetAttr META "comment:my comment"` or `SetAttr DESCRIPTION`, you can use `$_userdesc_$`. If you use something like `subject:my comment`, use `$_subject_$`, or `$_imagedesc_$` if you use `imagedesc:my comment`. You could easily handle this with Evaluator, too, based on the file extension, I guess, or whatever rule you use to decide where to write the data.

Assuming you don't write to more than one property for a file, you could combine them into one (since blank values will be set to `""`):

`OpusCustomFields SET "mydesc:$_userdesc_$$_subject_$"`
