An expression calculates one result from the current ProjectWise context, CEL operations, and registered lookups.
Bentley ProjectWise-specific CEL environment
PW CEL
This technical handbook starts with a quick explanation of what PW CEL allows, what it no longer permits, and which limits matter. It then covers contexts, objects, lookups, Bentley extensions, Library Expressions, testing, and real attribute situations. It is a working PW configuration reference, not a lesson-based course.
This reference targets ProjectWise Administrator 2026 and keeps portable core CEL separate from ProjectWise contexts, objects, lookups, and Bentley extensions.
01 · ProjectWise scope
What PW CEL is
ProjectWise CEL is Bentley’s host-specific CEL environment for Document Attributes, WorkArea properties, and Workflow Rules Engine. It is not a general ProjectWise data API and cannot query arbitrary database tables, internal IDs, or bulk data.
PW CEL in three points
There is no arbitrary SELECT, table, view, join, stored procedure, or unrestricted access to the ProjectWise database.
Start with a supported object or lookup instead of a database table. Complex SQL logic may require precomputed data or an integration redesign.
Configuration areas
02 · Runtime model
Contexts and objects
The active context decides which names and objects an expression can access. A valid expression in one context can fail in another.
Document Attributes
Available namesthisDocumentthisFolderthisFormthisUserutcTime- Used for
- Default value, Update value, and Value list expressions
- Important boundary
- Folder and WorkArea hierarchy functions are available only here.
WorkArea Properties
Available namesthisFormthisUserutcTime- Used for
- Property value expressions, mainly depending on other form fields
- Important boundary
- It does not expose the current folder name or parent hierarchy.
Workflow Rules Version
Available namescurrentVersionutcTime- Used for
- CHANGE_VERSION actions in Workflow Rules Engine
- Important boundary
- The expression result becomes the new document version value.
ProjectWise objects
Open an object to see its fields, types, methods, null behavior, and operational boundaries. These names are the actual ProjectWise context contract.
DocumentthisDocumentThe current document and its ProjectWise metadata.
| Field | Type | Meaning |
|---|---|---|
.guid | GUID | Document identifier |
.name | string | Document name |
.description | string | Document description |
.file | File | Associated file object |
.version | string | Version label |
.versionSeq | integer | System-maintained version sequence |
.workflow | Workflow | Assigned workflow |
.state | State | Current workflow state |
.createdBy | User | User who created the document |
.createdOn | timestamp | UTC creation time |
.updatedBy | User | User who last updated the document |
.updatedOn | timestamp | UTC time of the last update |
.application | Application | Associated application |
Methods
getPWLink()→stringgetPWLink(PWLinkType)→stringgetPWLink(PWLinkType, bool)→stringgetHttpLink()→stringgetHttpLink(AppToOpen)→stringAttribute FormthisFormA map of current, including unsaved, form values addressed by internal field name.
FolderthisFolderThe folder containing the current document and its position in the hierarchy.
| Field | Type | Meaning |
|---|---|---|
.guid | GUID | Folder identifier |
.name | string | Folder name |
.description | string | Folder description |
.workflow | Workflow | Assigned workflow |
.environment | Environment | Assigned Environment |
.updatedBy | User | User who last changed the folder |
.updatedOn | timestamp | UTC time of the last change |
.createdBy | User | User who created the folder |
.createdOn | timestamp | UTC creation time |
.isWorkArea | bool | Whether the folder is a WorkArea |
Methods
getWorkArea()→WorkArea | nullgetOwnerWorkArea()→WorkArea | nullgetOwnerWorkArea(string)→WorkArea | nullgetTopFolder()→FoldergetTopWorkArea()→WorkArea | nullgetRelativeFolder(int)→Folder | nullgetFolderPath(Folder)→list<Folder>WorkAreafolder hierarchy resultA WorkArea reached from a Folder or another WorkArea.
| Field | Type | Meaning |
|---|---|---|
.folder | Folder | Folder represented by this WorkArea |
.type | string | WorkArea type |
.properties | map<string,string> | WorkArea Type property values |
Methods
getWorkArea()→WorkAreagetOwnerWorkArea()→WorkArea | nullgetOwnerWorkArea(string)→WorkArea | nullgetTopFolder()→FoldergetTopWorkArea()→WorkArea | nullgetRelativeFolder(int)→Folder | nullgetFolderPath(Folder)→list<Folder>UserthisUser / document user fieldThe active user or a user referenced by another ProjectWise object.
| Field | Type | Meaning |
|---|---|---|
.name | string | Login name |
.description | string | User description or full name |
.email | string | Email address |
FilethisDocument.fileThe physical file associated with a document.
| Field | Type | Meaning |
|---|---|---|
.name | string | File name |
.size | int | File size in bytes |
.updatedBy | User | User who last updated the file |
.updatedOn | timestamp | UTC time of the last file update |
Workflowdocument.workflow / folder.workflowThe workflow assigned to a document or folder.
| Field | Type | Meaning |
|---|---|---|
.name | string | Workflow name |
.description | string | Workflow description |
StatethisDocument.stateThe current workflow state of a document.
| Field | Type | Meaning |
|---|---|---|
.name | string | State name |
.description | string | State description |
ApplicationthisDocument.applicationThe ProjectWise application associated with a document.
| Field | Type | Meaning |
|---|---|---|
.name | string | Application name |
EnvironmentthisFolder.environmentThe Environment assigned to a folder.
| Field | Type | Meaning |
|---|---|---|
.name | string | Environment name |
Documents, folders, and WorkAreas in practice
Open a situation to connect the object model to a concrete document, file, folder hierarchy, or WorkArea property and its expected result.
Documentdocument-metadata-summaryRead a document summary
An attribute needs a compact snapshot of the current document name, version, and workflow state.
{'name': thisDocument.name,
'version': thisDocument.version,
'state': thisDocument.state.name}thisDocument.name = 'BS-A1-DR-001.dgn'thisDocument.version = 'P03'thisDocument.state.name = 'Approved'{name: 'BS-A1-DR-001.dgn', version: 'P03', state: 'Approved'}map<string,string>- Read scalar metadata from the current Document object.
- Follow the state property to the nested State object.
- Return the selected values in one CEL map.
thisDocumentDocument.nameDocument.versionState.nameDocument → Filedocument-file-nameHandle a document with or without a file
A display value should show the physical file name, but new or placeholder documents may not have a file.
thisDocument.file.name == '' ? '(no file)' : thisDocument.file.namethisDocument.file.name = 'BS-A1-DR-001.dgn''BS-A1-DR-001.dgn'string- Read the nested File object from thisDocument.
- Check its name before using it.
- Return either the real file name or an explicit fallback.
thisDocument.fileFile.nameconditionalempty objectFolder → Environmentfolder-environmentShow the folder and its Environment
A calculated value should identify both the containing folder and the Environment assigned to it.
thisFolder.name + ' · ' + thisFolder.environment.namethisFolder.name = 'Drawings'thisFolder.environment.name = 'BS Document Attributes''Drawings · BS Document Attributes'string- Read the current containing Folder.
- Follow environment to the nested Environment object.
- Join both names into a readable label.
thisFolderFolder.nameFolder.environmentEnvironment.nameFolder hierarchyfolder-parentRead the parent folder name
The current document is in Drawings, while an attribute needs the discipline folder one level above it.
[thisFolder.getRelativeFolder(1)].map(parent, parent == null ? '' : parent.name)[0]folder path = Project > Building A > Architecture > DrawingsthisFolder.name = 'Drawings''Architecture'string- Start at the document folder.
- Move one level toward the top of the hierarchy.
- Return the parent name, or an empty string when no parent is available.
thisFoldergetRelativeFolderFolder.namenull guardFolder hierarchy → Documentdocument-full-pathBuild the complete document path
A legacy FULLNAME-style value must be assembled from the real folder hierarchy and document name.
thisFolder.getTopFolder().getFolderPath(thisFolder).map(f, f.name).join('\') + '\' + thisDocument.namefolder names = ['Project', 'Building A', 'Drawings']thisDocument.name = 'A-101.dgn''Project\Building A\Drawings\A-101.dgn'string- Find the top folder and request the path back to thisFolder.
- Replace Folder objects with their names and join them with backslashes.
- Append the current document name.
thisFolderthisDocument.namegetTopFoldergetFolderPathmapjoinFolder → WorkAreaowner-workarea-propertyRead a property from the owning Project WorkArea
A document attribute needs the project code stored on the nearest owning WorkArea of type Project.
[thisFolder.getOwnerWorkArea('Project')].map(wa, wa == null ? '' : wa.properties.ProjectCode)[0]owning WorkArea.type = 'Project'owning WorkArea.properties.ProjectCode = 'BS-RAIL-001''BS-RAIL-001'string- Search upward for the nearest owning WorkArea whose type is Project.
- Guard the object because the search can return null.
- Read ProjectCode from its string properties map.
thisFoldergetOwnerWorkAreaWorkArea.typeWorkArea.propertiesnull guardDocument creationnew-document-null-guardGuard thisDocument during document creation
A default expression can run before the new Document object is available.
thisDocument == null ? '(new document)' : thisDocument.namethisDocument = null'(new document)'string- Check the context object before reading any Document property.
- Return a safe creation-time value when it is null.
- Read the real name only after the Document exists.
thisDocumentnullconditionalDocument.name03 · Data selection
Lookups and supported datasets
PW CEL does not expose supported SQL tables. It exposes registered lookups and the exact built-in DMS.* datasets below.
Three lookup sources
Document Environment lookup
Uses attributes of documents in a selected Environment as lookup rows.
Standard lookup
Loads external ERP, CRM, or other data through PowerShell or the SDK.
Built-in lookup
Exposes selected ProjectWise system data through DMS.* lookup names.
How registered lookups are defined and populated
A lookup definition and its data source are separate concerns. Environment lookups read live document attributes; Standard lookups receive uploaded rows; built-ins are supplied by ProjectWise.
Environment lookupNew-PWLookupA live view over selected attributes of documents in one Environment.
The definition does not copy Environment rows. New, changed, and deleted document attribute values are reflected by subsequent lookup evaluations. A new attribute column must be added to the lookup definition before CEL can select it.
| Definition field | Example | What it controls |
|---|---|---|
| Lookup name | LKP.Status | Unique in the datasource; DMS. is reserved |
| Environment name | Lookup Table - General Attributes | Environment used as the live row source |
| Columns | PW_CODE, PW_DESCRIPTION | Environment attributes exposed to CEL |
| Default value column | PW_CODE | Used as value by select() |
| Default description column | PW_DESCRIPTION | Optional description used by select() |
| Primary filter | PW_FILTER = Status | Optional fixed, case-insensitive equals filters |
| Sorting columns | PW_SORTORDER = asc | Logical order; the sort column need not be selectable |
Standard lookupNew-PWLookup · Set-PWLookupData · SDKA ProjectWise-managed dataset loaded from an external system.
The definition describes the columns and defaults; data is uploaded separately. Keeping it synchronized with SQL, ERP, CRM, CSV, or another source is the integration’s responsibility.
| Definition field | Example | What it controls |
|---|---|---|
| Lookup name | my.towns | Unique in the datasource; DMS. is reserved |
| Columns | country, region, name | At most 10 string columns, 255 characters each |
| Default value column | name | Used as value by select() |
| Default description column | region | Optional description used by select() |
| Sorting columns | sort_order = asc | Optional logical sorting |
Built-in lookup datasets
| Lookup name | Selectable columns | Required hidden filter | Contents |
|---|---|---|---|
DMS.Users | namedescriptionemail | — | All datasource users |
DMS.UserLists | namedescription | — | All user lists |
DMS.UsersInList | namedescriptionemail | list | Users in one list; filtering by list is required |
DMS.UserGroups | namedescription | — | Datasource user groups |
DMS.UsersInGroup | namedescriptionemail | group | Users in one group; filtering by group is required |
DMS.Workflows | namedescription | — | Datasource workflows |
DMS.States | namedescription | — | Datasource states |
DMS.StatesInWorkflow | namedescription | workflow | States in one workflow; filtering by workflow is required |
Lookup query pipeline
Get a lookup, apply the most selective lookup filters, then call select() for rows or selectOne() for one string.
getLookup('DMS.UsersInGroup')
.filterEquals('group', 'Design')
.select('name', 'description')Documented lookup functions
Lookup filters produce another Lookup object. Data is materialized only by select() or selectOne(), so narrow large datasets before selecting.
getLookup
LookupgetLookup(name)Gets a registered or built-in lookup; names are case-insensitive.
getLookup('DMS.Users')listLookups
list<string>listLookups()Lists registered and built-in lookup names in the datasource.
listLookups()filterEquals
Lookup<Lookup>.filterEquals(column, value)Adds an equality filter; chained lookup filters are applied together.
.filterEquals('country', 'US')filterNotEquals
Lookup<Lookup>.filterNotEquals(column, value)Excludes rows whose column equals the supplied value.
.filterNotEquals('status', 'inactive')filterContains
Lookup<Lookup>.filterContains(column, substring)Adds a case-insensitive substring filter.
.filterContains('state', 'new')filterNotContains
Lookup<Lookup>.filterNotContains(column, substring)Excludes rows containing the substring, case-insensitively.
.filterNotContains('name', 'test')filterIn
Lookup<Lookup>.filterIn(column, values)Keeps rows whose column value occurs in the supplied list.
.filterIn('status', ['active', 'pending'])filterNotIn
Lookup<Lookup>.filterNotIn(column, values)Excludes rows whose column value occurs in the supplied list.
.filterNotIn('status', ['inactive', 'closed'])select
list<{value: string, description: string}><Lookup>.select([valueColumn], [descriptionColumn])Returns matching value-list rows. With no arguments it uses the configured default columns.
.select('name', 'description')selectOne
string<Lookup>.selectOne(column[, fallback])Returns one column value. Without fallback, zero or multiple rows fail; with fallback, both cases return it.
.selectOne('description', '')04 · Host additions
Bentley / ProjectWise extensions
These names are supplied by ProjectWise. Do not present them as portable core CEL syntax.
Folder and WorkArea access
getWorkAreagetOwnerWorkAreagetTopFoldergetTopWorkAreagetRelativeFoldergetFolderPathDocument Attributes context only; object-returning methods may return null.
String extensions
charAtformatincrementAlphaVersionindexOflastIndexOfparseparseRegexreplacereversesplitsubstringtoLowertoUppertrimtakeLefttakeRightdropLeftdropRightProjectWise additions include .NET-style format, parsers, and slicing helpers.
List extensions
takeLefttakeRightdropLeftdropRightReturn a selected edge or the remainder of a list.
Time extensions
toLocalTimetoUTCsecondsOffsetUse TZDB/IANA names and a ProjectWise local-time compound object.
Document links
getPWLinkgetHttpLinkPWLinkTypeAppToOpenCreates PW URL/URN links or web links targeting WEB, WEBVIEW, or PWE.
Signatures, return types, behavior, and examples
Folder and WorkArea hierarchyDocument Attributes context onlyNavigate from the document folder toward the datasource root and back.
getWorkArea
WorkArea | nullFolder.getWorkArea() / WorkArea.getWorkArea()Converts a WorkArea folder to a WorkArea object; a normal folder returns null.
thisFolder.getWorkArea().typegetOwnerWorkArea
WorkArea | null<Folder|WorkArea>.getOwnerWorkArea([workAreaType])Finds the nearest owning WorkArea, optionally restricted by case-insensitive type.
thisFolder.getOwnerWorkArea('Project').properties.CodegetTopFolder
Folder<Folder|WorkArea>.getTopFolder()Returns the top-most folder relative to the current document path.
thisFolder.getTopFolder().namegetTopWorkArea
WorkArea | null<Folder|WorkArea>.getTopWorkArea()Returns the top-most WorkArea in the current path, or null when none exists.
thisFolder.getTopWorkArea().typegetRelativeFolder
Folder | null<Folder|WorkArea>.getRelativeFolder(level)0 keeps the origin, positive moves toward the top, negative moves toward the document.
thisFolder.getTopFolder().getRelativeFolder(-3).descriptiongetFolderPath
list<Folder><Folder|WorkArea>.getFolderPath(endFolder)Returns the ordered folder path between the receiver and the end folder.
thisFolder.getTopFolder().getFolderPath(thisFolder).map(f, f.name).join('\')String functions18 ProjectWise entriesFormatting, parsing, searching, splitting, and controlled slicing.
charAt
string<string>.charAt(position)Character at a zero-based position
'abc'.charAt(1)format
string<format>.format(value[, locale]).NET-compatible value, number, and date formatting
'{0:yyyy-MM-dd}'.format(utcTime)incrementAlphaVersion
string<string>.incrementAlphaVersion([alphabet])Increments the last alphabetic version position
'Z'.incrementAlphaVersion()indexOf
int<string>.indexOf(fragment[, offset])First zero-based occurrence, optionally from an offset
'abc'.indexOf('b')lastIndexOf
int<string>.lastIndexOf(fragment[, maxIndex])Last zero-based occurrence, optionally bounded
'abcba'.lastIndexOf('b')parse
list<map><string>.parse(patterns)Parses named A, N, and wildcard fragments; returns a singleton result list
'P01.02'.parse(['{pre:A}{maj:N}.{min:N}'])parseRegex
list<map><string>.parseRegex(patterns)Tests regex patterns and returns match index plus capture groups
'P01.02'.parseRegex([R'(P\d+)([.])(\d+)'])replace
string<string>.replace(what, replacement[, limit])Replaces all or a limited number of occurrences
'abccc'.replace('c', 'x', 2)reverse
string<string>.reverse()Reverses the characters
'abc'.reverse()split
list<string><string>.split(separator[, limit])Splits text, optionally limiting the result count
'ab-cd-ef'.split('-', 2)substring
string<string>.substring(position[, count])Returns text from a position; the second argument is count, not end index
'abc'.substring(1, 2)toLower
string<string>.toLower()Converts text to lowercase
'ABC'.toLower()toUpper
string<string>.toUpper()Converts text to uppercase
'abc'.toUpper()trim
string<string>.trim()Removes leading and trailing whitespace
' ab '.trim()takeLeft
string<string>.takeLeft(count)Returns the first count characters
'abcdef'.takeLeft(3)takeRight
string<string>.takeRight(count)Returns the last count characters
'abcdef'.takeRight(3)dropLeft
string<string>.dropLeft(count)Drops the first count characters
'abcdef'.dropLeft(2)dropRight
string<string>.dropRight(count)Drops the last count characters
'abcdef'.dropRight(2)List functionstake / dropSelect or remove elements from either edge of a list.
takeLeft
list<list>.takeLeft(count)Keeps the first count elements
[1,2,3,4].takeLeft(2)takeRight
list<list>.takeRight(count)Keeps the last count elements
[1,2,3,4].takeRight(2)dropLeft
list<list>.dropLeft(count)Drops the first count elements
[1,2,3,4].dropLeft(2)dropRight
list<list>.dropRight(count)Drops the last count elements
[1,2,3,4].dropRight(2)Time functions and objectstimestamp · duration · PWLocalTimeUTC evaluation time, IANA zones, local formatting, and conversion.
timestamp and duration
timestamp / durationtimestamp(...) ± duration(...)Core CEL timestamp arithmetic and comparisons remain available.
timestamp('2020-12-10T00:00:00Z') + duration('1h')toLocalTime
PWLocalTime<timestamp>.toLocalTime(timeZone)Converts UTC to a ProjectWise local-time object using a TZDB/IANA name.
utcTime.toLocalTime('Europe/London')secondsOffset
int<PWLocalTime>.secondsOffsetExposes the active time-zone offset in seconds.
utcTime.toLocalTime('Europe/Berlin').secondsOffsettoUTC
timestamp<PWLocalTime>.toUTC()Converts a local-time object back to its UTC timestamp.
utcTime.toLocalTime('Europe/London').toUTC()format local time
string<format>.format(PWLocalTime[, locale])Formats after applying the local-time offset.
'{0:yyyy-MM-dd}'.format(utcTime.toLocalTime('Europe/London'))05 · Help in practice
Official ProjectWise CEL situations
Bentley’s help examples show how context objects and extensions combine into complete attribute expressions. Each pattern below explains the input, transformation, result, and host boundary.
Dependent title-block date
Set a date only when the author field carries a real value.
thisForm.TB_DRAWN_BY != '---' ? '{0:yyyy-MM-dd}'.format(utcTime) : '---'thisForm reads the current unsaved author value. The conditional either formats the evaluation time or preserves the title-block placeholder.
thisForm.TB_DRAWN_BY = 'P.Novák'utcTime = timestamp('2026-08-17T09:15:00Z')'2026-08-17'stringthisFormutcTimeformatconditionalFile extension
Read the final segment of an associated file name.
[thisDocument.file.name.split('.')].map(r, r[r.size()-1])The singleton list creates a scoped value, split produces file-name segments, and the final index selects the extension.
thisDocument.file.name = 'BS-A1-DR-001.dgn''dgn'stringthisDocument.filesplitmapsizeFixed-format file name
Extract a named numeric fragment and provide a non-match fallback.
thisDocument.file.name.parse(['{a:N}P{b:N}.{c:N}.DWG']).map(r, r.match == 0 ? r.b : -1)parse returns one result map. match identifies the successful pattern and b contains the requested fragment.
thisDocument.file.name = '100P20.30.DWG''20'stringthisDocument.fileparsemapconditionalCurrent-user initials
Build initials from the current user description.
thisUser.description.split(' ').map(r, r.substring(0,1)).join()The description is split into words, every word contributes its first character, and join produces one string.
thisUser.description = 'Peter Novák''PN'stringthisUsersplitmapsubstringjoinFormatted document date
Format the document update timestamp for an attribute value.
'{0:yyyy-MM-dd}'.format(thisDocument.updatedOn)updatedOn is a UTC timestamp; format returns the requested text representation.
thisDocument.updatedOn = timestamp('2026-08-17T14:20:00Z')'2026-08-17'stringthisDocument.updatedOnformatDocument path
Assemble a readable folder path and document name without a legacy FULLNAME token.
thisFolder.getTopFolder().getFolderPath(thisFolder).map(f, f.name).join('\\') + '\\' + thisDocument.nameThe hierarchy function returns Folder objects, map keeps their names, join inserts separators, and the document name is appended.
folder names = ['Project', 'Building A', 'Drawings']thisDocument.name = 'A-101.dgn''Project\Building A\Drawings\A-101.dgn'stringthisFolderthisDocumentgetTopFoldergetFolderPathmapjoin06 · BS attributes in practice
Real BS attribute samples
These expressions come from the supplied BS ProjectWise configuration. Each sample connects the actual attribute code to example form values or lookup rows, the expected output, and a step-by-step explanation.
PW_FULLNAMEbs-pw-fullnameResolve a full name from the selected ProjectWise user
The form stores a ProjectWise login in PW_USERNAME, but the title block needs the readable user description.
getLookup('DMS.Users')
.filterEquals('name', thisForm.PW_USERNAME)
.selectOne('description', '')thisForm.PW_USERNAME = 'pnovak'DMS.Users row = {name: 'pnovak', description: 'Peter Novák'}'Peter Novák'string- Open the built-in DMS.Users dataset.
- Keep the row whose name equals the form login.
- Return its description, or an empty string when exactly one row is not available.
thisForm.PW_USERNAMEDMS.UsersnamedescriptionPW_USERINITIALSbs-pw-userinitialsBuild initials from the resolved full name
PW_FULLNAME already contains a readable name and the next attribute must derive compact initials.
thisForm.PW_FULLNAME.trim()
.split(' ')
.map(p, p.charAt(0))
.join()
.toUpper()thisForm.PW_FULLNAME = 'Peter Novák''PN'string- Remove whitespace around the full name.
- Split the name into words and keep the first character of each word.
- Join the characters and normalize them to uppercase.
thisForm.PW_FULLNAMEtrimsplitmapcharAtjointoUpperTB_DRAWN_BYbs-tb-drawn-byPopulate the title-block author only when triggered
A trigger decides whether the current user’s configured title-block name is written or a placeholder is kept.
thisForm.TRIG_DRAWN == '1'
? getLookup('Lkp.UserSupl')
.filterEquals('PW_USERNAME', thisUser.name)
.selectOne('PW_TBNAME', '')
: '---'thisForm.TRIG_DRAWN = '1'thisUser.name = 'pnovak'Lkp.UserSupl row = {PW_USERNAME: 'pnovak', PW_TBNAME: 'P.Novák'}'P.Novák'string- Check the trigger first.
- Find the current login in the supplemental user lookup.
- Return the configured title-block name.
thisForm.TRIG_DRAWNthisUser.nameLkp.UserSuplPW_TBNAMEFI_ROLE_CODEbs-fi-role-codeDerive a role code from the folder hierarchy
The role is encoded in the description of a folder at a fixed position below the top folder.
thisFolder.getTopFolder().getRelativeFolder(-3).descriptionfolder path = Project > Building A > Architecture > DocumentsDocuments.description = 'ARC''ARC'string- Move from the document folder to the top folder.
- Move three levels back toward the document.
- Read the selected folder description.
thisFoldergetTopFoldergetRelativeFolderFolder.descriptionRV_DATE_1bs-rv-date-1Stamp a local approval date
The revision date stays as a placeholder until an approver is present; then it uses the London calendar date.
thisForm.TB_APPROVED_BY == '---'
? '---'
: '{0:yyyy-MM-dd}'.format(utcTime.toLocalTime('Europe/London'))thisForm.TB_APPROVED_BY = 'A.Smith'utcTime = timestamp('2026-08-17T22:30:00Z')'2026-08-17'string- Check whether a real approver exists.
- Convert the UTC evaluation time to Europe/London.
- Format only the local calendar date.
thisForm.TB_APPROVED_BYutcTimetoLocalTimeformatPW_USERNAMEbs-pw-usernameOffer only users missing from the supplemental lookup
An administrator is adding supplemental user records and should not see logins that are already registered.
getLookup('DMS.Users')
.select('name')
.map(dmsuser,
!(dmsuser in getLookup('Lkp.UserSupl').select('PW_USERNAME')),
dmsuser)DMS.Users names = ['pnovak', 'asmith', 'bnovak']Lkp.UserSupl PW_USERNAME values = ['pnovak', 'asmith']only bnovak remains selectablevalue-list rows- Load all datasource user names.
- Load user names already present in Lkp.UserSupl.
- Keep only names that are not in the supplemental set.
DMS.UsersLkp.UserSuplselectmapin07 · Reuse and proof
Library Expressions
Database-stored CEL expressions are registered below lib. A library expression accepts one map argument, reads it through arg, and can return any CEL-supported type.
lib.calculate({'x': 1, 'y': 2})
// inside the library expression
arg.x + arg.yPowerShell management
Get-PWCelLibraryExpressionNew-PWCelLibraryExpressionUpdate-PWCelLibraryExpressionRemove-PWCelLibraryExpressionTesting with pwps
Invoke-PWCelExpression supports contextless, version, and Document Attributes executions with the corresponding ProjectWise context values.
Invoke-PWCelExpression -Expression "1 + 1"Invoke-PWCelExpression -Expression "currentVersion.incrementAlphaVersion()" -CurrentVersion "A"Invoke-PWCelExpression -Expression "thisDocument.name" -DocumentGUID "<guid>" -FormFields @{ Field = "value" }Built-in WRE version expressions
ProjectWise documents these 20 DMS.* version algorithms. Library Expressions can expose built-ins through the lib.DMS.* namespace.
DMS.INC_ALPHADMS.INC_ALPHANUM_MAJDMS.INC_ALPHANUM_MAJ_P1DMS.INC_ALPHANUM_MINDMS.INC_ALPHANUM_MIN_P1DMS.INC_MAJDMS.INC_MAJ_P1_P1DMS.INC_MAJ_P1_P2DMS.INC_MAJ_P2_P1DMS.INC_MINDMS.INC_MIN_P1_P1DMS.INC_MIN_P1_P2DMS.INC_MIN_P2_P1DMS.INC_NUMBERDMS.INC_NUMBER_P1DMS.PUBDMS.PUB_P1DMS.REMOVE_ALPHANUM_MINDMS.REMOVE_MINDMS.REMOVE_MIN_P108 · Legacy → PW CEL
Migration from legacy configurations
This is one part of PW CEL. Cases distinguish ProjectWise system variables from actual SQL functions and identify whether the change is direct, semantic, or a redesign.
What the Bentley migration appendix covers
ProjectWise system variables
Context values such as user, form, folder, WorkArea, and UTC time. Internal numeric IDs, table names, and several legacy variables have no direct CEL access.
$USER.NAME$$EDIT#XXX$VAULT.NAME$$DATE$Built-in SQL functions
Documented replacements include conversions, conditions, null fallback, strings, dates, and collection operations. Several SQL functions have no direct replacement.
IIFISNULLCHARINDEXDATEADDSUBSTRINGISO19650 stored procedures
Bentley documents CEL replacement patterns for these six legacy procedures.
dms_ISO19650_attr_lkpdms_ISO19650_attr_lkp_statedms_ISO19650_user_name_calcdms_ISO19650_action_namesdms_ISO19650_datedms_ISO19650_root_folder_attributeCurrent user name
Replace the user system variable with a ProjectWise CEL context object.
$USER.NAME$thisUser.nameActive form field value
Convert an $EDIT attribute reference to the thisForm map.
$EDIT#DisciplinethisForm.DisciplineOwner WorkArea property
Replace a project system variable with explicit hierarchy traversal.
$PROJECT#PROJECT_CodethisFolder.getOwnerWorkArea().properties.CodeCurrent date in a local time zone
Replace $DATE$ with an explicit UTC conversion and format.
$DATE$'{0:yyyy-MM-dd}'.format(utcTime.toLocalTime('Europe/Bratislava'))Conditional value
Replace SQL IIF with the CEL conditional operator.
IIF(500 < 1000, 'YES', 'NO')500 < 1000 ? 'YES' : 'NO'Fallback value for null
Replace SQL ISNULL with an explicit null check.
ISNULL(prop, 'ABC')prop == null ? 'ABC' : propCharacter position in text
Account for different index bases when replacing CHARINDEX.
CHARINDEX('C', 'ABC')'ABC'.indexOf('C') + 1Document path
Replace the unsupported FULLNAME variable by assembling a path from objects.
$DOCUMENT.FULLNAME$thisFolder.getTopFolder().getFolderPath(thisFolder).map(f, f.name).join('\\') + '\\' + thisDocument.name