Skip to content

Expression Language Guide

When a Synchronisation Rule flows an attribute from one system to another, it often needs to do more than copy a value across unchanged. You might want to build an email address from a first and last name, force a username to lower case, decide whether an account should be enabled based on someone's employment status, or skip an object entirely unless it meets a condition. JIM's expression language is how you describe these transformations.

An expression is a short, readable formula that JIM evaluates during synchronisation to produce the value for a target attribute. If you have ever written a formula in a spreadsheet cell, this will feel familiar: you reference attributes much as a spreadsheet references cells, combine them with operators and functions, and JIM works out the result for each object as it is synchronised. No programming experience is required.

Where Expressions Fit

Expressions live inside Synchronisation Rules, in what is known as the Attribute Flow: the part of a rule that decides which value lands in each target attribute. Most flows are a straightforward direct mapping (copy cs["mail"] straight into mv["Email"]); you only reach for an expression when a plain copy is not enough.

Within a Synchronisation Rule, expressions are used in:

  • Export attribute mappings
    Transform metaverse attributes before sending them to a Connected System.
  • Import attribute mappings
    Transform Connected System attributes before storing them in the metaverse.
  • Conditional logic
    Choose different values based on conditions (e.g. enable or disable an account based on employee status).
  • Scoping filters
    Determine which objects are in scope for a Synchronisation Rule.

To add or edit these mappings in the admin UI, see Synchronisation Rules. The rest of this guide focuses on the expressions themselves: the syntax, the built-in functions, and worked examples you can adapt.

Expressions are also used by example data generation templates to construct a generated attribute's value from other attributes on the same object, for example building an email address whose domain is derived from a generated company name. Only the mv accessor applies in that context (generation builds Metaverse Objects directly, so there is no Connected System Object), but the same syntax and functions described below apply.

Quick Examples

Before diving into the details, here are a few examples to give you a feel for how expressions work:

// Build an email address from first and last name
Lower(mv["First Name"]) + "." + Lower(mv["Last Name"]) + "@company.com"

// Enable or disable an account based on employee status
IIF(Eq(mv["Employee Status"], "Active"), 512, 514)

// Use the preferred name if available, otherwise fall back to the first name
Coalesce(mv["Preferred Name"], mv["First Name"])

Attribute Access

Expressions work with two sources of data:

  • mv
    Attributes on the Metaverse Object (the central identity record).
  • cs
    Attributes on the Connected System Object (the external system record).

Access an attribute by putting its name in square brackets and quotes:

mv["Display Name"]
mv["Employee Status"]
mv["Department"]

cs["sAMAccountName"]
cs["userAccountControl"]

Attribute names are matched case-insensitively, so mv["Department"], mv["department"], and mv["DEPARTMENT"] all refer to the same attribute. This applies to attribute names only; attribute values are compared case-sensitively by default, which is why text comparisons use Eq() and Lower() (see String Comparison). Mirroring the casing shown in the JIM admin UI keeps expressions readable, but it is not required for them to work. For the wider picture of where JIM is case-sensitive and where it is forgiving, see Case Sensitivity.

Operators

You can use standard operators to combine values, do arithmetic, and make comparisons.

Joining Text

Use + to join text values together:

mv["First Name"] + " " + mv["Last Name"]
// If First Name is "Jane" and Last Name is "Smith", the result is "Jane Smith"

"CN=" + mv["Account Name"] + ",OU=Users,DC=company,DC=local"

Arithmetic

Standard maths operators work with numbers:

Operator Meaning Example Result
+ Add 10 + 3 13
- Subtract 10 - 3 7
* Multiply 10 * 3 30
/ Divide 10 / 3 3
% Remainder 10 % 3 1

Comparisons

Compare values using these operators. The result is either true or false:

Operator Meaning Example Result
> Greater than 10 > 3 true
< Less than 10 < 3 false
>= Greater than or equal to 10 >= 10 true
<= Less than or equal to 3 <= 10 true
!= Not equal to 10 != 3 true

Important: For comparing text values (strings), always use the Eq() function instead of ==. See String Comparison below.

Logic

Combine multiple conditions using logical operators:

Operator Meaning Example Result
&& AND 10 > 3 && 5 > 2 true
\|\| OR 10 > 3 \|\| 5 < 2 true
! NOT !(10 > 3) false

Example -- check that an employee is both active and in the IT department:

Eq(mv["Employee Status"], "Active") && Eq(mv["Department"], "IT")

String Comparison

Always use Eq() for comparing text values, NOT ==.

The == operator can give incorrect results when comparing attribute values to text. This is a technical limitation of how attribute values are stored internally.

// WRONG -- may give incorrect results
IIF(mv["Employee Status"] == "Active", 512, 514)

// CORRECT -- always use Eq() for text comparisons
IIF(Eq(mv["Employee Status"], "Active"), 512, 514)

For case-insensitive comparisons (where "Active", "ACTIVE", and "active" should all match), wrap the value in Lower() first:

Eq(Lower(mv["Status"]), "active")

Rule of thumb: Use Eq() whenever you are comparing attribute values to text. Use >, <, >=, <=, != for number comparisons.

Built-in Functions

String Functions

Function Description Example
Trim(value) Remove spaces from the start and end Trim(mv["Name"])
Upper(value) Convert to uppercase Upper(mv["Name"])
Lower(value) Convert to lowercase Lower(mv["Name"])
Capitalise(value) Capitalise each word (handles hyphenated names like "O'Brien-Smith") Capitalise(mv["Name"])
Left(value, count) Take the first N characters Left(mv["Name"], 3)
Right(value, count) Take the last N characters Right(mv["Name"], 4)
Substring(value, start, length) Extract a portion of text Substring(mv["Name"], 2, 4)
Replace(value, old, new) Replace one piece of text with another Replace(mv["Email"], "@old.com", "@new.com")
Length(value) Count the number of characters Length(mv["Name"])
IsNullOrEmpty(value) Check if a value is missing or blank IsNullOrEmpty(mv["Name"])
IsNullOrWhitespace(value) Check if a value is missing, blank, or only spaces IsNullOrWhitespace(mv["Name"])
StartsWith(value, prefix) Check if text begins with a specific value StartsWith(mv["Email"], "admin")
EndsWith(value, suffix) Check if text ends with a specific value EndsWith(mv["Email"], "@company.com")
Contains(value, search) Check if text contains a specific value Contains(mv["Email"], "@company")

Conditional Functions

Function Description Example
IIF(condition, trueValue, falseValue) Return one value if a condition is true, another if false IIF(Eq(mv["Status"], "Active"), 512, 514)
Coalesce(value1, value2) Use the first value if it exists, otherwise use the second Coalesce(mv["Preferred Name"], mv["First Name"])
Eq(value1, value2) Check if two values are equal (required for text comparisons) Eq(mv["Department"], "IT")

Conversion Functions

Function Description Example
ToString(value) Convert a value to text ToString(mv["EmployeeId"])
ToInt(value) Convert text to a whole number (returns 0 if not a valid number) ToInt(mv["Age"])

Date Functions

Function Description Example
Now() Current date and time (UTC) Now()
Today() Current date at midnight (UTC) Today()
FormatDate(date, format) Format a date as text (e.g., "2026-03-15") FormatDate(mv["HireDate"], "yyyy-MM-dd")
ToFileTime(date) Convert a date to Active Directory's FILETIME format ToFileTime(mv["Account Expires"])
FromFileTime(filetime) Convert an Active Directory FILETIME back to a date FromFileTime(cs["accountExpires"])

Distinguished Name (DN) Functions

Function Description Example
EscapeDN(value) Safely escape special characters for use in LDAP distinguished names "CN=" + EscapeDN(mv["Display Name"]) + ",OU=Users,DC=domain,DC=local"

Password Generation

Function Description Example
RandomPassword(length, extendedChars) Generate a random password. Set extendedChars to true for special characters (!@#$%^&*) RandomPassword(16, true)
RandomPassphrase(wordCount, separator) Generate a passphrase from random words RandomPassphrase(4, "-")

Collection Functions

These functions work with multi-valued attributes (attributes that hold a list of values rather than a single value).

Function Description Example
CollectionContains(collection, value) Check if a list of values contains a specific value CollectionContains(cs["memberOf"], "CN=Admins")
Split(value, delimiter) Split a delimited text value into a list Split(cs["coursesCompleted"], "\|")
Join(collection, delimiter) Combine a list of values into a single delimited text value Join(mv["Groups"], ",")

Account Control Functions

These functions are primarily used with Active Directory's userAccountControl attribute, which stores account settings as a single number using individual bit flags.

Function Description Example
EnableUser(uac) Enable a user account EnableUser(cs["userAccountControl"])
DisableUser(uac) Disable a user account DisableUser(cs["userAccountControl"])
SetBit(value, bit) Turn on a specific flag SetBit(mv["uac"], 65536)
ClearBit(value, bit) Turn off a specific flag ClearBit(mv["uac"], 65536)
HasBit(value, bit) Check if a specific flag is turned on HasBit(cs["userAccountControl"], 2)

⚠️ Nulls, Missing Inputs, and Whitespace

This is the most important section to read before you write an expression that references an attribute which might be absent. How JIM treats a missing input decides whether a target value is cleared, kept, or silently filled with malformed data. Get this wrong and an expression can quietly clear good data, or write a broken value that passes straight through to the target system.

When you reference an attribute the object does not have (for example cs["middleName"] on a person with no middle name), the expression sees null. There are four behaviours to understand, and a per-mapping setting (point 5) that lets you have JIM refuse the outcome rather than guess at it.

1. A null result means "no value", never "leave it alone"

When an expression evaluates to null, JIM treats that as a deliberate assertion that the mapping supplies no value, not as "no opinion, leave it alone". This is intentionally different from the behaviour of some traditional ILM tools, where a function call that returned null meant "do nothing".

What happens to the target depends on how many Synchronisation Rules contribute to the attribute:

  • A single contributor (the common case).
    A null result clears the target: any existing value on the Metaverse attribute is removed.
  • Multiple contributors.
    Attribute Priority resolves the outcome. A null from the winning rule hands the attribute to the next-priority contributor within the same synchronisation run, so a lower-priority source's value takes over rather than the attribute going blank; the target is only cleared when no contributor supplies a value. The exception is a rule with "Null is a value" set: its null is authoritative, the target is cleared, and lower-priority contributors are not consulted. A null from a lower-priority rule never disturbs the winning value.

The authoring takeaway is the same either way: a null result is never "keep the current value". If you do not want a missing input to change the target, you must guard the expression (see point 3).

2. Built-in string functions are null-safe and propagate null

The string functions (Trim, Upper, Lower, Capitalise, Left, Right, Substring, Replace, EscapeDN, and similar) are null-safe: given a null input, they return null rather than erroring. That is convenient, but it means null travels through them unchanged.

So a bare Trim(cs["middleName"]) on a person with no middle name evaluates to null, which (per point 1) clears the target attribute:

// HAZARD -- if middleName is absent, this evaluates to null and CLEARS the target
Trim(cs["middleName"])

// SAFE -- fall back to the existing target value, or to an empty result you control
Coalesce(Trim(cs["middleName"]), mv["Middle Name"])

To avoid an accidental clear, guard the expression so it never resolves to a bare null for a value you want to preserve: use Coalesce() to supply a fallback, or IIF() with an IsNullOrEmpty() / IsNullOrWhitespace() check (see point 3).

3. String concatenation does NOT yield null; it yields a malformed value

This is the more dangerous case, because it fails silently with wrong data rather than clearing. In a + concatenation, a null operand is treated as an empty string, not as null. So the whole expression produces a real, non-null value that is stored as-is and bypasses the null/clear path entirely.

// HAZARD -- if Last Name is absent, this produces "jane." (a real value), not null
Lower(cs["First Name"]) + "." + Lower(cs["Last Name"]) + "@company.com"
// Result: "jane.@company.com" -- a malformed email, written without complaint

The same hazard produces malformed LDAP distinguished names when a component is missing:

// HAZARD -- if Account Name is absent, this produces "CN=,OU=Users,..." -- a broken DN
"CN=" + mv["Account Name"] + ",OU=Users,DC=company,DC=local"

Steer around it by validating or supplying fallbacks before composing the value:

Technique Use it to Example
Coalesce(a, b) Supply a fallback when the first value is missing Coalesce(mv["Preferred Name"], mv["First Name"])
IIF(condition, trueVal, falseVal) Build the composed value only when its inputs are present, otherwise fall back to a safe value IIF(IsNullOrWhitespace(cs["Last Name"]), mv["Email"], Lower(cs["First Name"]) + "." + Lower(cs["Last Name"]) + "@company.com")
IsNullOrEmpty / IsNullOrWhitespace Test an input before composing with it IsNullOrWhitespace(cs["Last Name"])
EscapeDN plus careful construction Build directory DNs safely; still guard each required component for presence "CN=" + EscapeDN(mv["Display Name"]) + ",OU=Users,DC=company,DC=local"

EscapeDN() protects against special characters such as commas in a value; it does not protect against a value being absent. A missing required component still yields a malformed DN, so guard for presence as well as escaping for safety.

Where a component simply has no safe fallback, guarding is not enough and you want JIM to refuse the value outright: set the mapping's Missing Input Behaviour (point 5).

4. Whitespace-only results become no value (by default)

A result that is only whitespace (spaces, tabs, newlines) is, by default, collapsed to no value and clears the target, exactly like null. This is governed per import mapping by the Treat whitespace as no value control in inbound value processing, which is on by default. Switch it off for a mapping where whitespace is genuinely meaningful, and a whitespace-only result is stored as a literal value instead (the portal flags it with a (whitespace) indicator so it is not mistaken for an empty cell).

5. Missing Input Behaviour: have JIM refuse rather than guess

Points 1 to 4 are about writing an expression that copes with an absent input. Missing Input Behaviour is the other half: it lets you tell JIM what to do when an attribute your expression reads has no value, so a mapping you have not guarded cannot quietly produce a malformed value.

It is set per expression source on an Attribute Flow, in the portal beside the expression itself, and applies to import and export mappings alike. JIM works out which attributes the expression reads from the mv["..."] and cs["..."] accessors in it; you do not list them. An input counts as missing when the object has no value for it, when the value is null, or when it is an empty string.

Behaviour What happens when an input is missing Choose it when
Evaluate anyway (default) The expression runs with the input absent and contributes whatever it returns; points 1 to 4 apply in full. The expression handles absence itself, with IIF(), Coalesce() or an IsNullOrEmpty check.
Contribute no value The expression is not evaluated. The mapping contributes nothing, resolved by Attribute Priority and "Null is a value" like any other no-value outcome. Nothing is reported. The attribute is optional, and another contributor or the existing value should stand.
Fail this mapping The expression is not evaluated and an Expression Missing Input error is recorded against the object. Every other Attribute Flow on the Synchronisation Rule still runs, and the target attribute keeps whatever it already held. The attribute matters enough to investigate, but not enough to hold up the rest of the object.
Fail the object The expression is not evaluated and nothing at all flows for the object, which is recorded as an Expression Missing Input error. The value is identity-critical, such as a Distinguished Name or an account name, and a partially populated object is worse than none.

Both failure behaviours report against the object on every run until the missing value is supplied or the configuration changes, so the errors are a work queue rather than a one-off notification. Neither behaviour writes anything: "Fail this mapping" leaves the target attribute exactly as it was, and "Fail the object" leaves the whole object untouched.

The default is Evaluate anyway on every existing and newly created mapping, so nothing changes until you choose otherwise. Guarding the expression and setting a behaviour are complementary rather than alternatives: guard where the absence is expected and you know the right answer, and set a behaviour where it is not.

// Guarded, so Evaluate anyway is correct: the expression knows what to do without an input
IIF(IsNullOrWhitespace(cs["Last Name"]), mv["Email"], Lower(cs["First Name"]) + "." + Lower(cs["Last Name"]) + "@company.com")

// Unguardable, so set Fail the object: there is no safe DN for a person with no Account Name
"CN=" + EscapeDN(mv["Account Name"]) + ",OU=Users,DC=company,DC=local"

Set it from PowerShell with New-JIMSyncRuleMapping -MissingInputBehaviour, or over the REST API with missingInputBehaviour on the mapping source. Change it on a mapping that already exists with Set-JIMSyncRuleMapping -MissingInputBehaviour, or PATCH /sync-rules/{id}/mappings/{mappingId}.

Summary

Expression evaluates to Stored as Effect on target
null (for example Trim() of an absent attribute) No value ❌ Clears any existing value
Whitespace only, Treat whitespace as no value on (default) No value ❌ Clears any existing value
Whitespace only, Treat whitespace as no value off Literal whitespace ✅ Stored (flagged (whitespace) in the portal)
Concatenation with a missing operand (for example a + "." + null) A real, possibly malformed value ✅ Stored as-is, bypassing the null/clear path
A non-empty value That value ✅ Stored

The "Effect on target" column describes an attribute with a single contributing Synchronisation Rule. Where several rules contribute, a "no value" outcome feeds Attribute Priority resolution instead of clearing outright (see point 1).

The rule of thumb: reference a possibly-absent attribute and you risk clearing the target; concatenate one and you risk writing malformed data. Guard with Coalesce(), IIF(), and the IsNullOrEmpty / IsNullOrWhitespace checks, set a Missing Input Behaviour (point 5) where there is no safe fallback, and test with sample data (including the missing-input case) before you deploy.

Common Scenarios

This section shows practical examples of expressions you will use when setting up identity synchronisation.

Enabling and Disabling User Accounts

Active Directory uses a userAccountControl attribute to control whether an account is enabled or disabled. The common values are:

Value Meaning
512 Normal account, enabled
514 Normal account, disabled
66048 Normal account, enabled, password never expires

Enable or disable based on employee status:

IIF(Eq(mv["Employee Status"], "Active"), 512, 514)

This reads as: "If the Employee Status is Active, set the value to 512 (enabled), otherwise set it to 514 (disabled)."

Check if an account is currently disabled:

IIF(HasBit(cs["userAccountControl"], 2), "Disabled", "Enabled")

Setting Account Expiration Dates

Active Directory stores expiration dates in a special format called FILETIME. JIM provides functions to convert between normal dates and this format automatically.

Set the account expiration from an employee's end date:

ToFileTime(mv["Employee End Date"])

Convert an AD expiration date back to a readable date:

FromFileTime(cs["accountExpires"])

Important notes:

  • If the date attribute is empty or missing, these functions return nothing (null), which asserts "no value" for the target: for a sole contributor any existing value is removed; see Nulls, Missing Inputs, and Whitespace
  • AD treats 0 and very large numbers as "never expires" -- FromFileTime() returns nothing for these values
  • ToFileTime() safely handles empty text, missing values, and invalid dates by returning nothing

Building Email Addresses

Lower(mv["First Name"]) + "." + Lower(mv["Last Name"]) + "@company.com"

For "Jane Smith", this produces: jane.smith@company.com

Building a Display Name with Title

Coalesce(mv["Title"], "") + " " + mv["First Name"] + " " + mv["Last Name"]

Coalesce uses the title if one exists, otherwise uses an empty string -- avoiding a "null" appearing in the output.

Building Distinguished Names (DNs)

When constructing LDAP distinguished names, always use EscapeDN() to handle special characters (commas, quotes, etc.) in names:

"CN=" + EscapeDN(mv["Display Name"]) + ",OU=Users,DC=domain,DC=local"

Without EscapeDN(), a name like "Smith, Jane" would break the DN because of the comma.

Generating Initials

Upper(Left(mv["First Name"], 1)) + Upper(Left(mv["Last Name"], 1))

For "Jane Smith", this produces: JS

Placing Users in Different OUs by Department

IIF(Eq(mv["Department"], "IT"),
    "OU=IT,OU=Users,DC=domain,DC=local",
    IIF(Eq(mv["Department"], "HR"),
        "OU=HR,OU=Users,DC=domain,DC=local",
        "OU=General,OU=Users,DC=domain,DC=local"))

This nests IIF functions to check multiple conditions: first IT, then HR, with a default of General for everyone else.

Adding a Prefix to Account Names

IIF(Eq(mv["Department"], "IT"), "tech-" + mv["Account Name"], mv["Account Name"])

IT department users get a "tech-" prefix; everyone else keeps their account name as-is.

Splitting Delimited Values into a List

When a source system stores multiple values in a single field separated by a delimiter (e.g., "COURSE1|COURSE2|COURSE3"), use Split() to break them into separate values.

Example -- training courses from a CSV file:

The source CSV has pipe-separated courses in a single column:

employeeId,coursesCompleted
E001,"SOFT101|SOFT201|SEC101"

Use this expression to create individual values on the Metaverse Object:

Split(cs["coursesCompleted"], "|")

This creates three separate values: SOFT101, SOFT201, and SEC101.

Notes:

  • Empty entries are automatically removed
  • Whitespace is trimmed from each value
  • Returns nothing for missing or empty input
  • The delimiter can be any text (e.g., ",", "|", ";")

Combining a List into a Single Value

When exporting to a system that does not support multi-valued attributes, use Join() to combine them into one delimited value.

Join(mv["Group Memberships"], "|")

If the attribute has the values Admin, Users, and Developers, this produces: Admin|Users|Developers

Notes:

  • Returns nothing for empty lists
  • Empty and missing values are filtered out
  • Uses a comma as the default delimiter if none is specified

Validation and Troubleshooting

Testing an expression

The Attribute Flow dialog tests an expression as you write it, so you find out what it produces before you save it rather than from the next synchronisation.

Choose Expression as the source type and type your expression. JIM reads the attributes the expression references and offers a box for each one, labelled as the expression addresses it (cs["givenName"], mv["Display Name"]). Fill in sample values, choose Run test, and the output appears below with the type it evaluated to.

Leave a box empty to see what happens when the object has no value for that attribute. An empty box is passed to the expression as no value, not as an empty string, so this is how you check an expression against the hazard the previous section describes:

Lower(cs["givenName"]) + "." + Lower(cs["sn"]) + "@corp.local"

With both values supplied this produces ada.lovelace@corp.local. Clear the surname box and it produces ada.@corp.local: a syntactically fine string that is not an email address, which nothing downstream can tell apart from a good one. Seeing that before saving is the point.

The same evaluation is available outside the portal, over Test-JIMExpression and the REST API, for testing an expression from a script.

Validation

JIM validates expressions when you save a Synchronisation Rule. If an expression has a syntax error, you will see an error message indicating what went wrong and where in the expression the problem is. The tester reports the same errors, so a malformed expression is caught while you are writing it.

Common errors:

  • Missing closing quote or bracket
    Check that every " and ( has a matching pair.
  • Unknown function name
    Check the spelling; function names are case-sensitive.
  • Wrong number of parameters
    Check the function reference above for the correct parameters.
  • Syntax errors
    Look for misplaced operators or missing commas between function parameters.

Troubleshooting

When an expression does not produce the result you expect:

  1. Check the attribute name spelling
    Attribute names are matched case-insensitively, so mv["department"] and mv["Department"] are equivalent; casing is never the cause. Do check the name is spelled correctly and matches an attribute that exists in the JIM admin UI.
  2. Use Eq() for text comparisons
    Using == for text is a common mistake (see String Comparison).
  3. Check for missing values
    If an attribute does not exist on the object, it returns nothing (null), which can clear the target or, in a concatenation, produce a malformed value. Use Coalesce() or IsNullOrEmpty() to handle this; see Nulls, Missing Inputs, and Whitespace for the full picture.
  4. Test with sample data
    Use the tester in the Attribute Flow dialog to try your expression with sample values, including leaving an input empty to see what an object without that attribute would produce (see Testing an expression).
  5. Check the worker logs
    If expressions fail during sync, the worker service logs the error details.

Best Practices

  1. Always use Eq() for text comparisons
    The == operator can give incorrect results when comparing attribute values.

  2. Handle missing values
    Use Coalesce() to provide a fallback, or IsNullOrEmpty() to check before using a value. This prevents an expression clearing the target or writing malformed data when an attribute has not been populated yet; see Nulls, Missing Inputs, and Whitespace.

  3. Always use EscapeDN() in distinguished names
    This prevents special characters in names from breaking LDAP paths.

  4. Test before saving
    Use the expression test feature in the UI to verify your expressions with sample data.

  5. Keep expressions simple
    If an expression is getting complex, consider splitting the logic across multiple attribute mappings.

  6. Document complex expressions
    Add a note in the Synchronisation Rule's description explaining what complex expressions do, so the next person can understand them.