> ## 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.

# Use a filed rate table

> Declare a rating table in RSL, save its rows as CSV, and look up rates in calculations.

This guide reads the synthetic dwelling-fire table from [Write your first rater](https://docs.canary.effectiveai.app/rsl/tutorial/first-rater.md).
To follow along, save a copy of that rater and enter its folder:

```sh theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
rslc examples get dwelling
cd dwelling
```

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
WROTE dwelling/rater.rsl
WROTE dwelling/tables/BaseRate.csv
```

With a `dwelling/` folder from the tutorial, run `cd dwelling` alone. The first excerpts quote `rater.rsl` in that folder.

## Declare the table

Define the protection-class bands and perils in `Types`:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Peril = {
    fire,
    wind,
    theft
},
ProtectionClass = IntBands(1, 4, 7, 10..),
```

Declare `BaseRate` in `Tables`:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
BaseRate [cite: manual:p3] :
    table dims (protection: ProtectionClass) -> {
        rate: Rate[Peril]
    }
    columns: {
        rate[fire]: "fire",
        rate[wind]: "wind",
        rate[theft]: "theft"
    }
```

* `dims` defines the lookup key.
* `rate: Rate[Peril]` returns one rate per peril.
* `columns` maps those rates to CSV columns.
* `cite` identifies the source location.

## Save the CSV rows

The rows of `BaseRate` live in `tables/BaseRate.csv`:

```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
protection,fire,wind,theft
1,2.10,0.80,0.40
4,2.60,0.80,0.45
7,3.40,0.85,0.50
10,5.20,0.90,0.60
```

Each protection value starts a band: 1–3, 4–6, 7–9, or 10 and above.

Each key identifies one row. Keep numeric values as digits with a decimal point where needed.

## Look up a rate

Pass the table name and key to `lookup!`, then select the returned value:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
base_rate[p: Peril]: Rate =
    lookup!(BaseRate, self.dwelling.protection_class).rate[p],
```

For protection class 5, the lookup selects the row beginning at 4. Its fire rate is 2.60.

The lookup has a name, `base_rate`, and the premium reads that name:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
peril_premium[p: Peril]: Premium [cite: manual:p8] =
    round(self.dwelling.coverage_a / 1000 * self.base_rate[p], 2, half_up),
```

A lookup inside arithmetic gets its own name. Arithmetic over named values can nest, so the premium is one calculation.

For Coverage A of 250,000, the fire premium is `250 × 2.60 = 650`.

> [accordion] Tables with multiple keys
>
> A table can use several key columns. The `fn-tables` example on the [examples page](https://docs.canary.effectiveai.app/rsl/reference/examples.md) keys its base rate by territory and construction:
>
> ```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> BaseRate [cite: manual:p2] :
>     table dims (territory: Territory, construction: Construction) -> {
>         rate: Rate
>     },
> ```
>
> Its CSV contains one row per key combination:
>
> ```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> territory,construction,rate
> metro,frame,820
> metro,masonry,740
> suburban,frame,690
> suburban,masonry,620
> rural,frame,910
> rural,masonry,830
> ```
>
> Pass keys in declaration order:
>
> ```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> base_rate: Rate = lookup!(BaseRate, self.territory, self.construction).rate
> ```

> [accordion] CSV headers with different names
>
> Map RSL names to the CSV headers with `columns`. This small rater reads a ZIP table whose CSV has its own headers:
>
> ```rsl zip/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Contents (filed ZIP rates)",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Zip = Code(pattern: "[0-9]{5}")
> }
>
> Tables = {
>     ZipRate [cite: manual:p15] :
>         table dims (zip: Zip) -> {
>             rate: Rate
>         }
>         columns: {
>             zip: "zip_code",
>             rate: "annual_rate"
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             zip: Key(ZipRate),
>             contents: Num {precision: int} where self in [5000, 250000]
>         },
>         step premium {
>             zip_rate: Rate = lookup!(ZipRate, self.zip).rate,
>             premium: Premium [cite: manual:p16] = round(self.contents / 1000 * self.zip_rate, 0, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv zip/tables/ZipRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> zip_code,annual_rate
> 94110,100
> 94111,unfiled
> 94112,125.50
> ```
>
> The `columns` block maps the key `zip` and the output `rate` to the two headers:
>
> ```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> columns: {
>     zip: "zip_code",
>     rate: "annual_rate"
> }
> ```

### Read the rate an input selects

The subscript on a map column is a map binder, such as `p`, or one member, such as `@Peril.fire`.
This small rater takes the peril as an input:

> [accordion] one-peril/rater.rsl
>
> ```rsl one-peril/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Dwelling Fire (one peril)",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     Peril = {
>         fire,
>         wind,
>         theft
>     },
>     ProtectionClass = IntBands(1, 4, 7, 10..),
>     Money = Num {precision: 0.01}
> }
>
> Tables = {
>     BaseRate [cite: manual:p3] :
>         table dims (protection: ProtectionClass) -> {
>             rate: Rate[Peril]
>         }
>         columns: {
>             rate[fire]: "fire",
>             rate[wind]: "wind",
>             rate[theft]: "theft"
>         }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             coverage_a: Money where self in [50000, 2000000],
>             protection_class: Num {precision: int} where self in [1, 10],
>             peril: Peril
>         },
>         step premium {
>             base_rate[p: Peril]: Rate = lookup!(BaseRate, self.protection_class).rate[p],
>             premium: Premium [cite: manual:p8] =
>                 round(self.coverage_a / 1000 * self.base_rate[self.peril], 0, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv one-peril/tables/BaseRate.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> protection,fire,wind,theft
> 1,2.10,0.80,0.40
> 4,2.60,0.80,0.45
> 7,3.40,0.85,0.50
> 10,5.20,0.90,0.60
> ```

To read the rate for the peril that the request names, read the column into a map and index that map with the input:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
base_rate[p: Peril]: Rate = lookup!(BaseRate, self.protection_class).rate[p],
premium: Premium [cite: manual:p8] =
    round(self.coverage_a / 1000 * self.base_rate[self.peril], 0, half_up)
```

For protection class 5 and the peril `wind`, `self.base_rate[self.peril]` is 0.80.
An input as the column's own subscript, `.rate[self.peril]`, gets
[`E-MAP-DOMAIN`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-map-domain).

## Handle missing keys

Every lookup must find a value for each key it can receive. Choose the behavior supported by the filing:

| Filing rule | Implementation |
| - | - |
| An “all other” rate applies | Add a cited `otherwise` value |
| Only listed keys are accepted | Restrict the input to filed keys |
| A business rule excludes certain keys | Add an input rule with its code and citation |

An uncovered lookup fails `rslc check` with [`E-LOOKUP-GAP`](https://docs.canary.effectiveai.app/rsl/reference/check-messages/calculations#e-lookup-gap).

### Use a filed fallback

Add `otherwise` when the filing provides an all-other value, as the `fn-tables` example does:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
ZipFactor [cite: manual:p4] :
    table dims (zip: Zip) -> {
        factor: Factor
    }
    otherwise [cite: manual:p4]: { factor: 1 },
```

A ZIP absent from the CSV receives the factor 1. The fallback needs a citation to the filing’s all-other rule.

> [accordion] Accept only filed keys with Key(T)
>
> For a table with one supported key column, `Key(T)` accepts values naming its filed rows.
>
> The `zip` rater under “CSV headers with different names” declares a five-digit ZIP type and a table:
>
> ```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> Zip = Code(pattern: "[0-9]{5}")
> ```
>
> ```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> ZipRate [cite: manual:p15] :
>     table dims (zip: Zip) -> {
>         rate: Rate
>     }
> ```
>
> Its input uses the table’s name in the input type:
>
> ```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> zip: Key(ZipRate),
> ```
>
> A missing key receives `INPUT-KEY-UNFILED` before rating. A wholly `unfiled` row also counts as missing.

> [accordion] Represent a blank filed rate
>
> Use `unfiled` when the filing leaves an output blank. The `zip` rater's CSV marks one row:
>
> ```csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> zip_code,annual_rate
> 94110,100
> 94111,unfiled
> 94112,125.50
> ```
>
> A row whose outputs are all `unfiled` is treated as absent.
>
> Restrict accepted keys, or use a filing-supported `otherwise` value. A lookup that can reach an uncovered missing row produces `E-LOOKUP-GAP`.
>
> For a table with several output cells, a partly `unfiled` row produces `E-TABLE-COL`.
> Split outputs into separate tables when their filed rows differ.

## Locate a point in a filed grid

Some plans key a table by a grid cell that the filing assigns from the risk's latitude and longitude.
Add each cell's filed representative point to the table, and declare the grid as a geography.

> [accordion] grid/rater.rsl
>
> ```rsl grid/rater.rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> spec {
>     rsl: "0.4.1",
>     carrier: "Example Mutual",
>     state: AZ,
>     product: "Homeowners Hail (projected grid)",
>     effective_date: 2026-01-01
> }
>
> Sources = {
>     manual: Source = {
>         artifact: artifact("00000000-0000-4000-8000-000000000000"),
>         kind: pdf
>     }
> }
>
> Types = {
>     GridId = Code(pattern: "G[0-9]{4}")
> }
>
> Constants = {
>     base_premium [cite: manual:p2] = 900
> }
>
> Tables = {
>     Grid [cite: manual:p547] :
>         table dims (grid_id: GridId) -> {
>             factor: Factor,
>             latitude: Num {precision: 0.000001},
>             longitude: Num {precision: 0.000001}
>         }
> }
>
> Geographies = {
>     TerritoryGrid [cite: manual:p547] = projected_grid {
>         crs: esri_102008,
>         cell: 200,
>         origin_x: -2346800.383,
>         origin_y: -1764199.182,
>         cells: Grid,
>         latitude: latitude,
>         longitude: longitude,
>         on_line: least_key
>     }
> }
>
> Records = {
>     Policy = {
>         inputs {
>             latitude: Num {precision: exact} where self in [-90, 90],
>             longitude: Num {precision: exact} where self in [-180, 180]
>         },
>         step territory [cite: manual:p547] {
>             grid_factor: Factor =
>                 match locate(TerritoryGrid, self.latitude, self.longitude) {
>                     Some(g) => lookup!(Grid, g).factor,
>                     None => 1.0
>                 }
>         },
>         step premium [cite: manual:p5] {
>             premium: Premium [cite: manual:p5] = round(base_premium * self.grid_factor, 2, half_up)
>         }
>     }
> }
>
> RatingOutcomes = {
>     premium: Policy.premium
> }
> ```
>
> ```csv grid/tables/Grid.csv theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
> grid_id,factor,latitude,longitude
> G0001,1.10,33.450000,-112.070000
> G0002,1.25,33.460000,-112.080000
> ```

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Tables = {
    Grid [cite: manual:p547] :
        table dims (grid_id: GridId) -> {
            factor: Factor,
            latitude: Num {precision: 0.000001},
            longitude: Num {precision: 0.000001}
        }
}

Geographies = {
    TerritoryGrid [cite: manual:p547] = projected_grid {
        crs: esri_102008,
        cell: 200,
        origin_x: -2346800.383,
        origin_y: -1764199.182,
        cells: Grid,
        latitude: latitude,
        longitude: longitude,
        on_line: least_key
    }
}
```

`locate` returns the key of the filed cell that holds the point, as an option:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
grid_factor: Factor =
    match locate(TerritoryGrid, self.latitude, self.longitude) {
        Some(g) => lookup!(Grid, g).factor,
        None => 1.0
    }
```

A point on a line between cells takes the lesser code.
Write the `None` arm as the filing directs: refuse the risk, remove the factor, or assign a filed code.
A rater that uses `locate` rates on the native engine.

When the filed cells are squares of latitude and longitude, declare a `degree_grid`.
Its `cell` and origin are decimal degrees, as in the `fn-geography` example:

```rsl theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
Geographies = {
    WildfireGrid [cite: manual:p3] = degree_grid {
        cell: 0.01,
        origin_latitude: 0,
        origin_longitude: 0,
        cells: WildfireCell,
        latitude: latitude,
        longitude: longitude,
        on_line: least_key
    }
}
```

`locate(WildfireGrid, …)` works as it does for a projected grid.

## Check the rater

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

```text theme={"languages":{"custom":["languages/rsl.tmLanguage.json"]}}
PASS rater.rsl (strict)
cost premium: 16 + 2*|Policy.items| (each list of size 1: 18, 3: 22, 10: 36)
```

The check verifies lookup coverage for the inputs the rater accepts.
Compare the extracted rates with the filing and test independent examples to verify their accuracy.

## Next steps

* [Interpolate between table rows](https://docs.canary.effectiveai.app/rsl/how-to/interpolate-between-rows.md) when the filing rates between two filed points.
* [Round a premium](https://docs.canary.effectiveai.app/rsl/how-to/round-a-premium.md) to apply the filing’s rounding rules.
* Read [the table messages](https://docs.canary.effectiveai.app/rsl/reference/check-messages/tables.md) to investigate table diagnostics.


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