Workspaces and Settings

Every non-trivial application requires some level of configuration to customize its run-time behavior and help it locate data resources required for it to perform its functions. An iTwin.js Workspace comprises the Settings that supply this configuration and the WorkspaceContainers that provide those resources. Settings inside of Workspace.settings provide values for individual SettingNames, some of which point to one or more WorkspaceDbs that provide binary resources for particular purposes. The anatomy of a Workspace is illustrated below:

graph LR
  subgraph Workspace
    S["Settings
(priority-ordered stack)"] subgraph Settings Containers SC1["SettingsDb
iTwin scope"] SC2["SettingsDb
iModel scope"] end subgraph Workspace Containers WC1["WorkspaceDb
fonts, textures"] WC2["WorkspaceDb
templates"] end end SC1 -->|"loads as
SettingsDictionary"| S SC2 -->|"loads as
SettingsDictionary"| S S -->|"settings point to"| WC1 S -->|"settings point to"| WC2

Settings are stored in SettingsDb containers (cloud-hosted, versioned, and discoverable by containerType: "settings"). Binary resources like fonts, textures, and images are stored in WorkspaceDb containers. At runtime, each SettingsDb becomes one SettingsDictionary in the Settings priority stack, and those settings tell the application where to find the WorkspaceDbs it needs.

To explore Workspace concepts, let's take the example of an imaginary application called "LandscapePro" that allows users to decorate an iModel by adding landscaping features like trees, shrubs, flower beds, and patio furniture.

Settings

Settings are how administrators of an application or project configure the workspace for end-users. Be careful to avoid confusing them with "user preferences", which can be configured by individual users. For example, an application might provide a check box to toggle "dark mode" on or off. Each individual user can make their own choice as to whether they want to use this mode - it is a user preference, not a setting. But an administrator may define a setting that controls whether users can see that check box in the first place.

A Setting is simply a name-value pair. The value can be of one of the following types:

  • A string, number, or boolean;
  • An object containing properties of any of these types; or
  • An array containing elements of one of these types.

A SettingName must be unique, 1 to 1024 characters long with no leading nor trailing whitespace, and should begin with the schema prefix of the schema that defines the setting. For example, LandscapePro might define the following settings:

"landscapePro/ui/defaultToolId" "landscapePro/ui/availableTools" "landscapePro/flora/preferredStyle" "landscapePro/flora/treeDbs" "landscapePro/hardinessRange"

Each setting's name begins with the "landscapePro" schema prefix followed by a forward slash. Forward slashes are used to create logical groupings of settings, similar to how file paths group files into directories. In the above example, "ui" and "flora" are two separate groups containing two settings each, while "hardinessRange" is a top-level setting. An application user interface that permits the user to view or edit settings would probably present these groups as individual nodes in a tree view, or as tabs.

Settings schemas

The metadata describing a group of related Settings is defined in a SettingGroupSchema. The schema is based on JSON Schema, with the following additions:

  • schemaPrefix (required) - a unique name for the schema. All of the names in the schema inherit this prefix.
  • description (required) - a description of the schema appropriate for displaying to a user.
  • settingDefs - an object consisting of SettingSchemas describing individual Settings, indexed by their SettingNames.
  • typeDefs - an object consisting of SettingSchemas describing reusable types of Settings that can be referenced by SettingSchemas in this or any other schema.
  • order - an optional integer used to sort the schema in a user interface that lists multiple schemas, where schemas of lower order sort before those with higher order.

We can define the LandscapePro schema programmatically as follows:

const schema: SettingGroupSchema = { schemaPrefix: "landscapePro", description: "LandscapePro configuration settings", settingDefs: { "flora/preferredStyle": { type: "string", description: "The name of one of a set of predefined 'styles' of foliage that might be used to select appropriate flora", }, "flora/treeDbs": { type: "array", extends: "itwin/core/workspace/workspaceDbList", }, "ui/defaultTool": { type: "string", description: "Id of the tool that is active when the application starts", }, "ui/availableTools": { type: "array", description: "Ids of tools that should be shown to the user", items: { type: "string", }, combineArray: true, }, "hardinessRange": { type: "object", description: "Specifies the upper and lower limits on the hardiness zone for flora that can survive in a region", properties: { minimum: { type: "integer", extends: "landscapePro/hardinessZone", description: "The lower limit on hardiness zone for flora that can survive in a region", }, maximum: { type: "integer", extends: "landscapePro/hardinessZone", description: "The upper limit on hardiness zone for flora that can survive in a region", }, }, }, }, typeDefs: { hardinessZone: { type: "integer", description: "A USDA hardiness zone used to describe the surivability of plants in a geographical region", minimum: 0, maximum: 13, }, }, };

This schema defines 5 settingDefs and 1 typeDef. Note the "landscapePro" schema prefix, which is implicitly included in the name of each settingDef and typeDef in the schema - for example, the full name of the "hardinessRange" setting is "landscapePro/hardinessRange".

The "hardinessZone" typeDef represents a USDA hardiness zone as an integer between 0 and 13. The "hardinessRange" settingDef reuses that typeDef for both its "minimum" and "maximum" properties by declaring that each extends that type. Note that extends requires the schema prefix to be specified, even within the same schema that defines the typeDef.

The "flora/treeDbs" settingDef extends the "workspaceDbList" typeDef from a different schema - the workspace schema delivered with the application, with the "itwin/core/workspace" schema prefix.

Registering schemas

Schemas enable the application to validate that the setting values loaded at run-time match the expected types - for example, if we try to retrieve the value of the "landscapePro/ui/defaultToolId" setting and discover a number where we expect a string, an exception will be thrown. They can also be used by user interfaces that allow administrators to configure settings by enforcing types and other constraints like the one that requires "hardinessZone" to be an integer between 0 and 13. To do this, the schema must first be registered.

The set of currently-registered schemas can be accessed via IModelHost.settingsSchemas. You can register new ones in a variety of ways. Most commonly, applications will deliver their schemas in JSON files, in which case they can use SettingsSchemas.addFile to supply a single JSON file or SettingsSchemas.addDirectory to supply a directory full of them. In our case, however, we've defined the schema programmatically, so we'll register it using SettingsSchemas.addGroup:

IModelHost.settingsSchemas.addGroup(schema);

Your application should register its schemas shortly after invoking IModelHost.startup. Registering a schema adds its typeDefs and settingDefs to SettingsSchemas.typeDefs and SettingsSchemas.settingDefs, respectively. It also raises the SettingsSchemas.onSchemaChanged event. All schemas are unregistered when IModelHost.shutdown is invoked.

Settings dictionaries

The values of Settings are provided by SettingsDictionarys. The Settings for the current session can be accessed via the settings property of IModelHost.appWorkspace. You can add new dictionaries to provide settings values at any time during the session, although most dictionaries will be loaded shortly after IModelHost.startup.

Let's load a settings dictionary that provides values for some of the settings in the LandscapePro schema:

const values: SettingsContainer = { "landscapePro/ui/defaultTool": "place-shrub", "landscapePro/ui/availableTools": ["place-shrub", "place-koi-pond", "apply-mulch"], }; const props: SettingsDictionaryProps = { // A unique name for this dictionary. name: "LandscapeProDefaults", // This dictionary's priority relative to other dictionaries. priority: SettingsPriority.defaults, }; IModelHost.appWorkspace.settings.addDictionary(props, values);

Now you can access the setting values defined in the dictionary via IModelHost.appWorkspace.settings:

let defaultTool = IModelHost.appWorkspace.settings.getString("landscapePro/ui/defaultTool"); // "place-shrub" let availableTools = IModelHost.appWorkspace.settings.getArray<string>("landscapePro/ui/availableTools"); // ["place-shrub", "place-koi-pond", "apply-mulch"] let preferredStyle = IModelHost.appWorkspace.settings.getString("landscapePro/flora/preferredStyle"); // undefined const preferredStyleOrDefault = IModelHost.appWorkspace.settings.getString("landscapePro/flora/preferredStyle", "default"); // "default"

Note that getString returns undefined for "landscapePro/preferredStyle" because our dictionary didn't provide a value for it. The overload of that function (and similar functions like Settings.getBoolean and Settings.getObject) allows you to specify a default value to use if the value is not defined.

Note: In general, avoid caching the values of individual settings - just query them each time you need them, because they can change at any time. If you must cache (for example, if you are populating a user interface from the setting values), listen for and react to the Settings.onSettingsChanged event.

Any number of dictionaries can be added to Workspace.settings. Let's add another one:

IModelHost.appWorkspace.settings.addDictionary({ name: "LandscapeProOverrides", priority: SettingsPriority.application, }, { "landscapePro/flora/preferredStyle": "coniferous", "landscapePro/ui/defaultTool": "place-koi-pond", "landscapePro/ui/availableTools": ["place-gazebo", "apply-mulch"], });

This dictionary adds a value for "landscapePro/flora/preferredStyle", and defines new values for the two settings that were also defined in the previous dictionary. See what happens when we look up those settings' values again:

defaultTool = IModelHost.appWorkspace.settings.getString("landscapePro/ui/defaultTool"); // "place-koi-pond" availableTools = IModelHost.appWorkspace.settings.getArray<string>("landscapePro/ui/availableTools"); // ["place-gazebo", "apply-mulch", "place-shrub", "place-koi-pond"] preferredStyle = IModelHost.appWorkspace.settings.getString("landscapePro/flora/preferredStyle"); // "coniferous"

Now, as expected, "landscapePro/flora/preferredStyle" is no longer undefined. The value of "landscapePro/ui/defaultTool" has been overwritten with the value specified by the new dictionary. And the "landscapePro/ui/availableTools" array now has the merged contents of the arrays defined in both dictionaries. What rules determine how the value of a setting is resolved when multiple dictionaries provide a value for it? The answer lies in the dictionaries' SettingsPrioritys.

Settings priorities

Configurations are often layered: an application may ship with built-in default settings, that an administrator may selectively override for all users of the application. Beyond that, additional configuration may be needed on a per-organization, per-iTwin, and/or per-iModel level. SettingsPriority defines which dictionaries' settings take precedence over others - the dictionary with the highest priority overrides any other dictionaries that provide a value for a given setting.

A SettingsPriority is just a number, but specific values carry semantics:

SettingsDictionarys of application priority or lower reside in IModelHost.appWorkspace. Those of higher priority are stored in an IModelDb.workspace - more on those shortly.

What about the "landscapePro/ui/availableTools" array? In the LandscapePro schema, the corresponding settingDef has SettingSchema.combineArray set to true, meaning that - when multiple dictionaries provide a value for the setting - instead of being overridden, they are merged together to form a single array, eliminating duplicates, and sorted in descending order by dictionary priority.

iModel settings

So far, we have been working with IModelHost.appWorkspace. But - as mentioned above - each IModelDb has its own workspace as well, with its own Settings that can override and/or supplement the application workspace's settings. These settings are stored as SettingsDictionarys in the iModel's be_Props table. When the iModel is opened, its Workspace.settings are populated from those dictionaries. So, an application is working in the context of a particular iModel, it should resolve setting values by asking IModelDb.workspace, which will fall back to IModelHost.appWorkspace if the iModel's settings dictionaries don't provide a value for the requested setting.

Since an iModel is located in a specific geographic region, LandscapePro wants to limit the selection of foliage based on the USDA hardiness zone(s) in which the iModel resides. An administrator could configure the hardiness zone of an iModel as follows:

interface HardinessRange { minimum: number; maximum: number; } const range: HardinessRange = { minimum: 6, maximum: 8 }; await iModel.acquireSchemaLock(); iModel.saveSettingDictionary("landscapePro/iModelSettings", { "landscapePro/hardinessRange": range, });

Note that modifying the iModel settings requires obtaining an exclusive write lock on the entire iModel. Ordinary users should never perform this kind of operation - only administrators.

The next time we open the iModel, the new settings dictionary will automatically be loaded, and we can query its settings:

const hardinessRange = iModel.workspace.settings.getObject<HardinessRange>("landscapePro/hardinessRange"); // returns { minimum: 8, maximum: 10 } defaultTool = iModel.workspace.settings.getString("landscapePro/ui/defaultTool"); // returns "place-koi-pond" as specified by IModelHost.appWorkspace.settings.

The "hardinessRange" setting is obtained from the iModel's settings dictionary, while the "defaultTool" falls back to the value defined in IModelHost.appWorkspace.settings.

SettingsDb

So far, we have loaded SettingsDictionarys directly at run-time — building them in code and adding them to Workspace.settings. This works for small-scale configuration and testing, but real-world deployments need a way to store settings in the cloud, version them over time, and discover them without opening an iModel. That is the role of SettingsDb.

A SettingsDb is a dedicated CloudSqlite database that stores settings as a flat JSON object — SettingName keys mapped to Setting values, with no binary resources. Its containers are tagged with containerType: "settings" in their cloud metadata, making them discoverable independently of any iModel. This is what distinguishes a SettingsDb from a WorkspaceDb: a SettingsDb is where you start. You load settings from a SettingsDb, and those settings tell your application which WorkspaceDbs hold the binary resources it needs.

Reading settings

A SettingsDb provides two read methods:

Both methods auto-open and auto-close the underlying database if it is not already open. For batches of reads, call SettingsDb.open before the operations and SettingsDb.close afterwards to avoid repeated open/close overhead.

How SettingsDb fits the priority system

When a SettingsDb is loaded into the runtime via Workspace.getSettingsDb, its contents become one SettingsDictionary in the Settings priority stack. The data flow is:

SettingsDb → JSON → Settings.addJson() → one SettingsDictionary in the priority stack

Each SettingsDb occupies a single slot in the priority system. Multiple SettingsDbs — for example, one scoped to an iTwin at SettingsPriority.iTwin and one scoped to an iModel at SettingsPriority.iModel — become separate dictionaries with separate priorities. The runtime resolves conflicts using the standard priority rules: the highest-priority dictionary that provides a value for a given setting name wins.

Discovering settings containers

You can find all settings containers for a given iTwin by using SettingsEditor.queryContainers to query by iTwinId (and optionally iModelId):

// Query all settings containers for a given iTwin. // SettingsEditor.queryContainers is a convenience wrapper around // BlobContainer.service.queryContainersMetadata that automatically filters by containerType: "settings". const settingsContainers = await SettingsEditor.queryContainers({ iTwinId }); // Each entry includes the containerId, label, description, and other metadata. for (const entry of settingsContainers) { const { containerId: id, label, description } = entry; // Use the containerId to load the SettingsDb via Workspace.getSettingsDb. expect(id).to.be.a("string"); expect(label).to.be.a("string"); expect(description).to.satisfy((d: unknown) => d === undefined || typeof d === "string"); }

This is useful when your application needs to enumerate available settings — for example, building an admin UI that lets users choose which settings profile to load — without hardcoding container IDs.

If you want to open the matching containers for editing in a single call, use SettingsEditor.findContainers. It queries the service, requests write tokens, and opens each matching container:

// Find and open settings containers for a given iTwin in a single call. // This queries the BlobContainer service for settings containers matching the iTwinId, // requests write access tokens, and opens each matching container. const editor = SettingsEditor.construct(); const containers = await editor.findContainers({ iTwinId }); // Use the first container — it is ready for reading or editing its SettingsDbs. const container = containers[0]; const settingsDb = container.getEditableDb(); expect(settingsDb).to.not.be.undefined; editor.close();

Creating a SettingsDb and writing settings

Note: Creating and managing SettingsDb data is a task for administrators. End-users consume settings through the Settings runtime API. The following walkthrough shows the admin-side workflow.

The example below creates a new cloud container, writes some initial settings, and publishes them:

const editor = SettingsEditor.construct(); // Create a new cloud container to hold the SettingsDb. const container: EditableSettingsCloudContainer = await editor.createNewCloudContainer({ metadata: { label: "Regional Park Design", description: "Project-level settings for the Regional Park landscape design" }, scope: { iTwinId }, manifest: { settingsName: "AppSettings", description: "Application configuration settings", contactName: "Lief E. Greene", }, }); // Acquire the write lock and open an editable SettingsDb. container.acquireWriteLock("Lief E. Greene"); const editableDb: EditableSettingsDb = container.getEditableDb({ dbName: "settings-db" }); editableDb.open(); // Write settings directly. const appSettings: SettingsContainer = { "myApp/ui/theme": "dark", "myApp/ui/fontSize": 14, "myApp/ui/sidebar": true, }; editableDb.updateSettings(appSettings); // Read individual settings back. const theme = editableDb.getSetting<string>("myApp/ui/theme"); // "dark" const fontSize = editableDb.getSetting<number>("myApp/ui/fontSize"); // 14 const sidebar = editableDb.getSetting<boolean>("myApp/ui/sidebar"); // true editableDb.close(); container.releaseWriteLock(); editor.close();

The key steps are:

  1. Create an editor — call SettingsEditor.construct. The caller is responsible for calling close() when finished.
  2. Create a containerSettingsEditor.createNewCloudContainer creates a container automatically tagged with containerType: "settings".
  3. Acquire the write lockEditableSettingsCloudContainer.acquireWriteLock. Only one user can hold the lock at a time.
  4. Open an EditableSettingsDbEditableSettingsCloudContainer.getEditableDb returns an EditableSettingsDb.
  5. Write settings — use EditableSettingsDb.updateSettings to replace all settings, or EditableSettingsDb.updateSetting to update a single setting entry.
  6. Release the lockEditableSettingsCloudContainer.releaseWriteLock publishes your changes. Alternatively, EditableSettingsCloudContainer.abandonChanges discards them.

Important: Always release the write lock when you are done. Failing to release it will prevent other administrators from modifying the container until the lock expires.

Updating individual settings

Often you need to change a single setting without touching the rest. EditableSettingsDb.updateSetting reads the existing settings, updates the specified entry, and writes the result back — other settings are preserved:

const editor = SettingsEditor.construct(); const container: EditableSettingsCloudContainer = await editor.createNewCloudContainer({ metadata: { label: "Park Design", description: "Landscape settings" }, scope: { iTwinId }, manifest: { settingsName: "PatchSettings", contactName: "Lief E. Greene" }, }); container.acquireWriteLock("Lief E. Greene"); const editableDb: EditableSettingsDb = container.getEditableDb(); editableDb.open(); // Write initial settings. editableDb.updateSettings({ "myApp/ui/theme": "light", "myApp/ui/fontSize": 14, "myApp/ui/sidebar": true, }); // Update just the theme — other settings are preserved. editableDb.updateSetting({ settingName: "myApp/ui/theme", value: "dark" }); const theme = editableDb.getSetting<string>("myApp/ui/theme"); // "dark" (updated) const fontSize = editableDb.getSetting<number>("myApp/ui/fontSize"); // 14 (preserved) const sidebar = editableDb.getSetting<boolean>("myApp/ui/sidebar"); // true (preserved) editableDb.close(); container.releaseWriteLock(); editor.close();

To remove a setting entirely, use EditableSettingsDb.removeSetting.

To inspect all settings in a SettingsDb, use SettingsDb.getSettings, which returns a deep copy:

const editor = SettingsEditor.construct(); const container: EditableSettingsCloudContainer = await editor.createNewCloudContainer({ metadata: { label: "Park Design", description: "Landscape settings" }, scope: { iTwinId }, manifest: { settingsName: "InspectSettings", contactName: "Lief E. Greene" }, }); container.acquireWriteLock("Lief E. Greene"); const editableDb: EditableSettingsDb = container.getEditableDb(); editableDb.open(); editableDb.updateSettings({ "myApp/display/units": "metric", "myApp/display/precision": 3, "myApp/display/showGrid": true, }); // Use getSettings() to get a copy of all settings as a SettingsContainer. const allSettings: SettingsContainer = editableDb.getSettings(); // allSettings is { "myApp/display/units": "metric", "myApp/display/precision": 3, "myApp/display/showGrid": true } editableDb.close(); container.releaseWriteLock(); editor.close();

Versioning

Like WorkspaceDbs, each SettingsDb uses semantic versioning. Once a version is published to cloud storage it becomes immutable. To evolve settings, create a new version via EditableSettingsCloudContainer.createNewSettingsDbVersion, make changes, and release the write lock. The versioning workflow is the same as described in creating workspace resources.

Putting it together: settings that point to resources

In a typical deployment, the end-to-end flow looks like this:

  1. Admin creates a settings container for the iTwin and writes settings like "landscapePro/flora/treeDbs" that point to WorkspaceDbs holding binary resources.
  2. Admin creates workspace containers holding the versioned WorkspaceDbs (fonts, textures, templates, etc.).
  3. At runtime, the application discovers and loads the settings container via Workspace.getSettingsDb, which adds a SettingsDictionary to the Settings priority stack.
  4. The application reads settings — for example, settings.getSetting("landscapePro/flora/treeDbs") — and uses them to load the appropriate WorkspaceDbs.

This two-layer design keeps settings and resources in separate containers with independent access control, versioning, and write locks. The workspace resources section below shows how to create and access WorkspaceDbs, and the accessing workspace resources section shows how settings point to them.

Workspace resources

"Resources" are bits of data that an application depends on at run-time to perform its functions. The kinds of resources can vary widely from one application to another, but some common examples include:

  • GeographicCRSes used to specify an iModel's spatial coordinate system.
  • Images that can be used as pattern maps for Textures.

It might be technically possible to store resources in Settings, but doing so would present significant disadvantages:

  • Some resources, like images and fonts, may be defined in a binary format that is inefficient to represent using JSON.
  • Some resources, like geographic coordinate system definitions, must be extracted to files on the local file system before they can be used.
  • Some resources may be large, in size and/or quantity.
  • Resources can often be reused across many projects, organizations, and iModels.
  • Administrators often desire for resources to be versioned.
  • Administrators often want to restrict who can read or create resources.

To address these requirements, workspace resources are stored in immutable, versioned CloudSqlite databases called WorkspaceDbs, and Settings are configured to enable the application to locate those resources in the context of a session and - if relevant - an iModel.

A WorkspaceDb can contain any number of resources of any kind, where "kind" refers to the purpose for which it is intended to be used. For example, fonts, text styles, and images are different kinds of resources. Each resource must have a unique name, between 1 and 1024 characters in length and containing no leading or trailing whitespace. A resource name should incorporate a schemaPrefix and an additional qualifier to distinguish between different kinds of resources stored inside the same WorkspaceDb. For example, a database might include text styles named "itwin/textStyles/styleName" and images named "itwin/patternMaps/imageName". Prefixes in resource names are essential unless you are creating a WorkspaceDb that will only ever hold a single kind of resource.

Ultimately, each resource is stored as one of three underlying types:

  • A string, which quite often is interpreted as a serialized JSON object. Examples include text styles and settings dictionaries.
  • A binary blob, such as an image.
  • An embedded file, like a PDF file that users can view in a separate application.

String and blob resources can be accessed directly using WorkspaceDb.getString and WorkspaceDb.getBlob. File resources must first be copied onto the local file system using WorkspaceDb.getFile, and should be avoided unless they must be used with software that requires them to be accessed from disk.

WorkspaceDbs are stored in access-controlled WorkspaceContainers backed by cloud storage. So, the structure of a Workspace is a hierarchy: a Workspace contains any number of WorkspaceContainers, each of which contains any number of WorkspaceDbs, each of which contains any number of resources. The container is the unit of access control - anyone who has read access to the container can read the contents of any WorkspaceDb inside it, and anyone with write access to the container can modify its contents.

Creating workspace resources

Note: Creating and managing data in workspaces is a task for administrators, not end-users. Administrators will typically use a specialized application with a user interface designed for this task. For the purposes of illustration, the following examples will use the WorkspaceEditor API directly.

LandscapePro allows users to decorate a landscape with a variety of trees and other flora. So, trees are one of the kinds of resources the application needs to access to perform its functions. Naturally, they should be stored in the Workspace. Let's create a WorkspaceDb to hold trees of the genus Cornus.

Since every WorkspaceDb must reside inside a WorkspaceContainer, we must first create a container. Creating a container also creates a default WorkspaceDb. In the createTreeDb function below, we will set up the container's default WorkspaceDb to be an as-yet empty tree database.

const editor = WorkspaceEditor.construct(); async function createTreeDb(genus: string): Promise<EditableWorkspaceDb> { const label = `Trees ${genus}`; const description = `Trees of the genus ${genus}`; const container: EditableWorkspaceContainer = await editor.createNewCloudContainer({ // A description of the new `CloudSQLite.Container` for use as a `Workspace` container. metadata: { label: `Workspace for {label}`, description, }, // Ownership and datacenter are defined by the iTwin. Access rights are granted by RBAC administrators of the iTwin. scope: { iTwinId }, // The manifest to be embedded inside the default WorkspaceDb. manifest: { // A user-facing name for the WorkspaceDb. workspaceName: label, // A description of the WorkspaceDb's contents and purpose. description, // The name of someone (typically an administrator) who can provide help and information // about this WorkspaceDb. contactName: "Lief E. Greene", }, }); container.acquireWriteLock("Lief E. Greene"); return container.getEditableDb({}); }

Now, let's define what a "tree" resource looks like, and add some of them to a new WorkspaceDb. To do so, we'll need to make a new version of the empty "cornus" WorkspaceDb we created above. WorkspaceDbs use semantic versioning, starting with a pre-release version (0.0.0). Each version of a given WorkspaceDb becomes immutable once published to cloud storage, with the exception of pre-release versions. The process for creating a new version of a WorkspaceDb is as follows:

  1. Acquire the container's write lock. Only one person - the current holder of the lock - can make changes to the contents of a given container at any given time.
  2. Create a new version of an existing WorkspaceDb.
  3. Open the new version of the db for writing.
  4. Modify the contents of the db.
  5. Close the db.
  6. (Optionally, create more new versions of WorkspaceDbs in the same container).
  7. Release the container's write lock.

Once the write lock is released, the new versions of the WorkspaceDbs are published to cloud storage and become immutable. Alternatively, you can discard all of your changes via EditableWorkspaceContainer.abandonChanges - this also releases the write lock.

Semantic versioning and immutability of published versions are core features of Workspaces. Newly created WorkspaceDbs start with a pre-release version that bypasses these features. Therefore, after creating a WorkspaceDb, administrators should load it with the desired resources and then publish version 1.0.0. Pre-release versions are useful when making work-in-progress adjustments or sharing changes prior to publishing a new version.

interface TreeResource { commonName: string; hardiness: HardinessRange; light: "full" | "shade" | "partial"; } function addTree(treeDb: EditableWorkspaceDb, species: string, tree: TreeResource): void { // We use a prefix to distinguish trees from other kinds of resources that might be present in the same WorkspaceDb. const resourceName = `landscapePro/tree/${species}`; treeDb.addString(resourceName, JSON.stringify(tree)); } let cornusDb = await createTreeDb("cornus"); cornusDb.open(); addTree(cornusDb, "alternifolia", { commonName: "Pagoda Dogwood", hardiness: { minimum: 4, maximum: 8 }, light: "full", }); addTree(cornusDb, "asperifolia", { commonName: "Roughleaf Dogwood", hardiness: { minimum: 9, maximum: 9 }, light: "full", }); // Close the db and release the write lock, which publishes the changes to the cloud and makes // them visible to other users. cornusDb.close(); cornusDb.container.releaseWriteLock(); // We have just created and populated a prerelease version (0.0.0) of the cornusDb. // Let's mint version 1.0.0. // As before, the write lock must be acquired and released, and the db must be opened and closed // to publish the changes to the cloud. cornusDb.container.acquireWriteLock("Lief E. Greene"); const cornusMajorProps = (await cornusDb.container.createNewWorkspaceDbVersion({ versionType: "major", })).newDb; cornusDb = cornusDb.container.getEditableDb(cornusMajorProps); cornusDb.open(); cornusDb.close(); cornusDb.container.releaseWriteLock();

In the example above, we created version 1.1.0 of the "cornus" WorkspaceDb, added two species of dogwood tree to it, and uploaded it. Later, we might create a patched version 1.1.1 that includes a species of dogwood that we forgot in version 1.1.0, and add a second WorkspaceDb to hold trees of the genus abies:

cornusDb.container.acquireWriteLock("Lief E. Greene"); const cornusPatchProps = (await cornusDb.container.createNewWorkspaceDbVersion({ versionType: "patch", })).newDb; cornusDb = cornusDb.container.getEditableDb(cornusPatchProps); cornusDb.open(); addTree(cornusDb, "racemosa", { commonName: "Northern Swamp Dogwood", hardiness: { minimum: 4, maximum: 9 }, light: "full", }); cornusDb.close(); cornusDb.container.releaseWriteLock(); let abiesDb = await createTreeDb("abies"); abiesDb.open(); addTree(abiesDb, "amabilis", { commonName: "Pacific Silver Fir", hardiness: { minimum: 5, maximum: 5 }, light: "full", }); addTree(abiesDb, "balsamea", { commonName: "Balsam Fir", hardiness: { minimum: 3, maximum: 6 }, light: "full", }); abiesDb.close(); abiesDb.container.releaseWriteLock(); // Mint 1.0.0 of abiesDb abiesDb.container.acquireWriteLock("Lief E. Greene"); const abiesMajorProps = (await abiesDb.container.createNewWorkspaceDbVersion({ versionType: "major", })).newDb; abiesDb = abiesDb.container.getEditableDb(abiesMajorProps); abiesDb.open(); abiesDb.close(); abiesDb.container.releaseWriteLock();

Note that we created one WorkspaceContainer to hold versions of the "cornus" WorkspaceDb, and a separate container for the "abies" WorkspaceDb. Alternatively, we could have put both WorkspaceDbs into the same container. However, because access control is enforced at the container level, maintaining a 1:1 mapping between containers and WorkspaceDbs simplifies things and reduces contention for the container's write lock.

Accessing workspace resources

Now that we have some WorkspaceDbs, we can configure our Settings to use them. The LandscapePro schema defines a "landscapePro/flora/treeDbs" setting that extends the type itwin/core/workspace/workspaceDbList. This type defines an array of WorkspaceDbProps, and overrides the combineArray property to true. So, a setting of this type can be resolved to a list of WorkspaceDbs, sorted by SettingsPriority, from which you can obtain resources.

Let's write a function that produces a list of all of the available trees that can survive in a specified USDA hardiness zone:

async function getAvailableTrees(hardiness: HardinessRange): Promise<TreeResource[]> { // Resolve the list of WorkspaceDbs from the setting. const dbs = await iModel.workspace.getWorkspaceDbs({ settingName: "landscapePro/flora/treeDbs" }); // Query for all the trees in all the WorkspaceDbs and collect a list of those that match the hardiness criterion. const trees: TreeResource[] = []; Workspace.queryResources({ dbs, // Include only tree resources, as indicated by their name prefix. namePattern: "landscapePro/tree/%", nameCompare: "LIKE", callback: (resources: Iterable<{ name: string, db: WorkspaceDb }>) => { for (const resource of resources) { // Look up the tree as stringified JSON in the current WorkspaceDb. const str = resource.db.getString(resource.name); assert(undefined !== str); const tree = JSON.parse(str) as TreeResource; if (tree.hardiness.minimum <= hardiness.maximum && hardiness.minimum <= tree.hardiness.maximum) { trees.push(tree); } } }, }); return trees; }

Now, let's configure the "landscapePro/flora/treeDbs" setting to point to the two WorkspaceDbs we created, and use the getAvailableTrees function to retrieve TreeResources from it:

assert(undefined !== cornusDb.cloudProps); // Point the setting at the cornus WorkspaceDb. iModel.workspace.settings.addDictionary({ name: "LandscapePro Trees", priority: SettingsPriority.iModel, }, { "landscapePro/flora/treeDbs": [ { ...cornusDb.cloudProps }, { ...abiesDb.cloudProps }, ], }); const anyHardiness: HardinessRange = { minimum: 0, maximum: 13 }; let allTrees = await getAvailableTrees(anyHardiness); // Roughleaf Dogwood excluded because its hardiness range (9, 9) is outside of the iModel's range (6, 8). const iModelTrees = await getAvailableTrees(iModel.workspace.settings.getObject<HardinessRange>("landscapePro/hardinessRange", anyHardiness));

In the example above, allTrees includes all five tree species from the two genuses, because they all fall within the hardiness range (0, 13). iModelTrees excludes the Roughleaf Dogwood and Pacific Silver Fir, because their hardiness ranges of (9, 9) and (5, 5) do not intersect the iModel's hardiness range (6, 8).

Note that we configured the setting to point to the patch 1.1.1 version of the cornus WorkspaceDb that added the Northern Swamp Dogwood. If we had omitted the WorkspaceDbProps.version property, it would have defaulted to the latest version - in this case, 1.1.1 again, but if in the future we created a new version, that would become the new "latest" version and automatically get picked up for use.

If we configure the setting to use version 1.1.0, then allTrees will not include the Northern Swamp Dogwood added in 1.1.1:

iModel.workspace.settings.addDictionary({ name: "LandscapePro Trees", priority: SettingsPriority.iModel, }, { "landscapePro/flora/treeDbs": [ { ...cornusDb.cloudProps, version: "1.0.0", }, { ...abiesDb.cloudProps }, ], }); allTrees = await getAvailableTrees(anyHardiness);

We could also configure the version more precisely using semantic versioning rules to specify a range of acceptable versions. When compatible new versions of a WorkspaceDb are published, the workspace would automatically consume them without requiring any explicit changes to its Settings.

It may be tempting to "optimize" by calling getAvailableTrees once when your application starts up and caching the result to reuse throughout the session, but remember that the list of trees is determined by a setting, and settings can change at any time during the session. If you must cache, make sure you listen for the Settings.onSettingsChanged event to be notified when your cache may have become stale.

Last Updated: 18 March, 2026