> ## Documentation Index
> Fetch the complete documentation index at: https://docs.canary.effectiveai.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Raters and guidelines

> Why a rater holds the filed math and a guideline holds the underwriting rules, and how roles, referrals and the submission log follow.

An insurance program has two kinds of rules. The filed rating plan says what a risk costs. The
underwriting guideline says which risks the carrier writes, who decides, and what each decision
rests on. RSL keeps the two in separate files that share one language.

This page reads the staffing example of the synthetic carrier Example Mutual. Save both files in
one folder, `staffing/`, to run the commands on this page.

> [accordion] rater.rsl
>
> ```rsl staffing/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4",
>     carrier: "Example Mutual",
>     state: PA,
>     product: "Staffing Liability",
>     version: "0.1.0",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     rules: Source = {
>         artifact: artifact("5b0e7c0e-6f0a-4c56-9d39-0d1f1a1c7e11"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Experience = {
>         no_claims,
>         claims
>     }
> }
>
> Constants = {
>     rate_per_1000 [cite: rules:p1] = 2,
>     claims_factor [cite: rules:p2] = 1.5,
>     basic_limit [cite: rules:p3] = 1000000,
>     higher_limit_factor [cite: rules:p3] = 1.1
> }
>
> Records = {
>     Policy = {
>         inputs {
>             payroll: Num {precision: int} where self in [0, 100000000] [cite: rules:p1],
>             experience: Experience [cite: rules:p2],
>             limit: Num {precision: int} per quote where self in [1000000, 2000000] [cite: rules:p3]
>         },
>         step rating [cite: rules:p1] {
>             experience_factor: Num = if self.experience = @Experience.no_claims then 1 else claims_factor,
>             limit_factor: Num = if self.limit > basic_limit then higher_limit_factor else 1,
>             premium: Premium [cite: rules:p1] =
>                 round(self.payroll / 1000 * rate_per_1000 * self.experience_factor * self.limit_factor, 2, half_up)
>         }
>     } where self.payroll >= 1000 [code: "PAYROLL_BELOW_MINIMUM", message: "The payroll must be at least $1,000.", cite: rules:p1]
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```

> [accordion] guideline.rsl
>
> ```rsl staffing/guideline.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4",
>     carrier: "Example Mutual",
>     state: PA,
>     product: "Staffing Liability Underwriting Guideline",
>     version: "0.1.0",
>     effective_date: 2026-01-01
> }
>
> use ts = "rater.rsl"
>
> Roles = { assistant < uw < manager }
>
> Acts = { quote: @Role.uw, bind: @Role.uw }
>
> Sources = {
>     guideline: Source = {
>         artifact: artifact("0c1f6f43-1f55-4a55-8a53-7d1b6f0e2a22"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Operation = {
>         staffing,
>         peo
>     }
> }
>
> Constants = {
>     established_years [cite: guideline:p1] = 3,
>     large_premium [cite: guideline:p2] = 10000
> }
>
> Records = {
>     Submission = {
>         inputs {
>             operation: Operation [cite: guideline:p1],
>             years_in_operation: Num {precision: int} where self in [0, 300] [cite: guideline:p1],
>             financially_sound: Bool by @Role.uw when self.operation = @Operation.peo [cite: guideline:p1],
>             policy: ts.Policy,
>             policy.experience by @Role.uw
>         },
>         step premium [cite: guideline:p2] {
>             premium: Premium = ts(self.policy).premium
>         }
>     } where self.operation != @Operation.peo or (self.years_in_operation >= established_years and self.financially_sound) [code: "PEO_NOT_ESTABLISHED", message: "A PEO must be in operation for at least 3 years and be financially sound.", cite: guideline:p1]
>         refer new_venture when self.years_in_operation < established_years
>             to @Role.manager because "The firm is in operation for less than 3 years." [cite: guideline:p1]
>         refer large_account when self.premium >= large_premium
>             to @Role.manager because "The premium is $10,000 or more." [cite: guideline:p2]
> }
>
> RatingOutcomes = {
>     premium: Submission.premium
> }
> ```

## What each file owns

| | Rater, `rater.rsl` | Guideline, `guideline.rsl` |
| - | - | - |
| Follows | The filed rating manual | The carrier's underwriting guideline |
| Calculates | Premiums, from rates, tables and rounding rules | The condition of each referral |
| Asks for | The facts that the premium depends on | The rater's questions, and the facts that underwriting adds |
| Says who answers | Leaves each question open to every role | Gives questions to roles, with `by` |
| Turns away | A risk outside what the manual rates | A risk outside the carrier's appetite |
| Asks a person | Leaves decisions to the guideline | Refers a risk to a role for a sign-off, with `refer` |
| Lets a quote set | Its own inputs that it marks `per quote` | Its own inputs that it marks `per quote` |
| Changes with | A new filing | A change of appetite or of authority |

The split follows who changes each file, and why. A filed rate changes with a filing, on the
regulator's calendar. An authority limit changes when the carrier decides, often between filings.

The split also keeps the rater plain. A rater that a guideline uses declares only rating, so the
same file still rates a whole book as a bundle. One rater serves several guidelines, and this page
adds a second one below.

The facts of one submission live in a third place: the **submission log**. Its answers and
sign-offs go into the guideline, and the stage and the open questions come out.

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
                rater.rsl                    guideline.rsl
                ---------                    -------------
questions       payroll, experience,  --->   policy: ts.Policy, and its own questions
                limit
results         premium               --->   ts(self.policy).premium
stops           coded rule            --->   an ineligible submission, or a value to match
                                             Roles, Acts, by, refer, where

submission log  --->  guideline and rater, checked as one program  --->  status
(answers, sign-offs)                                    (stage, open questions, outcomes)
```

## One kind of program

A guideline is the same kind of program as a rater: answers go in, and outcomes come out. Three
lines of the guideline connect the two files:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
use ts = "rater.rsl"
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
            policy: ts.Policy,
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
            premium: Premium = ts(self.policy).premium
```

`use` links the rater under the name `ts`. The rater's record is one input of the guideline, so
the rater's questions keep their names and types. The call `ts(self.policy)` gives the rater's own
result for that policy.

Because both files are one kind of program, one check covers both:

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
cd staffing
rslc check guideline.rsl
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
PASS guideline.rsl (strict)
cost premium: 1 + 3*|Submission.policy| (each list of size 1: 4, 3: 10, 10: 31)
cost new_venture: 1 (each list of size 1: 1, 3: 1, 10: 1)
cost large_account: 2 + 3*|Submission.policy| (each list of size 1: 5, 3: 11, 10: 32)
```

The check holds the guideline to the rules of a rater: types, citations and rounding. A referral is
a calculated outcome, so the check reports its cost beside the premium.

A guideline reads a rater through its inputs and its outcomes only. A guideline line that reads a
calculation inside the rater gets the check message `E-USE`. So the rater's steps stay free to
change, and each guideline that uses it stays correct.

## Two ways to read the rater

The rater has a coded rule: it rates a payroll of \$1,000 or more. A guideline chooses what that
rule means for a submission.

### The short call

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
            premium: Premium = ts(self.policy).premium
```

The short call reads one outcome. A policy that breaks a coded rule of the rater makes the whole
submission ineligible, with the rater's code:

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc submission open short.jsonl --program guideline.rsl --as assistant:intake
rslc submission answer short.jsonl policy.payroll=500 --as assistant:intake
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
...
  2  assistant:intake  answered policy.payroll = 500
stage: Ineligible
declines:
  PAYROLL_BELOW_MINIMUM  The payroll must be at least $1,000.  at policy
  the account stays open: correct an answer, or decline it with `close declined`
```

### The match form

A second guideline for the same rater writes small accounts at a minimum premium, with a manager's
sign-off. Save it beside the rater as `small-accounts.rsl`.

> [accordion] small-accounts.rsl
>
> ```rsl staffing/small-accounts.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4",
>     carrier: "Example Mutual",
>     state: PA,
>     product: "Staffing Liability Small Accounts Guideline",
>     version: "0.1.0",
>     effective_date: 2026-01-01
> }
>
> use ts = "rater.rsl"
>
> Roles = { assistant < uw < manager, bureau < uw }
>
> Acts = { quote: @Role.uw, bind: @Role.uw }
>
> Sources = {
>     guideline: Source = {
>         artifact: artifact("0c1f6f43-1f55-4a55-8a53-7d1b6f0e2a22"),
>         kind: pdf
>     }
> }
>
> Constants = {
>     minimum_premium [cite: guideline:p3] = 250
> }
>
> Records = {
>     Submission = {
>         inputs {
>             policy: ts.Policy,
>             policy.experience by @Role.bureau
>         },
>         step premium [cite: guideline:p3] {
>             rated: Bool = match ts(self.policy) { Rated => true, Declined => false },
>             premium: Premium [cite: guideline:p3] =
>                 match ts(self.policy) { Rated(r) => r.premium, Declined => minimum_premium }
>         }
>     } refer below_minimum when not self.rated
>             to @Role.manager because "The payroll is below the minimum that the rater takes." [cite: guideline:p3]
> }
>
> RatingOutcomes = {
>     premium: Submission.premium
> }
> ```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
            rated: Bool = match ts(self.policy) { Rated => true, Declined => false },
            premium: Premium [cite: guideline:p3] =
                match ts(self.policy) { Rated(r) => r.premium, Declined => minimum_premium }
```

A call's result is a value with two cases. `Rated(r)` holds the rater's outcomes, read as
`r.premium`. `Declined` means that a coded rule of the rater failed. The guideline says what
happens in each case, so the same payroll now gives a referral and a premium:

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc submission open small.jsonl --program small-accounts.rsl --as assistant:intake
rslc submission answer small.jsonl policy.payroll=500 --as assistant:intake
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
...
  2  assistant:intake  answered policy.payroll = 500
stage: Open
open (1):
  below_minimum  signoff  @manager  because: The payroll is below the minimum that the rater takes.
                   reads policy.payroll = 500
referrals:
  below_minimum  Open  @manager
outcomes known so far:
  premium = 250
```

The premium is known before the loss experience is, because the `Declined` case reads only the
minimum premium.

| | Short call | Match form |
| - | - | - |
| Reads | One outcome, `ts(self.policy).premium` | Each case of the result |
| A coded rule of the rater fails | The submission is `Ineligible` | The `Declined` case gives the value |
| A value breaks a type or a range | The log refuses the answer | The log refuses the answer |
| Fits a guideline that | Writes only what the rater rates | Has its own path for a risk the rater stops |

A guideline reads one rater in one form: every call is short, or every call is a match.

## Roles in chains and roles beside each other

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Roles = { assistant < uw < manager }
```

A chain lists roles from lowest to highest. A role does what the roles below it do: it answers
their questions and replaces their answers. A write replaces only an answer from the writer's own
role or a role below it, so an assistant leaves a manager's decision in place.

Authority in underwriting is a ladder, and a chain states it once. Each question and each referral
then names only its lowest role.

The second guideline has two chains:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Roles = { assistant < uw < manager, bureau < uw }
```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
            policy.experience by @Role.bureau
```

`assistant` and `bureau` stand beside each other, because each chain holds only one of them. Each
one answers its own questions and leaves the other's answers alone. This is how a data source is a
role: a loss-data bureau owns the loss experience, and an underwriter above it may correct it.

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc submission answer small.jsonl policy.experience=claims --as assistant:intake
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
refused NOT_AUTHORIZED: answering `policy.experience` needs role `bureau` or a role above; `intake` is `assistant`
  candidates: bureau, uw, manager
...
```

One order of roles answers one question: who may answer this. A tool and a person get the same
kind of name, `role:name`, and the same rules.

## Referrals and sign-offs

A rule with a code makes a submission ineligible. A referral asks a person to decide:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
        refer new_venture when self.years_in_operation < established_years
            to @Role.manager because "The firm is in operation for less than 3 years." [cite: guideline:p1]
```

A referral is a named condition with a role and a reason. The compiler treats the condition as one
more outcome. So the log knows a referral as soon as the answers it reads are known, often before
the premium.

A sign-off stands for what its signer read. The status lists, under each open referral, the
answers that its condition reads. The sign-off entry records those answers and a hash of them.
So the answers that the signer saw are exactly the answers that the sign-off covers.

| After the sign-off | The sign-off |
| - | - |
| An answer that the condition reads changes | Lapses, and the referral opens again |
| That answer returns to the value that was signed off | Stands again |
| Any other answer changes | Stands |

A manager approves facts, and the approval covers those facts only. A higher payroll leaves the
approval of a new venture in place. A different age of the firm asks the manager again.

## Why a decline is a decision

A decline is the most serious thing that a submission records: the carrier tells the broker that
it turns the risk down. So the log keeps two things apart.

| What happens | Stage | What follows |
| - | - | - |
| An answer breaks a coded rule, or a sign-off declines | `Ineligible` | A corrected answer, or a decision |
| The role of the `quote` act writes `close declined` | `Declined` | The submission is final |

A typing error meets a rule as readily as a true answer does. A firm of 20 years, typed as 2, fails
the same rule as a firm of 2 years. The log has one way to tell them apart: a person or an agent
with authority says so.

So an answer is recorded or refused, and the account stays open either way. The decline itself is
an entry by the role that may offer a quote, and it names the codes that it rests on. The log
accepts it only while a rule or a sign-off declines.

## Why the log only grows

The log holds what people and tools said and did: one entry for each answer, sign-off, quote,
bind and close. Commands only add entries. The status is computed from the entries and the
program each time someone asks.

* The status always agrees with the answers, because the log stores only the answers.
* Each entry was valid when it was written. The log checks the type of an answer and the role of its writer first.
* Each moment is still there. The status after any entry is the status that the log had then.
* The program is fixed. A log holds a hash of its guideline and rater, and it refuses to read under a changed program.

For the same reason, a lapse repairs itself. A sign-off or a quote lapses because its hash differs
from the present one, and it stands again when the answers return.

Storage is separate from these rules. A log is a `.jsonl` file or a `.sqlite` database, and both
hold the same entries under the same rules. Each store adds an entry only at the end that the
writer read, so two writers take turns.

## Why a list item is named by its key

An answer to one item of a list needs an address that stays with that item. A place in the list,
such as the first item, moves when someone adds or removes an item before it. An answer at a place
then lands on another item.

So the log uses the identity that the program already gives: the key of a list that is `keyed by`
a field. An address reads `placements[class="clerical"].share`, and it names that item for the
life of the log.

* A list that lacks a key is answered whole, as one value.
* A keyed list is a set of items. The same items in another order are the same answers, and a quote of them stands.

## Why a quote is kept and named

A broker holds a quote, so the log keeps each quote as its own entry with the outcomes it gave.
A later answer changes the status of the quote, and the entry stays as it was.

* A log holds several quotes, such as two options for the broker. `bind` names the one that the insured accepts.
* A quote rests on the program, every answer and every sign-off. It lapses when one of them changes, and stands again when they return.
* `bind` takes a quote only while it stands. So a bound premium is always the premium that the answers give.

## Why every quote prices one risk

A broker often wants options: the same firm at two limits. The options differ in the terms of the
policy, and the facts about the firm stay the same.

The program draws that line. An input marked `per quote` is a **term**, and every other input is a
**fact**:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
            limit: Num {precision: int} per quote where self in [1000000, 2000000] [cite: rules:p3]
```

A **quote option** holds its own answers to terms, laid over the shared answers. A quote of the
option prices both together.

| Design choice | Reason |
| - | - |
| An option answers terms only | Two quotes of one submission always agree on the payroll and the history |
| An input is a fact unless it carries the mark | A forgotten mark gives a refusal, and a fact stays the same in each quote |
| The mark sits on the input's own declaration | Each input has one name, and a rater marks its terms for every guideline |
| A guideline marks only its own inputs | A guideline keeps the terms of its rater as the rater states them |
| A role's answer is always a fact | A judgment about the risk holds for every quote of that risk |
| A change to a fact reaches every option | Each quote that the change touches lapses, in every option |

The log refuses an answer to a fact in an option with the code `NOT_A_TERM`.
[Run a submission](https://docs.canary.effectiveai.app/rsl/how-to/run-a-submission.md#offer-quote-options) quotes two limits on one submission.

## What `--based-on` protects

The log takes one write at a time, which puts writes in order. Order leaves one gap open. A
correction that lands between a manager's read and the manager's sign-off gets approved unseen.

`--based-on` closes the gap. The writer names the last entry that they read, and the log compares
what the command reads at that entry and now. A difference refuses the write with the code `STALE`.

| Command | What it reads |
| - | - |
| A sign-off | The answers that the referral's condition reads, and an earlier sign-off of it |
| `answer`, `retract` | The value at the question's path |
| `quote` | Every answer and every sign-off |
| `close declined` | The rules and the sign-offs that decline |
| `bind`, `close` | Only that the entry exists |

The log compares what the command reads, and leaves the length of the log aside. So an unrelated
write by a colleague leaves the sign-off valid, and the writer retries only for a real change.

## Related

* [Write your first guideline](https://docs.canary.effectiveai.app/rsl/tutorial/first-guideline.md) builds the staffing guideline and works one submission.
* [Run a submission](https://docs.canary.effectiveai.app/rsl/how-to/run-a-submission.md) shows each command, with a real `STALE` refusal.
* [Submission log](https://docs.canary.effectiveai.app/rsl/reference/submission-log.md) lists every entry, stage and refusal code, and the contract that a program publishes.
* [What rslc checks and guarantees](https://docs.canary.effectiveai.app/rsl/explanation/checks-and-guarantees.md) explains the checks that a guideline shares with a rater.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.