diff --git a/site/guide/inventory/_example-next-review-date-and-days-remaining.qmd b/site/guide/inventory/_example-next-review-date-and-days-remaining.qmd
index d469087fd..0cd054d69 100644
--- a/site/guide/inventory/_example-next-review-date-and-days-remaining.qmd
+++ b/site/guide/inventory/_example-next-review-date-and-days-remaining.qmd
@@ -11,15 +11,12 @@ SPDX-License-Identifier: AGPL-3.0 AND ValidMind Commercial -->
Combining different inventory field types provides a flexible way to automatically track record review schedules. This example creates fields that calculate the next review date based on approval date and validation frequency, adjusted by risk tier, and then computes the days remaining until that review.
-Common date and time field types available in your formulas include:
+Built-in helper functions for working with dates include:
-- `vm_today` — Today's date (updates each time the formula runs)
-- `date` — Python's `datetime.date` class
-- `datetime` — Python's `datetime.datetime` class
-- `timedelta` — Duration in days, seconds, or microseconds
-- `relativedelta` — Duration in months, years, etc. (from `dateutil`)
-
-Here, we show you how to use `date`, `relativedelta`, and `vm_today`.
+- `today()` — Today's date as an ISO string (`"YYYY-MM-DD"`).
+- `safe_parse_date(value)` — Parses ISO dates/datetimes or millisecond/second epoch timestamps into an ISO string.
+- `add_months(date, n)` — Adds `n` months to an ISO date.
+- `days_between(a, b)` — Returns the number of days between two ISO dates.
#### Calculate the next review date
@@ -34,14 +31,14 @@ Determine the next review date based on an approval date and a frequency of vali
- Yearly
3. Create a `Risk Tier` calculation field that depends on the frequency of validation:
-
+
```python
def formula(params):
- if params.frequencyOfValidation == "Weekly":
+ if params["frequencyOfValidation"] == "Weekly":
return "Tier 1"
- elif params.frequencyOfValidation == "Monthly":
+ elif params["frequencyOfValidation"] == "Monthly":
return "Tier 2"
- elif params.frequencyOfValidation == "Yearly":
+ elif params["frequencyOfValidation"] == "Yearly":
return "Tier 3"
else:
return "N/A"
@@ -51,42 +48,24 @@ Determine the next review date based on an approval date and a frequency of vali
```python
def formula(params):
- """
- Calculate the next review date based on the approval date and review frequency.
-
- Args:
- params.dmApprovedDate (str): The approval date in 'YYYY-MM-DD' format.
- params.dmRiskTier (string): The review tier (Tier 1, Tier 2, or Tier 3).
-
- Returns:
- datetime: The next review date.
- """
- # Guard against empty dates
- if params.dmApprovedDate == "":
+ """Return the next review date based on the approved date and risk tier."""
+ if params["dmApprovedDate"] == "":
return "N/A"
-
- # Convert the approved_on date string to a datetime object
- approved_date = date.fromtimestamp((int(params.dmApprovedDate))/1000)
-
- # Define the review frequency mapping
+
+ approved_date = safe_parse_date(params["dmApprovedDate"])
+
review_frequency = {
- "Tier 1": relativedelta(months=3), # Quarterly
- "Tier 2": relativedelta(months=6), # Semi-annually
- "Tier 3": relativedelta(years=1), # Annually
+ "Tier 1": 3, # Quarterly
+ "Tier 2": 6, # Semi-annually
+ "Tier 3": 12, # Annually
}
-
- # Get the appropriate time delta based on the tier
- frequency = review_frequency.get(params.dmRiskTier)
- if not frequency:
- # "Invalid tier. Must be Tier 1, Tier 2, or Tier 3."
+
+ months = review_frequency.get(params["dmRiskTier"])
+ if not months:
return "N/A"
-
- # Calculate the next review date
- next_review_date = approved_date + frequency
- return next_review_date.isoformat()
- ```
-{fig-alt="A screenshot showing the screen for adding a calculation type field that automatically calculates the next review date" width=90% .screenshot}
+ return add_months(approved_date, months)
+ ```
You can now determine the next review date in workflows by making a workflow depend on `Approved Date`. To test, change the `Approved Date` after the fact and see how `Next Review Date` changes.
@@ -96,28 +75,14 @@ You can now determine the next review date in workflows by making a workflow dep
```python
def formula(params):
- """
- Calculate days remaining until the next review.
-
- Args:
- params.nextReviewDate (str): The next review date in ISO format.
-
- Returns:
- str: Days remaining until review (example:"45 days remaining").
- """
- # Get next review date (stored as ISO format string)
- next_review_date = getattr(params, "nextReviewDate", "")
+ """Return the number of days until the next review date."""
+ next_review_date = params.get("nextReviewDate", "")
if not next_review_date:
return "Not applicable"
- next_review = date.fromisoformat(next_review_date)
-
- # Calculate days until review using built-in vm_today
- difference = next_review - vm_today
- return f"{difference.days} days remaining"
- ```
-
-{fig-alt="A screenshot showing the screen for adding a calculation type field that automatically calculates the days remaining until the next review" width=90% .screenshot}
+ difference = days_between(next_review_date, today())
+ return "%d days remaining" % difference
+ ```
You can now check the number of days remaining until the next review.
:::
diff --git a/site/guide/inventory/_field-types.qmd b/site/guide/inventory/_field-types.qmd
index 619e33d3d..df5e65c8f 100644
--- a/site/guide/inventory/_field-types.qmd
+++ b/site/guide/inventory/_field-types.qmd
@@ -66,11 +66,35 @@ Attachments
::::
Calculation
-: Define a `formula(params)` function that automatically calculates and returns a read-only value based on the params dictionary, which includes selected custom field keys retrieved from your other inventory record fields.
+: Define a `formula(params)` function that reads field values from the `params` dictionary and returns a read-only value. Formulas are written in Starlark^[[Starlark](https://github.com/bazelbuild/starlark)], a small, sandboxed scripting language with a Python-style syntax and a set of built-in helper functions for working with dates, numbers, and lists.
+
+::: {.callout title="A note on calculated fields"}
+{{< var vm.product >}} runs calculated field formulas on the Starlark formula engine. Formulas authored on earlier releases were written in Python and have been migrated to Starlark automatically. When creating or editing formulas, you now must use the Starlark syntax.
+:::
+
+##### Available helpers
+
+Reference these helpers in your formulas — they cover the date, number, and list operations the engine does not expose directly:
+
+| Helper | Returns | Description |
+|---|---|---|
+| `today()` | `"YYYY-MM-DD"` | Current date as an ISO string. |
+| `safe_parse_date(value, default=None)` | `"YYYY-MM-DD"` or `default` | Parses ISO dates/datetimes or millisecond/second epoch timestamps. |
+| `to_iso(value)` | `"YYYY-MM-DD"` or `None` | Alias for `safe_parse_date` with no default. |
+| `add_days(date, n)` | `"YYYY-MM-DD"` or `None` | Adds `n` days to an ISO date. |
+| `add_months(date, n)` | `"YYYY-MM-DD"` or `None` | Adds `n` months to an ISO date. |
+| `days_between(a, b)` | `int` or `None` | Days between two ISO dates (`a` − `b`). |
+| `days_since(date)` | `int` or `None` | Days from `date` to today. |
+| `months_between(a, b)` | `int` or `None` | Full-month difference between two ISO dates. |
+| `get_year(date)` / `get_month(date)` / `get_day(date)` | `int` or `None` | Components of an ISO date. |
+| `safe_int(value, default=0)` | `int` | Tolerates strings and missing values. |
+| `safe_float(value, default=0.0)` | `float` | Tolerates strings and missing values. |
+| `list_count(list, value)` | `int` | Number of times `value` appears in `list`. |
+| `zfill(value, width)` | `string` | Zero-pads `value` to `width` characters. |
:::: {.content-visible when-format="html" when-meta="includes.inventory"}
1. Select from the drop-down of **[available record fields]{.smallcaps}** to allow your formula access to the field's values.^[Fields are grouped by field type.]
-2. Replace the demonstration formula with your own in the code box provided.^[**Stick to basic operations**.
Keep your code simple and avoid complex logic and imports.]
+2. Replace the demonstration formula with your own in the code box provided.^[Reference fields with dictionary-style access — `params["fieldKey"]` — and use the available helpers for date and number work.]
3. Click **Test Calculation {{< fa angle-down >}}** to open the testing area.
4. Enter sample values in the testing area then click **{{< fa play >}} Test Calculation** to validate your formula.
@@ -80,38 +104,29 @@ Calculation
### Example — Risk tier calculation
-You have numeric inventory fields of `materiality` and `complexity`, where a larger value indicates a lower risk.
+You have inventory fields named `materiality` and `complexity` that classify a record's risk.
##### Example formula
-You want a calculated field that automatically returns a risk score based on `materiality` and `complexity`:
+A calculated field that returns a combined risk tier based on `materiality` and `complexity`:
```python
- def formula(params):
- # High Risk: If materiality is high risk, return high risk regardless of complexity
- if params.materiality == "High Risk":
- return "High Risk"
- # Medium Risk: If materiality is low risk but complexity is high risk, return medium risk
- if params.materiality == "Low Risk" and params.complexity == "High Risk":
- return "Medium Risk"
- # Low Risk: Both materiality and complexity are low risk
- return "Low Risk"
+def formula(params):
+ if params["materiality"] == "High Risk":
+ return "High Risk"
+ if params["materiality"] == "Low Risk" and params["complexity"] == "High Risk":
+ return "Medium Risk"
+ return "Low Risk"
```
-##### Example Calculation field configuration
-
-Your calculated field is grouped under `Model Risk` inventory fields, and can only be manually overridden by that record's Validators:
-
-{fig-alt="A screenshot showing the screen for adding a calculation type field that automatically calculates risk tier" width=90% .screenshot}
-
:::
:::
::::
:::: {.content-visible when-format="html" when-meta="includes.artifacts"}
-1. Select from the drop-down of **[available artifact fields]{.smallcaps}** and **[record fields available via]{.smallcaps} `params.model`** to allow your formula access to the field's values.^[Fields are grouped by field type.]
-2. Replace the demonstration formula with your own in the code box provided.^[**Stick to basic operations**.
Keep your code simple and avoid complex logic and imports.]
+1. Select from the drop-down of **[available artifact fields]{.smallcaps}** and **[record fields available via]{.smallcaps} `params["model"]`** to allow your formula access to the field's values.^[Fields are grouped by field type.]
+2. Replace the demonstration formula with your own in the code box provided.^[Reference fields with dictionary-style access — `params["fieldKey"]` and `params["model"]["fieldKey"]` for parent inventory record data — and use the available helpers for date and number work.
4. Click **Test Calculation {{< fa angle-down >}}** to open the testing area.
5. Enter in sample values in the testing area then click **{{< fa play >}} Test Calculation** to validate your formula.
@@ -121,10 +136,10 @@ Your calculated field is grouped under `Model Risk` inventory fields, and can on
### Example — Use artifact types in your formulas to create dynamic calculations
-Use simple dot notation to include [artifact types](/guide/validation/manage-artifact-types.qmd){target="_blank"}:
+The [artifact type](/guide/validation/manage-artifact-types.qmd){target="_blank"} is exposed under `params["finding_type"]` so you can branch on it:
-- `finding_type.tag` — Technical tag: `VALIDATION_ISSUE`
-- `finding_type.name` — Human-readable name: `Validation Issue`
+- `params["finding_type"]["tag"]` — Technical tag: `VALIDATION_ISSUE`
+- `params["finding_type"]["name"]` — Human-readable name: `Validation Issue`
##### Example use cases
@@ -138,44 +153,39 @@ Use simple dot notation to include [artifact types](/guide/validation/manage-art
#### Severity calculation
```python
-# Calculate different severity scores based on artifact type
-
-if finding_type.tag == "VALIDATION_ISSUE":
- return severity_score * 2.5
-elif finding_type.tag == "POLICY_EXCEPTION":
- return severity_score * 1.0
-else:
- return severity_score * 1.8
+def formula(params):
+ if params["finding_type"]["tag"] == "VALIDATION_ISSUE":
+ return params["severity_score"] * 2.5
+ elif params["finding_type"]["tag"] == "POLICY_EXCEPTION":
+ return params["severity_score"] * 1.0
+ else:
+ return params["severity_score"] * 1.8
```
#### Due date calculation
```python
-# Dynamically calculate due dates based on artifact type
-
-base_days = 30
-if finding_type.tag == "VALIDATION_ISSUE":
- urgency_multiplier = 0.5 # 15 days
-elif finding_type.tag == "MODEL_LIMITATION":
- urgency_multiplier = 2.0 # 60 days
-else:
- urgency_multiplier = 1.0 # 30 days
-
-return base_days * urgency_multiplier
+def formula(params):
+ base_days = 30
+ if params["finding_type"]["tag"] == "VALIDATION_ISSUE":
+ urgency_multiplier = 0.5
+ elif params["finding_type"]["tag"] == "MODEL_LIMITATION":
+ urgency_multiplier = 2.0
+ else:
+ urgency_multiplier = 1.0
+ return base_days * urgency_multiplier
```
#### Custom fields & parent record data
```python
-# Combine artifact types with custom fields and parent record data
-
-risk_factor = custom_field_risk_score or 1.0
-model_criticality = params.model.get("criticality_level", "medium")
-
-if finding_type.tag == "VALIDATION_ISSUE" and model_criticality == "high":
- return risk_factor * 3.0
-else:
- return risk_factor * 1.5
+def formula(params):
+ risk_factor = params["custom_field_risk_score"] or 1.0
+ model_criticality = params["model"].get("criticality_level", "medium")
+ if params["finding_type"]["tag"] == "VALIDATION_ISSUE" and model_criticality == "high":
+ return risk_factor * 3.0
+ else:
+ return risk_factor * 1.5
```
:::
@@ -251,9 +261,9 @@ Attachments
: Upload supporting files for your [record](/guide/inventory/edit-inventory-fields.qmd#manage-attachments){target="_blank"} or [artifact](/guide/validation/add-manage-artifacts.qmd#manage-attachments){target="_blank"}. Files must be less than 50 MB each in size.
Calculation
-: Define a `formula(params)` function that automatically calculates and returns a read-only value based on the params dictionary, which includes selected custom field keys retrieved from your other inventory record fields.
+: Define a `formula(params)` function that reads field values from the `params` dictionary (`params["fieldKey"]`) and returns a read-only value. Formulas are written in Starlark and have access to a set of built-in helpers for dates, numbers, and lists.
- 1. Select from the drop-down of **[available record fields]{.smallcaps}**, or **[available artifact fields]{.smallcaps}** and **[record fields available via]{.smallcaps} `params.model`** (artifact fields) to allow your formula access to the field's values.
+ 1. Select from the drop-down of **[available record fields]{.smallcaps}**, or **[available artifact fields]{.smallcaps}** and **[record fields available via]{.smallcaps} `params["model"]`** (artifact fields) to allow your formula access to the field's values.
2. Replace the demonstration formula with your own in the code box provided.
4. Click **Test Calculation {{< fa angle-down >}}** to open the testing area.
5. Enter in sample values in the testing area then click **{{< fa play >}} Test Calculation** to validate your formula.