Tutorial: Configuring an iTwin Application with Settings and Workspace Resources

This tutorial walks through configuring a realistic iTwin.js application — LandscapePro, a landscape design tool — from scratch. By the end, you'll understand how settings, workspace resources, and the priority system work together.

Looking for reference documentation? This tutorial tells the story end-to-end. For detailed reference on individual APIs, see Settings and Workspace resources. For how the pieces relate, see the overview.

What LandscapePro needs

LandscapePro lets users decorate a landscape with trees, shrubs, and other flora. To function, the application needs two kinds of data at run-time:

  • Configuration — which tools are available, what foliage style to use, which USDA hardiness zone limits apply.
  • Resources — the actual tree definitions (species, hardiness ranges, light requirements) stored as versioned data in the cloud.

Settings handle the first need. WorkspaceDb containers handle the second. Let's build both, step by step.

Step 1: Define a settings schema

Every setting in iTwin.js belongs to a SettingGroupSchema. The schema describes what settings exist, their types, and how they validate. LandscapePro defines five settings — preferred foliage style, available tools, the default tool, a list of tree databases, and a hardiness range:

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, }, }, };

A few things to notice:

  • All setting names share the "landscapePro" prefix, keeping them isolated from other applications.
  • The "flora/treeDbs" setting extends a built-in type ("itwin/core/workspace/workspaceDbList") — this is how settings will later point to workspace resources.
  • The "hardinessZone" type is defined once in typeDefs and reused by the "hardinessRange" setting's minimum and maximum properties via extends.
  • The "ui/availableTools" setting has combineArray: true, which means multiple dictionaries can each contribute tools and they'll be merged together.

Register the schema shortly after startup so the runtime can validate settings on access:

IModelHost.settingsSchemas.addGroup(schema);

Step 2: Load settings at startup

Settings values come from SettingsDictionarys. Let's load one that provides defaults for LandscapePro:

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 read the values back:

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"

preferredStyle is undefined because our dictionary didn't provide a value for it. The second overload (getString("...", "default")) lets you supply a fallback.

Tip: Avoid caching setting values — query them each time you need them, because they can change when a higher-priority dictionary is loaded. If you must cache, listen for the Settings.onSettingsChanged event.

Step 3: Override settings with a second dictionary

Real applications layer settings from multiple sources. Let's add a second dictionary at a higher priority:

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"], });

The second dictionary is loaded at SettingsPriority.application (200), which is higher than the first dictionary's SettingsPriority.defaults (100). When we read the settings now:

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"
  • defaultTool changed from "place-shrub" to "place-koi-pond" — the higher-priority dictionary wins.
  • preferredStyle is now "coniferous" — the second dictionary filled in the gap.
  • availableTools contains tools from both dictionaries — because combineArray is true in the schema, arrays are merged rather than replaced.

This is the settings priority stack at work. The full priority order, from lowest to highest:

Priority Level Typical use
100 SettingsPriority.defaults Settings loaded at startup
200 SettingsPriority.application App-supplied overrides
300 SettingsPriority.organization Org-wide configuration
400 SettingsPriority.iTwin Per-iTwin configuration
500 SettingsPriority.branch Per-branch overrides
600 SettingsPriority.iModel Per-iModel overrides

Step 4: Persist settings to an iTwin

So far, our settings live only in memory. For production, administrators persist settings to the cloud so they're shared across sessions and users. This is typically done by a setup/admin flow: LandscapePro can save settings scoped to an iTwin, and any session that opens the iTwin workspace then sees the same configuration:

await IModelHost.saveSettingDictionary(iTwinId, "landscapePro/iTwinDefaults", { "landscapePro/flora/preferredStyle": "naturalistic", "landscapePro/ui/defaultTool": "place-shrub", "landscapePro/ui/availableTools": ["place-shrub", "place-koi-pond", "apply-mulch"], "landscapePro/hardinessRange": { minimum: 6, maximum: 8 }, });

IModelHost.saveSettingDictionary writes the dictionary to a cloud-hosted settings container associated with the iTwin. If no default settings container exists yet, this first write creates it. IModelHost.getITwinWorkspace is the read/discovery side only — it loads the iTwin workspace and returns an empty workspace if nothing has been written yet. To read the settings back, open the iTwin workspace:

const lpWorkspace = await IModelHost.getITwinWorkspace(iTwinId); const lpStyle = lpWorkspace.settings.getString("landscapePro/flora/preferredStyle"); // "naturalistic" const lpTool = lpWorkspace.settings.getString("landscapePro/ui/defaultTool"); // "place-shrub" lpWorkspace.close();

The returned Workspace includes the iTwin's settings merged with application defaults. Always call close() when finished to release cloud connections.

To delete a saved dictionary, call IModelHost.deleteSettingDictionary with the iTwin ID and dictionary name.

Step 5: Create workspace resources (tree databases)

Settings tell the application what to do. Workspace resources provide the data to do it with. LandscapePro needs tree definitions — species, hardiness ranges, light requirements. These are stored in versioned WorkspaceDb containers.

Every WorkspaceDb lives inside a WorkspaceContainer. Let's create a container and its default database for dogwood trees (Cornus):

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 define what a "tree" resource looks like and add some species. WorkspaceDbs use semantic versioning — each published version is immutable. The process is: acquire the write lock → create a version → add resources → close → release the lock (which publishes):

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();

Later, we discover a species we forgot. We create a patch version (1.0.1) and a second container for fir trees (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();

We now have two containers: cornus at version 1.0.1 (with three dogwood species) and abies at version 1.0.0 (with two fir species).

Step 6: Connect settings to resources

Here's where the two systems come together. We save a setting that tells LandscapePro which tree databases to load — pointing the "landscapePro/flora/treeDbs" setting at our two WorkspaceDb containers:

assert(undefined !== cornusDb.cloudProps); assert(undefined !== abiesDb.cloudProps); await IModelHost.saveSettingDictionary(iTwinId, "landscapePro/flora", { "landscapePro/flora/treeDbs": [ { ...cornusDb.cloudProps }, { ...abiesDb.cloudProps }, ], }); const workspaceForTreeDbs = await IModelHost.getITwinWorkspace(iTwinId); const workspaceTreeDbs = await workspaceForTreeDbs.getWorkspaceDbs({ settingName: "landscapePro/flora/treeDbs" }); workspaceForTreeDbs.close();

Because this setting is stored at the iTwin scope, every iModel in the iTwin resolves the same tree databases.

Step 7: Query resources at runtime

With the setting in place, application code can resolve the tree databases and query them. Here's a function that finds every tree suitable for a given 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; }

The key call is Workspace.getWorkspaceDbs — it reads the setting, opens the referenced WorkspaceDbs, and returns them. Then Workspace.queryResources iterates resources across all databases by name pattern.

Step 8: Update resource versions

When you publish a new version of a tree WorkspaceDb, update the iTwin setting so all iModels pick up the change:

await IModelHost.saveSettingDictionary(iTwinId, "landscapePro/flora", { "landscapePro/flora/treeDbs": [ { ...cornusDb.cloudProps, version: "1.1.1" }, { ...abiesDb.cloudProps }, ], });

Here we explicitly reference version 1.1.1 of the cornus database. If we had omitted the version property, it would default to the latest available version. Use explicit versions when you need deterministic, reproducible behavior — for example, in regulated workflows.

Step 9: Save settings into an iModel

Sometimes one iModel needs configuration that differs from the rest of its iTwin. Save settings directly into the iModel using EditTxn.saveSettingDictionary:

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

These settings are loaded at SettingsPriority.iModel (600) — the highest built-in priority — so they override everything below them. They persist across sessions and are automatically reloaded when the iModel is opened:

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

The hardiness range comes from the iModel dictionary. The default tool falls through to the app-level dictionary — the priority stack resolves the right value at each level.

Step 10: Override iTwin settings per iModel

Because iModel priority (600) is higher than iTwin priority (400), you can override any iTwin setting for a specific iModel. For example, if the iTwin says "naturalistic" but one iModel represents a formal garden:

// The iTwin setting says "naturalistic", but this iModel is a formal garden. await withEditTxn(iModel, async (txn) => txn.saveSettingDictionary("landscapePro/iModelOverrides", { "landscapePro/flora/preferredStyle": "formal", }));

All other iModels in the iTwin continue using the iTwin-level value.

Step 11: Reference iTwin settings from an iModel

An iModel doesn't automatically inherit iTwin settings — IModelDb.workspace falls back to IModelHost.appWorkspace, not to the iTwin workspace. To bridge them, save a reference to the iTwin settings container inside the iModel:

// Save a floating reference — no `version` field means the iModel // always loads the latest available version of the iTwin settings. const iTwinWorkspaceForModelRef = await IModelHost.getITwinWorkspace(iTwinId); const settingsSourcesForModelRef = iTwinWorkspaceForModelRef.settingsSources; assert(undefined !== settingsSourcesForModelRef); await withEditTxn(iModel, async (txn) => txn.saveSettingDictionary("landscapePro/iModelSettings", { "landscapePro/itwinSettingsRef": settingsSourcesForModelRef, })); iTwinWorkspaceForModelRef.close();

Because the reference has no version constraint, the iModel always uses the latest available version of the iTwin settings. This is usually what you want — updates to the iTwin's configuration automatically apply to all its iModels.

Pinning to a specific version

Sometimes you need reproducibility — a regulatory submission or a client deliverable that must use exactly the settings it was designed with. To pin the iModel to the current version of the iTwin settings, resolve each settings WorkspaceDb and record its exact version:

// Pin the iModel to the exact settings version currently in use. // Unlike the floating reference above, adding a `version` field locks // the iModel to a specific snapshot — configuration won't change when // the iTwin's settings are updated later. const iTwinWorkspaceToPin = await IModelHost.getITwinWorkspace(iTwinId); const floatingRefs = iTwinWorkspaceToPin.settingsSources; assert(undefined !== floatingRefs); // Add an explicit version to each settings source reference. const sources = Array.isArray(floatingRefs) ? floatingRefs : [floatingRefs]; const pinnedRefs = sources.map((source) => ({ ...source, version: "1.0.0" })); await withEditTxn(iModel, async (txn) => txn.saveSettingDictionary("landscapePro/iModelSettings", { "landscapePro/itwinSettingsRef": pinnedRefs, })); iTwinWorkspaceToPin.close();

The key difference is the version field. A floating reference (no version) follows the latest; a pinned reference (exact version like "1.0.0") locks the iModel to that snapshot. To unpin later, save the reference again without a version.

What we built

Starting from nothing, we:

  1. Defined a schema — described what settings LandscapePro uses and how they validate.
  2. Loaded settings — provided defaults and overrides using the priority stack.
  3. Persisted settings to an iTwin — so every iModel shares the same configuration.
  4. Created workspace resources — versioned tree databases in the cloud.
  5. Connected the two — settings point to workspace databases; the application resolves them at runtime.
  6. Customized per iModel — overrode specific settings for one iModel without affecting others.
  7. Pinned versions — locked an iModel to a specific settings snapshot for reproducibility.

Next steps

  • Settings reference — schemas, validation, the full priority system, settings containers, admin workflows.
  • Workspace resources reference — WorkspaceDb structure, resource types (string, blob, file), container access control.
  • Overview — how settings, workspace resources, and the three workspace scopes relate.

Last Updated: 23 April, 2026