Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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"
Expand All @@ -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()
```

![Adding a calculation type field that automatically calculates the next review date](calculation-field-next-review-date.png){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.

Expand All @@ -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"
```


![Adding a calculation type field that automatically calculates the days remaining until the next review](calculation-field-days-remaining.png){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.
:::
Expand Down
118 changes: 64 additions & 54 deletions site/guide/inventory/_field-types.qmd
Original file line number Diff line number Diff line change
Expand Up @@ -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**.<br>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.
<br><br>
Expand All @@ -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:

![Adding a calculation type field that automatically calculates risk tier](calculation-field.png){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**.<br>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.
<br><br>
Expand All @@ -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

Expand All @@ -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
```

:::
Expand Down Expand Up @@ -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.
Expand Down
Loading