Skip to content
Talk to our solutions team

`map` and `array`, collections

map and array are how a rule works with collections. They exist because the rule language cannot index or iterate one: there is no [0], no for, no comprehension. Every access goes through a function call, and every one of these returns a default rather than raising.

That constraint shapes what rules look like. A rule does not walk a table; it asks a question about it, find the row whose type is “principal”, how many rows are there, does this list contain the code. Where you genuinely need per-row logic, the answer is usually a table read (Rows) or a computation done before the rules run, not a loop.

Two rules hold for every function here:

  • Pass collections straight to the call. A local variable cannot hold a map or a list: the load-time check rejects m = in.Map("x"), and one that slips through aborts the execution.
  • Parameter types are exact. map.Get*, map.Has and the other plain map reads take a map, and array.* takes the list type to.List produces. A value typed as “anything”, such as what array.First returns, must go through to.Map first; a list from strings.Split, in.Rows or vocab.Items through to.List. The map.Path* family takes any value and needs neither.

Returns a fresh empty map.

Builds a map from alternating key/value pairs. Each call returns a new map.

ParameterTypeMeaning
kvany, variadickey1, value1, key2, value2, …
rule RecordAffordability "keep the inputs to the affordability decision together" {
when
in.Double("loan.income", 0.0) > 0.0
then
out.Set("affordability",
map.Of("amount", in.Double("loan.amount", 0.0),
"income", in.Double("loan.income", 0.0),
"ratio", math.Round(in.Double("loan.amount", 0.0) / in.Double("loan.income", 0.0), 2)));
}
facts
{ "loan": { "amount": 300000.0, "income": 90000.0 } }
out
{ "affordability": { "amount": 300000.0, "income": 90000.0, "ratio": 3.33 } }

An odd trailing argument is dropped without a word, so count them. A non-text key is converted to text. A list inside a map stays a list in the output, which is the way to return a list of strings.

FunctionReturns
map.GetStr(m, key, defaultVal)string. The default unless the value is text: a number is not converted
map.GetInt(m, key, defaultVal)int64, from an integer or a float
map.GetFloat(m, key, defaultVal)float64, from a float or an integer
map.GetBool(m, key, defaultVal)bool. The default unless the value is a boolean
map.GetMap(m, key)map, or nil
map.GetList(m, key)list, or nil
ParameterTypeMeaning
mmapThe map to read
keystringThe key
defaultValmatching typeReturned when the key is absent or holds another type
rule ReadRowAmount "the amount column, defaulting to zero" {
when
array.Len(to.List(in.Rows("invoice.lines"))) > 0
then
out.Set("firstLineAmount",
map.GetFloat(to.Map(array.First(to.List(in.Rows("invoice.lines")))), "amount", 0.0));
}
facts
{ "invoice": { "lines": [ { "sku": "A-1", "amount": 1200.0 }, { "sku": "B-7", "amount": 55.5 } ] } }
out
{ "firstLineAmount": 1200.0 }

Note the decimal point on 0.0, the same type-dispatch rule as elsewhere, and the to.Map around array.First: without it the call aborts with Call using interface {} as type map[string]interface {}.

Writes into a map in place. A nil map is ignored.

ParameterTypeMeaning
mmapThe map to modify
keystringThe key
valueanyThe value

Since no variable can hold a map, map.Set only reaches a map you can name again, such as one from in.Map. To build a map, use map.Of or map.SetPath, which return it. For results, use out.Set: it records the writing rule and carries attributes.

Removes a key, in place.

ParameterTypeMeaning
mmapThe map to modify
keystringThe key to remove

Reports whether the key is present.

ParameterTypeMeaning
mmapThe map to test
keystringThe key

Every key in the map, in no fixed order.

How many entries.

Whether the map has no entries. True for an empty map and for nil alike.

Whether v is a map at all, for telling an envelope from a bare value before reading it.

The Path* family reads a nested value in one call over a dotted path, where the Get* family would need one call per level. Every hop is nil-safe, and each takes any value as m. A literal dot in a key escapes as \. (written \\. inside a GRL string).

FunctionReturns
map.Path(m, path)Whatever is at path, or nil
map.HasPath(m, path)Whether path holds a non-nil value
map.PathStr(m, path, def)Text. A number at the path is rendered, not replaced by def
map.PathNum(m, path, def)float64. Document text such as "$412,000.00" converts
map.PathInt(m, path, def)int64, truncating
map.PathBool(m, path, def)bool. Yes, checked and the other affirmative words count
map.PathMap(m, path)The nested map, or an empty map, never nil
map.PathRows(m, path)Table rows, or an empty list
rule BorrowerCity "the city from the nested address block" {
when
map.HasPath(in.Map("application"), "borrower.address.city")
then
out.Set("city", map.PathStr(in.Map("application"), "borrower.address.city", ""));
out.Set("years", map.PathInt(in.Map("application"), "borrower.address.years", 0));
}
facts
{ "application": { "borrower": { "address": { "city": "Austin", "years": 4 } } } }
out
{ "city": "Austin", "years": 4 }

For a fact, in.Str("application.borrower.address.city", "") reads the same path directly. map.Path* is for a map that is not a fact: a row, a lookup result.

Each returns a new map, so it can be passed straight to out.Set.

FunctionReturns
map.SetPath(m, path, value)m with value written at path, creating the levels on the way. A nil m starts a new map
map.Merge(maps...)The maps combined left to right; a later key wins. A nil argument is skipped
map.Pick(m, keys...)Only the named keys that are present. A dotted key reads a nested value and is stored under its last segment
map.Omit(m, keys...)Everything except the named top-level keys
rule ShapeSummary "defaults under the caller's values, internal keys removed" {
when
in.Has("loan")
then
out.Set("summary",
map.Omit(map.Merge(map.Of("currency", "USD", "term", 360), in.Map("loan")), "internalId"));
}
facts
{ "loan": { "amount": 250000.0, "term": 180, "internalId": "x-77" } }
out
{ "summary": { "amount": 250000.0, "currency": "USD", "term": 180 } }

A list from to.List, array.New or another array call can be passed to any of these. Wrap any other list in to.List first: array.Len(strings.Split(s, ",")) aborts with Call using []string as type []interface {}, array.Len(to.List(strings.Split(s, ","))) works.

A fresh empty list.

How many elements.

ParameterTypeMeaning
arrlistThe list

Any list a call returns can also be counted with the language’s own .Len(): in.Rows("invoice.lines").Len(), bbox.Doc().Headings().Len(). Prefer .Len() for a list that is not already one to.List built.

rule TooManyBorrowers "more than four borrowers needs manual review" {
when
array.Len(to.List(in.Rows("application.borrowers"))) > 4
then
findings.Warn("TooManyBorrowers", "underwriting",
"More borrowers than the automated path handles");
out.Set("route", "manual");
}
facts
{ "application": { "borrowers": [
{ "name": "A" }, { "name": "B" }, { "name": "C" }, { "name": "D" }, { "name": "E" }
] } }
out
{ "route": "manual" }

The element at index, zero-based, or nil when out of range.

ParameterTypeMeaning
arrlistThe list
indexint64Zero-based position

array.GetStr(arr, index, defaultVal) → string · array.GetInt(...) → int64 · array.GetFloat(...) → float64

Section titled “array.GetStr(arr, index, defaultVal) → string · array.GetInt(...) → int64 · array.GetFloat(...) → float64”

Typed access with a default, for an index that may not exist.

ParameterTypeMeaning
arrlistThe list
indexint64Zero-based position
defaultValmatching typeReturned when the index is out of range or the element is another type

array.First(arr) → any · array.Last(arr) → any

Section titled “array.First(arr) → any · array.Last(arr) → any”

The first or last element, or nil for an empty list.

ParameterTypeMeaning
arrlistThe list

Last is the one worth remembering: reaching the final element without knowing the length is otherwise a two-step. Wrap the result in to.Map to read a row with map.Get*.

array.Contains(arr, value) → bool · array.ContainsStr(arr, value) → bool

Section titled “array.Contains(arr, value) → bool · array.ContainsStr(arr, value) → bool”

Membership. ContainsStr takes a list of strings, which is what strings.Split, vocab.Items and Headings() produce, without a to.List.

ParameterTypeMeaning
arrlistThe list to search
valueany / stringWhat to look for
rule KnownFormType "the classified type is one we handle" {
when
!array.ContainsStr(vocab.Items("supported_forms"), out.Str("docType", ""))
then
findings.Error("KnownFormType", "classification",
strings.Sprintf("Unsupported form type: %s", out.Str("docType", "")));
}

The position of the first occurrence, or -1 when absent.

ParameterTypeMeaning
arrlistThe list to search
valueanyWhat to look for

Whether the list has no elements.

array.Append(arr, value) → list · array.Prepend(arr, value) → list

Section titled “array.Append(arr, value) → list · array.Prepend(arr, value) → list”

A list with the element added at the end or the start.

ParameterTypeMeaning
arrlistThe starting list
valueanyWhat to add

A new list in reverse order.

The lists these return can be counted, searched or passed to another call. out.Set writes a list of plain values as its text form ("[a b c]"); to return one as a list, put it inside a map.Of.

array.Slice(arr, start, end): not callable

Section titled “array.Slice(arr, start, end): not callable”

Its bounds are a number type no rule value has, so every call aborts with reflect: Call using int64 as type int. The load-time check does not catch it. Use array.Get, array.First or array.Last for single elements.

The two functions that make table work possible without iteration. Both take rows, a list of maps, as in.Rows, to.Rows and Records() return them.

Returns the first row whose key column contains value, ignoring case, or nil when none does.

ParameterTypeMeaning
rowslist of mapThe rows to search
keystringThe column name
valuestringThe text the column must contain

Containment rather than equality: "principal" finds a row whose type reads "Principal & Interest". A nil result reads as absent through every map call, so no guard is needed.

rule PrincipalPayment "the payment table has a principal row" {
when
map.Has(array.FindRowBy(in.Rows("payments"), "type", "principal"), "amount")
then
out.Set("principal",
map.GetFloat(array.FindRowBy(in.Rows("payments"), "type", "principal"), "amount", 0.0));
}
facts
{ "payments": [
{ "type": "Escrow", "amount": 310.0 },
{ "type": "Principal & Interest", "amount": 1842.5 }
] }
out
{ "principal": 1842.5 }

On a document, pass the table’s rows: array.FindRowBy(bbox.Doc().Table().Records(), "type", "Principal").

array.FindRowByRegex(rows, key, pattern) → map

Section titled “array.FindRowByRegex(rows, key, pattern) → map”

The same, matching the column against a regular expression. An invalid pattern finds nothing.

ParameterTypeMeaning
rowslist of mapThe rows to search
keystringThe column name
patternstringThe regular expression to match

The form to use when containment is too loose or the wording varies in ways a substring cannot express, "Principal" but not "Principal Reduction".

rule PrincipalRowFlexible "match however this document words the principal row" {
when
map.Has(
array.FindRowByRegex(bbox.Doc().Table().Records(), "type", "(?i)^principal( &| and) interest"),
"amount")
then
out.Set("principal",
map.GetFloat(
array.FindRowByRegex(bbox.Doc().Table().Records(), "type", "(?i)^principal( &| and) interest"),
"amount", 0.0));
}

Deliberate: a rule states a condition and an action; a loop inside one is a program, and a program inside a rule is where rule engines stop being reviewable.

Where you find yourself wanting one:

You want toDo this instead
Act on each rowA Rows read, then hand Records() to out.Set
Find one rowarray.FindRowBy / FindRowByRegex
Count rows.Len() on the rows
Sum or aggregate a columnNot expressible in a rule. Compute it before the rules run and send it as a fact
Genuinely iterate with logicA script, called before the rules run