Skip to content

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:

{OPERATION_NAME}_{STATE}

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, returns GenericData[].
  • 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:

{
  "operations": [
    { "name": "structured_quote", "label": "Quote" }
  ]
}

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

  1. Raise a PR with both new files
  2. Deploy via Client Build & Deploy (Dev) or Create/Deploy Client Microservice Version (SIT/UAT)
  3. Open the relevant risk in Workbench and trigger the action
  4. 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