Custom Screens (Screen Meta Data)¶
A custom screen is the pop-up form a user sees when they open a risk action. It is defined entirely in JSON configuration — no code changes are required. Each screen consists of a config file (which links the screen to an operation and defines its lifecycle sub-statuses) and a layout file (which defines the groups, sections, and elements the user interacts with).
Prerequisite
You must have a risk action defined before creating a screen. The operationName in your screen config must exactly match an operations[].name in your action JSON. Read Anatomy of a Risk Action first.
File locations¶
config/{client}/screen-meta-data/
config/{bc}/ ← config files (one per screen)
files/{bc}/ ← layout files (one per screen)
The filename convention is not enforced by the platform, but the standard is {operation-name}.json for the config file and {operation-name}-meta-data.json for the layout file.
The config file¶
The config file links an operation to its layout and defines the sub-statuses used to track the action's lifecycle.
{
"operationName": "structured_quote",
"notRequiredSubStatus": "STRUCTURED_QUOTE_NOT_REQUIRED",
"initialSubStatus": "STRUCTURED_QUOTE_INITIATED",
"closingSubStatus": "STRUCTURED_QUOTE_COMPLETE",
"dataFile": "structured-quote-meta-data.json"
}
| Field | Required | Description |
|---|---|---|
operationName |
Yes | Must match the operation name in the risk action config exactly. A mismatch means Workbench cannot find the pop-up and the action will be broken. |
initialSubStatus |
Yes | Sub-status applied to the action instance when the user opens the screen for the first time. |
closingSubStatus |
Yes | Sub-status applied when the user saves/submits the screen successfully. |
notRequiredSubStatus |
Yes | Sub-status applied when the user marks the action as not required. |
dataFile |
Yes | Filename of the layout JSON file (relative to files/{bc}/). |
apiGet |
No | URL to fetch existing data when the screen opens. Use "riskValuesApi" to read from the platform's risk values store instead of a custom endpoint. |
apiPost |
No | URL to save data when the screen is submitted. Use "riskValuesApi" to write to the platform's risk values store. |
copiedToRiskTypes |
No | Array of risk types ("RENEWAL", "MTA") that should inherit this screen's saved data when the risk is renewed or adjusted. |
clientEndpoint |
No | Path prefix for client microservice calls (e.g. "/v1/fac-ri"). Used when apiGet/apiPost target your client microservice. |
Sub-status naming convention¶
Sub-statuses are free-form strings — there is no platform-enforced enum. Follow this convention:
Examples:
- STRUCTURED_QUOTE_INITIATED
- STRUCTURED_QUOTE_COMPLETE
- STRUCTURED_QUOTE_NOT_REQUIRED
Sub-statuses must be consistent with what is referenced in the risk action config's operations block. If the action config refers to a closingSubStatus that does not match the screen config, the action will not transition correctly.
apiGet and apiPost¶
When using custom endpoints:
- GET: Workbench calls
GET {underwritingApi}/generic-data/screen-data/{type}/{riskActionInstanceId}— no payload, returnsGenericData[]. - POST: Workbench calls
POST {underwritingApi}/generic-data/riskActionInstance/{riskActionInstanceId}/operation-name/{operationName}with this payload shape:
{
"data": { ...formValues },
"operationName": "structured_quote",
"riskActionInstanceId": 12345,
"save": true
}
When apiGet or apiPost is set to "riskValuesApi", the platform handles persistence automatically via the risk values store — no client microservice endpoint is needed.
The layout file¶
The layout file defines what the user sees. The structure is: groups → sections → elements.
[
{
"group": {
"key": "quote_details",
"name": "Quote Details"
},
"sections": [
{
"section": {
"key": "premium_section",
"name": "Premium"
},
"elements": [
{
"key": "premiumAmount",
"label": "Premium Amount",
"type": "currency-input",
"validation": [
{ "validator": "required" }
]
}
]
}
]
}
]
Groups¶
A group is the top-level container. It renders as a labelled card or panel in the UI.
| Field | Required | Description |
|---|---|---|
group.key |
Yes | Unique identifier for this group. |
group.name |
Yes | Display label shown in the UI. |
group.nameKey |
No | Localisation path (e.g. "generatedForm.quoteDetails.label"). Overrides name if the UI is running in multi-language mode. |
hideExpression |
No | JavaScript expression or function — hides the entire group when it evaluates to true. |
sections |
Yes | Array of sections inside this group. |
Sections¶
Sections subdivide a group. They can be plain (static) or repeatable (the user can add multiple rows).
| Field | Required | Description |
|---|---|---|
section.key |
Yes | Unique identifier for this section. |
section.name |
Yes | Display label. |
section.repeatable |
No | true for a standard repeatable section; "compact" for a compact row layout. |
section.maxLength |
No | Maximum number of rows in a repeatable section. Accepts a number or a data key whose value is the limit. |
section.repeatableConfig |
No | See Repeatable configuration below. |
section.alwaysUseDefault |
No | If true, always populates the section with the configured default values, ignoring any persisted data. |
hideExpression |
No | JavaScript expression — hides this section when true. Supports a { expression, key } object to evaluate against a specific risk data key. |
modifierClass |
No | One or more layout modifier classes. See Modifier classes. |
elements |
Yes | Array of elements inside this section. |
Elements¶
Elements are individual form fields. Every element requires a key, a type, and usually a label.
| Field | Required | Description |
|---|---|---|
key |
Yes | Unique identifier within the layout file. Used to reference the field in expressions, pipeline calls, and validators. |
type |
Yes | Element type — see full list below. |
label |
No | Display label shown to the user. |
hidden |
No | true to render the element but keep it invisible (useful for elements with computed default values). |
hideExpression |
No | JavaScript expression — hides the element when true. |
clearOnHide |
No | If true, clears the element's value when hideExpression causes it to hide. Without this, the value persists even when the field is not visible. |
disableExpression |
No | JavaScript expression — disables the element (read-only) when true. |
modifierClass |
No | One or more layout modifier classes. |
validation |
No | Array of validators — see Validators. |
defaultValue |
No | Default value configuration. |
optionList |
No | Option source for select-type elements — see Option sources. |
optionListParams |
No | Parameters for the option source (e.g. businessConstantType). |
repeatableConfig |
No | Configuration for repeatable type elements — see Repeatable configuration. |
repeatableElements |
No | Child elements rendered inside each row of a repeatable element. |
computationConfig |
No | Emitter/subscriber configuration for dynamic totals — see Dynamic computation. |
style |
No | Inline style overrides: { cellWidth, marginRight, width }. |
Element types¶
| Type | UI rendered |
|---|---|
text-input |
Single-line text field |
text-area |
Multi-line text field |
numeric-input |
Number input |
currency-input |
Currency number input (respects defaultDecimalPlaces.currency from app settings) |
percent-input |
Percentage input. Add percentageAsDecimal: true to store as a decimal (e.g. 0.15 instead of 15). |
date |
Date picker |
date-time |
Date and time picker |
date-duration |
Duration (e.g. number of months) |
date_range_field |
Date range picker (start + end) |
time |
Time picker |
utc-time |
Time picker (UTC-normalised) |
select |
Single-select dropdown |
multi-select |
Multi-select dropdown |
type-ahead-select |
Searchable single-select |
type-ahead-multi-select |
Searchable multi-select |
autocomplete |
Autocomplete text input |
autocomplete-multi-select |
Multi-value autocomplete |
radio-group |
Radio button group |
radio-selector |
Radio selector (card-style) |
checkbox |
Single checkbox |
checkbox-group |
Group of checkboxes |
toggle-switch |
Toggle switch |
repeatable |
Repeatable row group |
party |
Party search and selection |
party-contact |
Contact lookup linked to a party |
party-address-lookup |
Address lookup linked to a party |
address-lookup |
Standalone address lookup |
broker-contact |
Broker contact lookup |
file-upload |
File attachment field |
rich-text |
Rich text editor |
content-editable |
Inline editable content |
hidden |
Hidden field (value persisted, not shown) |
link |
Hyperlink display |
iframe |
Embedded iframe |
Numeric element options¶
| Field | Description |
|---|---|
decimalPlaces |
Number of decimal places to display. |
commaFormat |
If true, formats with thousands separators. |
clearOnNaN |
If true, clears the field value when input is not a valid number. |
step |
Step increment for the input control. |
percentageAsDecimal |
(percent-input only) Stores value as a decimal fraction. |
Option sources¶
For select, multi-select, type-ahead-select, and related elements:
optionList value |
Source |
|---|---|
"BUSINESS_CONSTANT" |
Loads from a business constants file. Set optionListParams.businessConstantType to the constant type name. |
"API" |
Loads from a client microservice endpoint at runtime. Set optionListParams.formOptionsSourceUrl to the relative endpoint path. Supports {riskId} placeholder. |
"ACTIVE_PRODUCTS" |
Loads active products for the current line of business. |
"UNDERWRITERS" |
Loads all underwriter users. |
"UNDERWRITING_ASSISTANTS" |
Loads all underwriting assistant users. |
"USER" |
Loads all Workbench users. |
"YES_NO" |
Static yes/no dropdown (yes first). |
"NO_YES" |
Static no/yes dropdown (no first). |
Additional option list config via optionListConfig:
| Field | Description |
|---|---|
autoSelectOnlyOption |
If true and only one option is returned, it is automatically selected. |
displayCurrentInputAsOption |
Allows the user to submit a value not in the list (free-text fallback). |
Repeatable configuration¶
When type is repeatable, configure the repeatable behaviour using repeatableConfig:
| Field | Description |
|---|---|
appendIndexToLabel |
Appends the row number to each row's label (e.g. "Coverage 1", "Coverage 2"). |
flexibleLayout |
Uses standard form element stacking instead of a table layout. |
headerLabels |
Shows column header labels above the first row. |
hideActions |
Hides the add and remove row buttons. Combine with oneRow: true for a fixed single-row table. |
oneRow |
Forces a single-row display. |
repeatButtonLabel |
Custom label for the "Add row" button. |
slim |
Compact display mode. |
title |
Label shown above the repeatable block. |
omitDisabledFieldsFromValue |
Excludes disabled controls from the submitted value payload. |
updateOnRiskDataChange |
Re-populates the repeatable from upstream risk data when it changes. |
There is no maximum row limit enforced by the platform. Any row count constraint must be implemented in client microservice validation or via section.maxLength.
Dynamic computation¶
Use computationConfig to automatically compute a value from a repeatable section (e.g. summing all premium rows into a total).
Emitter — placed on the source field inside the repeatable:
{
"key": "lineAmount",
"type": "currency-input",
"computationConfig": {
"emitter": {
"eventName": "quote-line-total-event"
}
}
}
Subscriber — placed on the target field (outside the repeatable, set as non-editable):
{
"key": "totalPremium",
"label": "Total Premium",
"type": "currency-input",
"modifierClass": "non-editable",
"computationConfig": {
"subscriber": {
"eventName": "quote-line-total-event",
"transformExpression": "model?.reduce((sum, el) => sum + Number(el.lineAmount), 0) || 0"
}
}
}
Event names must be globally unique
Use a descriptive name that includes the business class and action name to avoid collisions across screens.
Use computationSourceFormPath instead of eventName when multiple instances of the same screen exist simultaneously (e.g. multiple products open at the same time) — this binds to a specific form path instead of broadcasting an event:
"subscriber": {
"computationSourceFormPath": "coverageData.data.lines",
"transformExpression": "model?.reduce((sum, el) => sum + Number(el.lineAmount), 0) || 0"
}
Validators¶
Add validators to an element's validation array:
{
"key": "premiumAmount",
"type": "currency-input",
"validation": [
{ "validator": "required" },
{ "validator": "minValue", "value": 0, "message": "Premium cannot be negative" }
]
}
| Field | Description |
|---|---|
validator |
Validator name — see tables below. |
value |
Parameter passed to the validator (e.g. a minimum value, a field key, a regex pattern). |
message |
Error message shown to the user when validation fails. If omitted, a default platform message is used. |
messageKey |
Localisation path for the error message — overrides message in multi-language mode. |
Built-in validators¶
| Validator | Description |
|---|---|
required |
Field must have a value. |
min |
Minimum number value. |
max |
Maximum number value. |
minLength |
Minimum string length. |
maxLength |
Maximum string length. |
email |
Must be a valid email address. |
pattern |
Must match the provided regex pattern. |
Custom validators¶
| Validator | Description |
|---|---|
allRowsValidOrBlank |
All rows in a repeatable must either be fully valid or fully blank. |
alphanumericOnly |
Field may only contain letters and numbers. |
atLeastOneValueChecked |
At least one option must be selected in a checkbox group. |
exactLength |
Value must be exactly value characters long. |
forbiddenValuePresent |
Fails if the field value appears in the provided list. |
greaterThan |
Must be greater than value. |
greaterThanControl |
Must be greater than the value of the field named in value. |
lowerThanControl |
Must be lower than the value of the field named in value. |
matchRegex |
Must match the regex in value. |
maxValue |
Must be ≤ value. |
maxValueExclusive |
Must be < value. |
minValue |
Must be ≥ value. |
minValueExclusive |
Must be > value. |
noEmptyStrings |
Field value must not be an empty or whitespace-only string. |
nonNullable |
Value must not be null. |
numberInRange |
Value must be within configured range. |
oneInRepeatableRequired |
At least one row in the repeatable must have the specified fields populated. |
oneRequired |
At least one of the specified child controls must have a value. |
percentagesTotal100 |
All percentage values in the group must sum to 100. |
propertiesRequired |
Specified object properties must all be present and non-null. |
requiredForControlValue |
Field is required only when controlName equals value. |
requiredWithOthers |
Field is required when any of the specified sibling fields have a value. |
uniqueInRepeatable |
Values in this field must be unique across all repeatable rows. |
valueIsInteger |
Value must be a whole number. |
valuesEqualTotal |
The values of the specified controls must sum to total. |
invalidCharacters |
Fails if the value contains any of the characters in invalidChars. |
valueInListWarning |
Shows a warning (non-blocking) if the value appears in the provided list. |
Date validators¶
| Validator | Description |
|---|---|
dateAfter |
Date must be on or after value. Use "todaysDate" for a dynamic today comparison. |
dateAfterControl |
Date must be on or after the date in the field named by value. |
dateAfterAndNotOnControl |
Date must be strictly after (not equal to) the field named by value. |
dateBefore |
Date must be on or before value. |
dateBeforeControl |
Date must be on or before the date in the field named by value. |
dateBeforeWarning |
Shows a warning (non-blocking) if the date is before value. |
Modifier classes¶
modifierClass can be a single string or an array of strings. These apply CSS modifier classes to the element or section container.
| Value | Effect |
|---|---|
hide |
Hides the element. Prefer hideExpression for dynamic hiding. |
non-editable |
Renders the field as read-only. Use for computed fields. |
compact |
Compact spacing. |
condensed |
Condensed spacing. |
slim |
Slim spacing. |
inline |
Renders inline with adjacent elements. |
inline-radio |
Renders radio options inline horizontally. |
new-row |
Forces this element onto a new row in the grid. |
no-margin |
Removes all margins. |
no-margin-left |
Removes left margin. |
no-margin-right |
Removes right margin. |
no-padding |
Removes padding. |
no-resize |
Disables textarea resize handle. |
no-horizontal-resize |
Disables horizontal resize only. |
new-row |
Starts element on a new grid row. |
strike-through |
Applies strikethrough styling (for deprecated/removed fields). |
uppercase-text-input |
Forces text input to uppercase. |
flex-grow |
Element expands to fill available width. |
wrapped |
Wraps content. |
width-initial / 15 / 20 / 25 / 30 / 35 / 45 / 50 / 70 / 80 / 100 |
Sets the element width as a percentage of the container. |
Step-by-step: creating a screen from scratch¶
1. Confirm the operation name¶
Open the risk action JSON for your business class and placing type. Find the operation you want to attach a screen to:
The name value (structured_quote) is your operationName.
2. Create the config file¶
Create config/{client}/screen-meta-data/config/{bc}/structured_quote.json:
{
"operationName": "structured_quote",
"notRequiredSubStatus": "STRUCTURED_QUOTE_NOT_REQUIRED",
"initialSubStatus": "STRUCTURED_QUOTE_INITIATED",
"closingSubStatus": "STRUCTURED_QUOTE_COMPLETE",
"dataFile": "structured-quote-meta-data.json",
"apiGet": "riskValuesApi",
"apiPost": "riskValuesApi"
}
3. Create the layout file¶
Create config/{client}/screen-meta-data/files/{bc}/structured-quote-meta-data.json:
[
{
"group": {
"key": "quote_details",
"name": "Quote Details"
},
"sections": [
{
"section": {
"key": "risk_info",
"name": "Risk Information"
},
"elements": [
{
"key": "inceptionDate",
"label": "Inception Date",
"type": "date",
"validation": [{ "validator": "required" }]
},
{
"key": "expiryDate",
"label": "Expiry Date",
"type": "date",
"validation": [
{ "validator": "required" },
{
"validator": "dateAfterControl",
"value": "inceptionDate",
"message": "Expiry must be after inception"
}
]
}
]
},
{
"section": {
"key": "coverages",
"name": "Coverages",
"repeatable": true,
"repeatableConfig": {
"headerLabels": true,
"repeatButtonLabel": "Add Coverage"
}
},
"elements": [
{
"key": "coverageType",
"label": "Coverage Type",
"type": "select",
"optionList": "BUSINESS_CONSTANT",
"optionListParams": { "businessConstantType": "coverage_type" },
"validation": [{ "validator": "required" }]
},
{
"key": "limitAmount",
"label": "Limit",
"type": "currency-input",
"computationConfig": {
"emitter": { "eventName": "coverage-total-event" }
},
"validation": [{ "validator": "required" }, { "validator": "minValue", "value": 0 }]
}
]
},
{
"section": {
"key": "totals",
"name": "Totals"
},
"elements": [
{
"key": "totalLimit",
"label": "Total Limit",
"type": "currency-input",
"modifierClass": "non-editable",
"computationConfig": {
"subscriber": {
"eventName": "coverage-total-event",
"transformExpression": "model?.reduce((sum, el) => sum + Number(el.limitAmount), 0) || 0"
}
}
}
]
}
]
}
]
4. Verify the operation reference¶
Confirm the risk action references the screen's operationName in its operations array, and that the initialSubStatus, closingSubStatus, and notRequiredSubStatus values are consistent between the screen config and the action config.
5. Deploy and test¶
- Raise a PR with both new files
- Deploy via Client Build & Deploy (Dev) or Create/Deploy Client Microservice Version (SIT/UAT)
- Open the relevant risk in Workbench and trigger the action
- Confirm the screen opens, all fields render correctly, validation fires as expected, and the action transitions to the correct sub-status on save
See also¶
- Anatomy of a Risk Action — how screens connect to operations
- Display Expressions —
hideExpressionsyntax reference - Dynamic Computation — emitter/subscriber pattern
- Element Types — element type reference