Skip to content
All posts
ConfigurationInsurers

Configure a product: questions, covers, rules and pricing

A real outbound travel product, built as configuration: its inputs, its cover tree, eligibility and pricing workflows, refer and decline rules, and how it answers the standard quote form.

The BimaStack team · · 10 min read

A product on BimaStack is one versioned document, the product definition. It says what the product asks, what it covers, which workflow does each job, and what each finding means. The logic itself lives in the workflows and rate tables the definition names. This post builds a real one: outbound travel cover, from an insurer’s published flyer, with no code.

What a definition holds

Section Says
inputsThe questions, their types and their limits: required, a minimum and maximum, allowed values, an age range
coveragesThe covers and benefits, as a tree, with limits, deductibles, excess, waiting periods and conditions
stagesWhich workflow runs validation, eligibility, underwriting and pricing
rulesWhat each yes/no flag a stage raises means: reject the request, refer it, or decline it
quoteClassHow the product answers the standard quote form for its class, so it can be compared
optionsSwitches, such as pricing a declined request anyway

There are no product classes in the platform: no motor type, no travel type. Two products differ only in their definitions and the workflows and tables those name.

1. The questions

inputs
"inputs": [
  { "name": "destinationZone", "type": "ENUM",
    "allowedValues": ["AFRICA_ASIA", "EUROPE_SCHENGEN", "WORLDWIDE_BASIC",
                      "WORLDWIDE_PLUS", "WORLDWIDE_EXTRA"],
    "constraints": { "required": true } },
  { "name": "familyPolicy", "type": "BOOLEAN", "constraints": { "default": false } },
  { "name": "tripDays", "type": "NUMBER",
    "constraints": { "required": true, "minimum": 1, "maximum": 32 } },
  { "name": "travellerAges", "type": "LIST", "elementType": "NUMBER",
    "constraints": { "required": true, "minItems": 1, "maxItems": 10 } },
  { "name": "groupSize", "type": "NUMBER", "constraints": { "minimum": 1, "default": 1 } },
  { "name": "winterSports", "type": "BOOLEAN", "constraints": { "default": false } }
]

These are checked before any workflow runs, and every problem is reported at once with its path, such as inputs.travellerAges: a misspelt input is reported, never ignored, and “30” as text is not a number. A date can be bounded by today or by another answer; a date of birth can be held to an age range on the cover date.

2. The covers

coverages
"coverages": [
  { "code": "TRAVEL_PACKAGE", "name": "Outbound travel package",
    "type": "COVERAGE", "mandatory": true },
  { "code": "EMERGENCY_MEDICAL", "name": "Emergency medical & related expenses",
    "type": "BENEFIT", "parent": "TRAVEL_PACKAGE", "defaultSelected": true,
    "limit": { "currency": "USD", "fixed": 100000 } },
  { "code": "BAGGAGE", "name": "Checked-in luggage",
    "type": "BENEFIT", "parent": "TRAVEL_PACKAGE", "defaultSelected": true,
    "limit": { "currency": "USD", "fixed": 1500 } },
  { "code": "PERSONAL_ACCIDENT", "name": "Accidental death",
    "type": "BENEFIT", "parent": "TRAVEL_PACKAGE", "defaultSelected": true,
    "limit": { "currency": "USD", "fixed": 200000 } }
]
  • Sections, covers, benefits in one tree. A domestic package is a section with buildings, contents and all-risks under it.
  • Limits are fixed, a list to choose from, or a range, with a default.
  • Deductible and excess are an amount, or a percentage of the sum insured or of the claim with a minimum.
  • requires and excludes keep choices consistent: third party can’t be chosen with comprehensive.
  • enabledWhen switches a cover on from a yes/no answer.
  • A benefit needs no price to be on the quote with its limit. A pricing workflow may read the chosen covers and price each.

3. The stages

stages
"stages": {
  "eligibility": { "workflow": "travel-eligibility" },
  "pricing":     { "workflow": "travel-pricing",
                   "outputMapping": { "pricing.total": "grandTotal" } }
}

Each stage is optional, and each names a published workflow by key. A stage workflow’s inputs are filled by name from the answers, the chosen covers (coverages), the date and currency (context.asOfDate, context.currency) and every earlier stage’s outputs (eligibility.something). A mapping lets an existing workflow serve a stage without being edited: here the pricing workflow’s grandTotal is the product’s pricing.total.

The eligibility workflow: one yes/no flag
workflow "travel-eligibility" {
    input destinationZone : STRING
    input travellerAges : LIST<NUMBER>

    transform TE_maxAge {
        op: MAX
        elements: age : LIST<NUMBER>
    }
    formula TE_hasSenior {
        out flag : BOOLEAN
        in maxAge : NUMBER
        expression: maxAge >= 81
    }
    formula TE_notEuropeSchengen {
        out flag : BOOLEAN
        in destinationZone : STRING
        expression: destinationZone != "EUROPE_SCHENGEN"
    }
    formula TE_seniorOutsideEurope {
        out flag : BOOLEAN
        in hasSenior : BOOLEAN
        in notEurope : BOOLEAN
        expression: hasSenior and notEurope
    }
    output result {
    }
    connect travellerAges -> TE_maxAge.age
    connect TE_maxAge.max -> TE_hasSenior.maxAge
    connect destinationZone -> TE_notEuropeSchengen.destinationZone
    connect TE_hasSenior.flag -> TE_seniorOutsideEurope.hasSenior
    connect TE_notEuropeSchengen.flag -> TE_seniorOutsideEurope.notEurope
    connect TE_seniorOutsideEurope.flag -> result.seniorOutsideEurope
}

The pricing workflow reads the flat rate from the rate table by zone, individual or family, and trip length; age-rates each individual traveller and adds them up (a family uses the family rate as it is); doubles it for winter sports; takes the group discount; and rounds once. Nothing in it is specific to travel except the names.

The pricing workflow
formuladef "travel-age-factor" {
    in age : NUMBER
    expression: if(age < 18, 0.5, if(age <= 65, 1.0, if(age <= 75, 1.5, if(age <= 80, 2.0, 4.0))))
}
formuladef "travel-per-traveller-premium" {
    in rate : NUMBER
    in factor : NUMBER
    expression: rate * factor
}
workflow "travel-pricing" {
    input destinationZone : STRING
    input familyPolicy : BOOLEAN
    input tripDays : NUMBER
    input travellerAges : LIST<NUMBER>
    input groupSize : NUMBER
    input winterSports : BOOLEAN

    formula TR_familyKey {
        out label : STRING
        in familyPolicy : BOOLEAN
        expression: if(familyPolicy, "FAMILY", "INDIVIDUAL")
    }
    block TR_baseRate {
        in key1 : STRING
        in key2 : STRING
        in x : NUMBER
        out rate : NUMBER
        param tableCode = "travel-outbound-base-premium"
        binding: block "protected:table-rate:2d-band"
    }
    transform TR_ageFactors {
        op: MAP
        elements: age : LIST<NUMBER>
        binding: formula "travel-age-factor"
    }
    transform TR_perTraveller {
        op: MAP
        elements: factor : LIST<NUMBER>
        context: rate : NUMBER
        binding: formula "travel-per-traveller-premium"
    }
    transform TR_individualSum {
        op: SUM
        elements: amount : LIST<NUMBER>
    }
    formula TR_baseAmount {
        in familyPolicy : BOOLEAN
        in familyRate : NUMBER
        in individualSum : NUMBER
        expression: if(familyPolicy, familyRate, individualSum)
    }
    formula TR_winterMultiplier {
        in winterSports : BOOLEAN
        expression: if(winterSports, 2, 1)
    }
    formula TR_groupDiscountPercent {
        in groupSize : NUMBER
        expression: if(groupSize >= 201, 25, if(groupSize >= 101, 20, if(groupSize >= 51, 15, if(groupSize >= 21, 10, if(groupSize >= 10, 5, 0)))))
    }
    formula TR_total {
        in base : NUMBER
        in winterMultiplier : NUMBER
        in groupDiscountPercent : NUMBER
        expression: round(base * winterMultiplier * (1 - groupDiscountPercent / 100), 2, "HALF_EVEN")
    }
    output result {
    }

    connect familyPolicy -> TR_familyKey.familyPolicy
    connect destinationZone -> TR_baseRate.key1
    connect TR_familyKey.label -> TR_baseRate.key2
    connect tripDays -> TR_baseRate.x
    connect travellerAges -> TR_ageFactors.age
    connect TR_ageFactors.mapped -> TR_perTraveller.factor
    connect TR_baseRate.rate -> TR_perTraveller.rate
    connect TR_perTraveller.mapped -> TR_individualSum.amount
    connect familyPolicy -> TR_baseAmount.familyPolicy
    connect TR_baseRate.rate -> TR_baseAmount.familyRate
    connect TR_individualSum.sum -> TR_baseAmount.individualSum
    connect winterSports -> TR_winterMultiplier.winterSports
    connect groupSize -> TR_groupDiscountPercent.groupSize
    connect TR_baseAmount -> TR_total.base
    connect TR_winterMultiplier -> TR_total.winterMultiplier
    connect TR_groupDiscountPercent -> TR_total.groupDiscountPercent
    connect TR_total -> result.grandTotal
}

4. The rules

rules
"rules": [
  { "code": "SENIOR_OUTSIDE_EUROPE_SCHENGEN", "stage": "eligibility", "effect": "REFER",
    "message": "Persons aged 81 and above are covered only under the Europe/Schengen plan",
    "firedBy": "seniorOutsideEurope" }
]
Effect Means
ERRORThe request is invalid: it ends as a validation error, never as an outcome
DECLINENot offered. Not priced, unless options.pricingOnDecline is set
REFERNeeds an underwriter. Still priced, so the quote shows what it would cost

The outcome is DECLINE if any decline rule fired, else REFER if any refer rule fired, else ACCEPT, with the reasons in the order the rules are written. The workflow only says yes or no; the definition decides what that means. A threshold belongs in a rate table, not typed into the formula.

What it quotes

Zone Policy Days Ages Extras Outcome Total
Worldwide BasicIndividual530ACCEPT$22.34
Worldwide PlusIndividual1010, 70ACCEPT$69.86 (17.465 + 52.395)
Worldwide ExtraFamily2040Winter sportsACCEPT$217.76 (108.88 × 2)
Worldwide BasicIndividual530Group of 15ACCEPT$21.22 (22.34 × 0.95)
Worldwide BasicIndividual582REFER, still priced$89.36 (22.34 × 4.0)

5. Answering the standard quote form

Clients don’t fill in a form built from one product. Each class of business has one friendly standard form, the same for every insurer, and each product says how it fills its own inputs from that form’s answers. That is what lets one quote compare every insurer.

quoteClass
"quoteClass": { "code": "TRAVEL",
  "inputs": {
    "destinationZone": { "from": "region", "values": {
      "EAST_AFRICA": "AFRICA_ASIA", "AFRICA": "AFRICA_ASIA", "ASIA": "AFRICA_ASIA",
      "SCHENGEN": "EUROPE_SCHENGEN", "WORLDWIDE_EXCL_US_CA": "WORLDWIDE_BASIC",
      "WORLDWIDE": "WORLDWIDE_PLUS" } },
    "tripDays":      { "from": "departureDate", "as": "DAYS_SPANNED", "to": "returnDate" },
    "travellerAges": { "from": "travellers.age" },
    "groupSize":     { "from": "travellers", "as": "COUNT" },
    "winterSports":  { "addOn": "WINTER_SPORTS" } },
  "accepts": { "tripType": ["SINGLE"] },
  "highlights": { "MEDICAL": "EMERGENCY_MEDICAL", "BAGGAGE": "BAGGAGE" } }
  • values translates the form’s choices into the product’s own. An answer with no entry is one it doesn’t sell, so it is left out of that comparison, never priced wrongly.
  • as derives a value: days spanned between two dates, a count, a sum, an age from a date of birth or the reverse.
  • accepts limits what it prices: this product sells single trips, so an annual multi-trip request leaves it out.
  • Nothing personal is needed to price. The check refuses a mapping that reads a question asked only after a plan is chosen.

Checked before it goes live

Saving refuses a definition with a structural problem: a duplicate code, a cover whose parent doesn’t exist, a limit default outside its options, a rule for an undeclared stage. Publishing also checks that the documents agree:

  • every stage workflow is published on the version’s start date;
  • every input a stage workflow declares can be supplied, with a compatible type;
  • every rule’s flag is a yes/no output of its stage’s workflow;
  • pricing produces pricing.total as a number;
  • the quote form exists, and every question, add-on and highlight the mapping names is on it.

So a broken link fails when you publish, not on a client’s first quote. The Setup checklist shows, for any product, what it still needs and what each piece is used by.

Test it and publish it

  1. 1

    Simulate the draft

    Run the unpublished definition against real cases. Nothing is recorded. Ask for the trace to see each stage’s steps.

    Products
  2. 2

    Check what’s missing

    The Setup checklist lists every workflow, table and link the product needs, and whether each is ready.

    Setup checklist
  3. 3

    Approve, publish, schedule

    A second person approves. Publish now, or from a future date while the current version keeps quoting until then.

    Approvals

Launch your next product

Bring your rate card and wording; we’ll configure it with you.

Book a demo