<a id="set-property-value-expressions"></a>
<a id="id1"></a>
# Set Property Value expressions

An expression is a small formula that TrialGrid works out when the Action runs. It always sets the property: whatever the expression produces is written, and if the expression cannot be worked out — for example arithmetic on a property that is blank — the property is cleared and a warning is recorded against the object, so a broken expression is never silent.

Reference, Set and User properties cannot be the target of an expression. See [Operations](operations.html.md#action-operations) for why a User property cannot be set by an Action at all.

The following functions and values are available.

| Written as | Meaning |
| --- | --- |
| `today()` | The current date (server time). Use it in a Date expression. |
| `now()` | The current date and time (UTC). Use it in a DateTime expression. |
| `days(n)`, `weeks(n)` | A span of whole days or weeks, added to or subtracted from a date or datetime. |
| `hours(n)`, `minutes(n)`, `seconds(n)` | A span of time. Only meaningful with a datetime — added to a date they have no effect (a date counts whole days only). |
| `months(n)`, `years(n)` | A span of whole months or years. These *clamp* to a real date: `months(1)` added to 31 January gives 28 February, not an impossible 31 February. For an exact number of days use `days(28)` rather than `months(1)`. |
| `to_date(x)` | Converts a value to a **date**. Applied to a datetime it gives that datetime's calendar day **in UTC**, so `to_date(object.attribute("Collected At"))` extracts the day from a date-and-time property. It also parses a plain `"2026-07-21"` string. A blank value stays blank (clears the property). |
| `to_datetime(x)` | Converts a value to a **date and time**, always in UTC. A plain date becomes midnight UTC; a string carrying an offset or a `Z` is converted to UTC. A blank value stays blank (clears the property). |
| `str(x)` | Converts a value to text, so a date, time or number can be joined into a text property — e.g. `object.attribute("Audit") + " at " + str(now())`. A blank value becomes an empty string. |
| `user` | The person running the Action. Write it on its own to set a User property to them, or use `user.username`, `user.email` or `user.full_name` to put their details into a text property. `full_name` falls back to the username when no name is set.<br>Setting a User property to someone who does not work in this Organization is refused when the Action runs. As with any expression that cannot produce a value, the property is emptied and a warning is shown against the object, so check the warnings after a run. |
| `object.attribute("Name")` | The value of another of this object's custom properties, named exactly as it appears. A Reference property returns the referenced object's id, and a User property returns the chosen person's user id, not their name. |
| `object.has_label("Name")` | True when this object carries the named label, False otherwise (the name is matched without regard to case). Use it in a condition, e.g. `"Locked" if object.has_label("Locked") else "Open"`. The label must be one defined in this URL. |
| `default(value, fallback, ...)` | The first argument that is not blank. Useful for seeding, e.g. `default(object.attribute("Version"), 0) + 1`. More fallbacks can be chained: `default(object.attribute("Preferred"), object.attribute("Backup"), "None")`. |

<a id="blank-values-and-conditions"></a>
## Blank values and conditions

A blank Date or Number has *no* value — it is not treated as zero or as any particular date — so doing arithmetic on a blank (`object.attribute("Start Date") + days(10)` when *Start Date* is empty) is an error, and the property is cleared with a warning. To guard against a blank, use a conditional so the arithmetic only runs when there is a value:

```
object.attribute("Start Date") + days(10) if object.attribute("Start Date") else today()
```

The `if ... else ...` conditional is the way to guard arithmetic. `default()` only chooses between plain values — `default(object.attribute("X") + days(10), today())` still fails on a blank *X*, because the arithmetic is worked out before `default` chooses.

You can compare and combine values: `==` and `in` compare text without regard to case (`object.attribute("Status") == "open"` matches a stored *Open*), and `+` joins text (`object.attribute("Audit") + "\n" + user.full_name` appends a line to a long-text property).

<a id="dates-and-times"></a>
## Dates and times

**All dates and times are in UTC.** `now()`, `today()`, and every stored date/datetime are UTC, so a date never shifts under a timezone.

A Date property expects an actual date and a DateTime property an actual date-and-time — the expression result is *not* guessed into shape. `today() + days(10)` and `object.attribute("Enrolment Date") + weeks(6)` produce a date, so they write straight into a Date property; a datetime written into a Date property (or a plain `"2026-07-21"` string) is refused rather than silently trimmed. When you need to change one into the other, convert it explicitly:

- `to_date(x)` gives the date. On a datetime it takes the **UTC** calendar day, so `to_date(object.attribute("Collected At"))` writes the day a date-and-time was collected into a Date property. A datetime near midnight lands on the correct UTC day rather than the local one.
- `to_datetime(x)` gives a UTC date-and-time. A plain date becomes midnight UTC.

A blank input to either stays blank, so `to_date(object.attribute("Collected At"))` clears the target when nothing was collected rather than erroring.

<a id="numbers-and-precision"></a>
## Numbers and precision

A numeric result must fit the property's configured number of decimal places — it is **not** silently rounded. A result of `2.6` written to a property that allows no decimal places clears the property and records a warning rather than storing `3`, so a value is never quietly changed. If you want rounding, make the expression produce a value that already fits (for example keep the property's decimal places, or compute a whole number). Trailing zeros are not precision, so `2.00` still fits a whole-number property.

<a id="working-with-a-property-value"></a>
## Working with a property value

Most of the time you use `object.attribute("Name")` directly — in arithmetic, a comparison, or a concatenation — and it behaves as the right kind of value automatically. When you need to be explicit, the value offers these:

| Written as | Result |
| --- | --- |
| `object.attribute("Name").raw_value()` | The value **exactly as stored**, as text, with no conversion — a number stays `"5.0"`, a date stays `"2026-07-21"`. Use this when you want the stored text rather than a typed value. |
| `object.attribute("Name").is_empty()` | `True` when the property is unset or blank, `False` otherwise. The reliable way to test for a blank before doing arithmetic on a Date or Number. |

So the blank guard above can also be written to say plainly what it checks:

```
object.attribute("Start Date") + days(10) if not object.attribute("Start Date").is_empty() else today()
```

Two things to watch:

- **These are functions — write the parentheses.** `.is_empty()` tests emptiness; `.is_empty` (no parentheses) is *always* treated as true, so the check silently never fires.
- `.raw_value()` gives a property's own stored text; use `str(x)` to turn any other value — a date, time or number, such as `str(now())` — into text for joining into a text property.

The plain `if object.attribute("X") ...` short form (without `.is_empty()`) means "is set" for most properties, but for a Yes/No (Boolean) property it means "is Yes". Use `not object.attribute("X").is_empty()` when you specifically mean "has a value" on a Boolean.

<a id="renaming-or-deleting-a-property-an-expression-reads"></a>
## Renaming or deleting a property an expression reads

A property named in an expression is protected so an expression cannot be orphaned without you knowing:

- **Renaming** a property that an expression reads rewrites the expression to the new name automatically, so it keeps working — you do not have to hunt down and edit every expression.
- **Deleting** a property that an expression reads is **refused**: the delete tells you which Actions read it, so you can change or remove those expressions first. (Deleting a property that an operation merely *sets* is allowed — that operation is simply removed, since it can no longer do anything.)
- **Changing the type** of a property an expression reads is refused if the new type would stop the expression evaluating — for example turning a Number that an expression does arithmetic on into Text.

<a id="worked-examples"></a>
## Worked examples

| Expression | Result |
| --- | --- |
| `today() + days(10)` | A due date ten days from today. |
| `object.attribute("Enrolment Date") + weeks(6)` | Six weeks after an enrolment date held on the object. |
| `object.attribute("Version") + 1` | Increments a numeric version each time the Action runs. |
| `to_date(object.attribute("Collected At"))` | Copies the UTC day of a date-and-time property into a Date property. |
| `object.attribute("Audit") + user.full_name + " at " + str(now()) + "\n"` | Appends a "who and when" line to a text audit property each time the Action runs. |
| `"#ff0000" if object.attribute("Overdue") == "Yes" else "#00ff00"` | Red when overdue, green otherwise (into a Color property). |
| `"Locked" if object.has_label("Locked") else "Editable"` | Sets a status text from whether the object carries the *Locked* label. |

<a id="testing-an-expression"></a>
## Testing an expression

The **Test** button evaluates the expression there and then and shows the result, without saving anything or changing any object. It runs against *sample* values, not real data: `now()` and `today()` are a fixed sample instant, `user` is a sample user, and each `object.attribute("…")` is filled with a value of the right type for that property — a number is `1`, a date is a sample date, a choice is its first option, and so on.

So Test confirms two things: the expression is valid, and what *kind* of value it produces (and whether that value fits the property). It does **not** predict which branch a conditional will take for a real object — because every sample number is `1`, `object.attribute("A") >= object.attribute("B")` is true under Test regardless of what A and B will actually hold. Treat the shown result as "this is a valid value of the right type", not as the value a particular object will get.
