> ## Documentation Index
> Fetch the complete documentation index at: https://docs.lumos.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Using the Lumos Expression Language in Workflows

## Background

When configuring workflows to automatically create and update access through Lumos, you may find yourself needing to reference attributes from the target user.

For example:

* When creating a username (setting it to `first_name.last_name`)
* When assigning custom attributes in apps like Slack or Okta (`team`, `title`, `manager`)
* When scheduling an action relative to a user's start date (e.g. 2 days before)
* When updating a last name (ensuring names remain aligned with values from the HRIS)

The Lumos Expression Language (LEL) allows you to reference user attributes, perform transformations, and build conditional logic directly within your workflows.

**Important:** LEL uses its own syntax. It is *not* Python, JavaScript, or any other general-purpose programming language. Standard library functions like `timedelta`, `isoformat()`, or `datetime` are **not available**. This guide covers every function and operator that LEL supports.

***

## What You Can Do With Expression Language

With Lumos Expression Language, you can:

* Reference attributes from user identities in your workflows
* Combine identity attributes with static text
* Transform data with built-in functions (lowercase, uppercase, split, etc.)
* Perform date arithmetic (add or subtract days/hours from dates)
* Create conditional logic to handle different scenarios
* Generate random values for temporary passwords or unique identifiers
* Convert non-Latin characters to ASCII equivalents
* Normalize country names to standard ISO codes
* Skip setting an attribute entirely when a condition isn't met

***

## Syntax Basics

### Referencing Identity Attributes

To reference an attribute from the user in your workflow, use the `user` object:

```
user.email
```

This returns the user's email address. You can access any of the standard attributes listed in the Available Attributes section below.

### The Pipe Operator (`|`)

LEL supports two ways to apply transformations: the **pipe operator** and **method-call syntax**. Both are equivalent — use whichever you find more readable.

```
# Pipe syntax
user.given_name | lower

# Method syntax
user.given_name.lower()

# Both produce the same result
```

For functions that take arguments:

```
# Pipe syntax
user.start_date | add_days(3)

# Function syntax
add_days(user.start_date, 3)
```

### String Concatenation

Use the `+` or `~` operator to combine values together. Both work the same way.

```
user.given_name + "." + user.family_name
```

### Static Text

Wrap static text in single or double quotes:

```
user.given_name + '.' + user.family_name + '@company.com'
```

***

## Available Attributes

### Standard Attributes

These can be accessed directly on the `user` object:

| **Attribute** | **Type** | **Description** |
| - | - | - |
| `user.email` | string | Email address |
| `user.given_name` | string | First name |
| `user.family_name` | string | Last name |
| `user.full_name` | string | Full display name |
| `user.title` | string | Job title |
| `user.team` | string | Team or department |
| `user.status` | string | Lifecycle status |
| `user.start_date` | datetime | Employment start date |
| `user.end_date` | datetime | Employment end date |
| `user.manager` | object | Manager identity (has the same attributes as `user` — e.g., `user.manager.email`) |
| `user.stable_identifier` | string | Unique stable identifier |
| `user.source_app` | object | Source app info (e.g., `user.source_app.user_friendly_label`) |

### Custom Attributes

Custom attributes defined in your Lumos settings are accessed via the `custom_attributes` field:

```
user.custom_attributes["preferred_name"]
user.custom_attributes["cost_center"]
user.custom_attributes["Employee ID"]
```

### Groups

If your workflow context includes group information:

```
user.groups[0].name        # Name of the first group
user.groups | len          # Number of groups
```

***

## Operators

### Comparison Operators

| **Operator** | **Description** | **Example** |
| - | - | - |
| `==` | Equal to | `user.team == 'Engineering'` |
| `!=` | Not equal to | `user.status != 'active'` |
| `>` | Greater than | `len(user.given_name) > 2` |
| `<` | Less than | `len(user.email) < 50` |
| `>=` | Greater than or equal | — |
| `<=` | Less than or equal | — |

### Logical Operators

| **Operator** | **Description** | **Example** |
| - | - | - |
| `and` | Both conditions must be true | `user.team == 'Eng' and user.title != 'Intern'` |
| `or` | At least one condition must be true | `user.custom_attributes["preferred_name"] or user.given_name` |
| `not` | Negates a condition | `not user.end_date` |

### String Operators

| **Operator** | **Description** | **Example** |
| - | - | - |
| `+` | Concatenation | `user.given_name + ' ' + user.family_name` |
| `~` | Concatenation (same as `+`) | `random_string(2, CHARSET_ALPHA_UPPER) ~ random_string(6, CHARSET_NUMERIC)` |

***

## Conditional Logic

### Inline If/Else

Use inline `if`/`else` to return different values based on a condition:

```
user.custom_attributes["preferred_name"] if user.custom_attributes["preferred_name"] else user.given_name
```

A more complex example with string concatenation:

```
(user.custom_attributes["preferred_name"] + "." + user.family_name)
if user.custom_attributes["preferred_name"]
else (user.given_name + "." + user.family_name)
```

### Fallbacks with `or`

For simple "use this value if available, otherwise use that value" logic, the `or` operator is more concise:

```
(user.custom_attributes["preferred_name"] or user.given_name) + "." + user.family_name
```

### Fallbacks with `default`

Use the `default` filter to provide a fallback value when an attribute might be missing or empty:

```
user.custom_attributes["nickname"] | default(user.given_name)
```

### Skipping an Attribute with `DO_NOT_SET`

Sometimes you want to conditionally skip setting a value entirely — rather than setting it to an empty string. Use the special `DO_NOT_SET` sentinel:

```
user.custom_attributes["office_code"] if user.custom_attributes["office_code"] else DO_NOT_SET
```

When the expression evaluates to `DO_NOT_SET`, Lumos will skip the attribute update for that user entirely.

***

## Data Transformations

### String Transformations

| **Transformation** | **Usage** | **Example Result** | |
| - | - | - | - |
| Lowercase | \`user.given\_name | lower`or`user.given\_name.lower()\` | `"John"` → `"john"` |
| Uppercase | \`user.given\_name | upper`or`user.given\_name.upper()\` | `"John"` → `"JOHN"` |
| Capitalize | \`user.given\_name | capitalize\` | `"john"` → `"John"` |
| Title Case | \`user.full\_name | title\` | `"john smith"` → `"John Smith"` |
| Trim | \`user.given\_name | trim\` | `" John "` → `"John"` |
| Truncate | \`user.email | truncate(50)\` | Caps value at 50 characters |
| Replace | \`user.email | replace('.', '')\` | Replaces all occurrences |
| Split | \`user.email | split('@')`or`user.email.split('@')\` | Returns an array: `["john", "company.com"]` |
| Join | \`\['a', 'b'] | join(',')\` | `"a,b"` |
| Index | `user.given_name[0]` | First character: `"J"` | |
| Slice | `user.given_name[0:3]` | First 3 characters | |
| Length | \`user.email | len`or`len(user.email)\` | Number of characters |
| To Integer | \`'42' | int`or`int('42')\` | Converts string to number |
| To String | \`random\_int(1, 99) | string\` | Converts number to string |
| First | \`user.groups | first\` | First element of an array |
| Last | \`user.groups | last\` | Last element of an array |

### Special Character Removal

`strip_punc` — Removes all punctuation characters from a string, including ASCII punctuation (`!@#$%^&*` etc.) and Unicode punctuation (fullwidth commas, curly quotes, Japanese brackets, etc.):

```
(user.given_name | strip_punc) + "." + (user.family_name | strip_punc) + "@company.com"
```

This would transform `Jean-Pierre O'Connor` into `jeanpierre.oconnor@company.com`.

`strip_chars` — Removes only the specific characters you define:

```
user.given_name | strip_chars("-'")
```

This would transform `Jean-Pierre` into `JeanPierre` (hyphens removed, all other characters kept).

### Transliteration

Convert non-Latin characters (Greek, Chinese, Cyrillic, etc.) to Latin/ASCII equivalents:

```
transliterate(user.given_name)
```

* `transliterate('Müller')` → `"Muller"`
* `transliterate('Μαρία')` → `"Maria"`
* `transliterate('张伟')` → `"zhangwei"`

You can also pass a custom transliteration identifier for more control:

```
transliterate(user.given_name, 'Any-Latin')
```

The default identifier (`Any-Latin; Latin-ASCII;`) converts from any script to ASCII.

### Base64 Encoding

Encode a value as Base64 for systems that require it:

```
user.email | base64_encode
```

`"john@example.com"` → `"am9obkBleGFtcGxlLmNvbQ=="`

***

## Date Arithmetic

LEL provides built-in filters for performing arithmetic on date values. These are the **only** way to manipulate dates in LEL — standard programming constructs like `timedelta` or `isoformat()` are not available.

### `add_days(n)`

Adds a number of days to a date. Use negative numbers to subtract.

```
user.start_date | add_days(3)       # 3 days after start date
user.start_date | add_days(-2)      # 2 days before start date
```

### `subtract_days(n)`

Subtracts a number of days from a date.

```
user.start_date | subtract_days(7)  # 7 days before start date
```

### `add_hours(n)`

Adds a number of hours to a date.

```
user.start_date | add_hours(9)      # Start date at 9:00 AM
```

### Combining Date Filters

You can chain date filters together:

```
user.start_date | subtract_days(2) | add_hours(9)
```

This produces the date 2 days before the start date, at 9:00 AM.

### Handling Missing Dates

All date filters gracefully handle missing dates — if the input date is `None`, the result is also `None` (no error). Combine with conditional logic if you need different behavior:

```
user.start_date | subtract_days(2) if user.start_date else DO_NOT_SET
```

### Current Timestamp

Get the current date and time with `now()`:

```
now()            # Returns a datetime object
now() | string   # Returns an ISO-format datetime string
```

***

## Country Lookup

Normalize a country name or code to standard ISO format using `lookup_country`:

```
lookup_country('United States').alpha_2    # → "US"
lookup_country('US').name                  # → "United States"
lookup_country('Brazil').alpha_3           # → "BRA"
```

Useful with custom attributes:

```
lookup_country(user.custom_attributes["country"]).alpha_2
```

***

## Generating Random Values

Lumos supports generating cryptographically secure random values, useful for temporary passwords, unique IDs, and access codes.

### `random_string(length, charset?)`

Generates a random string of the specified length. Defaults to alphanumeric characters if no charset is specified.

```
random_string(12)    # → e.g. "aX7kP2mR4nQz"
```

With a custom character set:

```
random_string(8, 'ABCDEFGHIJKLMNOPQRSTUVWXYZ')    # → e.g. "KXPMTBZA"
```

**Built-in charset constants:**

| **Constant** | **Characters** |
| - | - |
| `CHARSET_ALPHANUMERIC` | `a–z`, `A–Z`, `0–9` (default) |
| `CHARSET_ALPHA` | `a–z`, `A–Z` |
| `CHARSET_ALPHA_UPPER` | `A–Z` |
| `CHARSET_ALPHA_LOWER` | `a–z` |
| `CHARSET_NUMERIC` | `0–9` |
| `CHARSET_HEX` | `0–9`, `a–f` |

**Composing formats** — use `~` to concatenate multiple random\_string calls:

```
random_string(2, CHARSET_ALPHA_UPPER) ~ random_string(6, CHARSET_NUMERIC)
```

→ e.g. `"XY654321"` (2 uppercase letters + 6 digits)

**Maximum length:** 50 characters per `random_string` call.

### `random_int(min, max)`

Generates a random integer within an inclusive range.

```
random_int(1000, 9999)                # → e.g. 7421
random_int(100000, 999999) | string   # → e.g. "482917" (as a string)
```

**Note:** `random_string` and `random_int` generate a **new value every time** the expression is evaluated. They are ideal for temporary passwords or one-time codes, but not for values that need to be stable across runs.

***

## Common Recipes

### Generate a username from name

```
(user.given_name[0] + user.family_name).lower()
```

→ e.g. `"jsmith"`

### Generate an email with preferred name fallback

```
((user.custom_attributes["preferred_name"] or user.given_name) + "." + user.family_name).lower() + "@company.com"
```

### Extract handle from email

```
user.email.split('@')[0]
```

### Set a value 2 days before start date

```
user.start_date | subtract_days(2)
```

### Schedule for 9 AM, 2 days before start date

```
user.start_date | subtract_days(2) | add_hours(9)
```

### Create a clean email from names with special characters

```
((user.given_name | strip_punc) + "." + (user.family_name | strip_punc)).lower() + "@company.com"
```

### Transliterate non-Latin names for provisioning

```
(transliterate(user.given_name) + '.' + transliterate(user.family_name)).lower() + '@company.com'
```

### Conditionally set an attribute, or skip it

```
user.custom_attributes["office_code"] if user.custom_attributes["office_code"] else DO_NOT_SET
```

### Generate a temporary password

```
random_string(4, CHARSET_ALPHA_UPPER) ~ random_string(4, CHARSET_NUMERIC) ~ random_string(4, CHARSET_ALPHA_LOWER)
```

→ e.g. `"KXPM4821abcd"`

### Get the first 4 characters of an Employee ID (safely)

```
user.custom_attributes["Employee ID"][:4] if len(user.custom_attributes["Employee ID"]) >= 4 else user.custom_attributes["Employee ID"]
```

***

## FAQs

**What happens if an attribute I reference doesn't exist?**

If you reference an attribute that doesn't exist, the workflow step will fail and generate an Error Task in Lumos with details about the failure. Use the `default` filter or `if`/`else` logic to handle potentially missing attributes gracefully.

**Can I use expressions in all workflow actions?**

Yes. Expressions can be used in any input field for any workflow action that expects a "string" value.

**Why doesn't** `timedelta`**,** `isoformat()`**, or** `datetime` **work?**

LEL is not Python. It uses its own set of built-in functions. For date manipulation, use `add_days`, `subtract_days`, and `add_hours`. For converting a date to a string, use `| string`.

**Can I reference attributes from connected systems (like Okta or AzureAD)?**

Yes, but you must first store them as custom attributes on the Identity in Lumos to use them in provisioning workflows.

**How do I handle names with special characters when creating email addresses?**

Use `strip_punc` to remove all punctuation, or `strip_chars` with specific characters to remove only certain ones. For non-Latin scripts, use `transliterate`.

**Are randomly generated values the same each time a workflow runs?**

No — `random_string` and `random_int` generate a new value every time the expression is evaluated.

**Can I use** `random_string` **to generate secure passwords?**

Yes. Lumos uses Python's `secrets` module under the hood, which is cryptographically secure.

**What does** `DO_NOT_SET` **do?**

When an expression evaluates to `DO_NOT_SET`, Lumos skips the attribute update for that user entirely. This is different from setting the value to an empty string — `DO_NOT_SET` means "don't touch this attribute at all."

**How complex can my expressions be?**

While you can create fairly complex expressions combining multiple conditions and transformations, Lumos recommends keeping expressions reasonably simple for maintainability.

***

## Other Questions?

Contact [support@lumos.com](mailto:support@lumos.com) or your CSM.
