Configuration & Technical Handbook

This is the complete reference for the people who design and configure Rangeen — every feature you can add, customise and connect, with worked examples. It's written to be read as a manual: skim the contents, or work through it to build a case type from nothing to a finished workflow. If you only use the system day to day, the Everyday User Guide is the friendlier starting point. Everything here reflects how the system works today.

Case types & fields Screen studio Automation grammar Templates & tokens Lists · Insights · Routines Roles · Add-ons · Security

1 · How the system is built

Everything hangs off the case type.

A case is the record for one matter. A case type is the mould every case of a given kind is stamped from. When you design a system in Rangeen, you are almost always working inside a case type — because a case type owns nearly everything a case can do:

  • Fields — the data a case records (Data manager).
  • Screens — how those fields are laid out (the Screen studio).
  • Templates — the letters, emails and memos it can produce.
  • Automations — the scripted routines that run for it.
  • To-dos & queries, assignment slots, quick-view config, collaborator profiles, and its lifecycle trigger bindings.

A few things are tenant-wide rather than per case type: the correspondent directory and its types, global variables, users / teams / roles, tenant settings, and the single organisation-wide case numbering sequence. The typical build order is: create the case type → define its fields → lay out screens → write templates → add automations → wire triggers → add queries/reports. Section 30 walks that path end to end.

Rangeen keeps an automatic version of a case type's whole design as you change it, and you can restore an earlier one — so designing is safe to experiment with. See section 25.

2 · Case types

The mould for each kind of matter.

Where: Manage → Case types

  • A case type has a Code (short, immutable, unique), a Name, an icon and a description. Opening a case of that type brings all of its screens, templates, automations, tasks and queries.
  • Multi-user vs single-user (AllowMultiUserAccess): single-user locks a case to one person at a time; multi-user lets several work it, with the case-lock banner warning when someone else is in.
  • Enable / disable: a disabled case type disappears from the new-case and query pickers but keeps its existing cases and history. A case type can't be deleted while any case, screen, workflow, field or query still references it.
  • Numbering is organisation-wide. Case types no longer carry their own prefix or sequence — every case, whatever its type, takes the next number from one shared six-digit sequence (see Organisation settings).
Changed: earlier versions gave each case type its own reference prefix (e.g. PI-0011). Numbering is now a single tenant-wide numeric sequence (e.g. 000123) with no per-type prefix. The old prefix/digits settings are gone from the case-type editor.

3 · Fields & data types

The data model is yours — defined field by field in Data manager.

Where: Manage → Data manager (needs database.manage + fields.manage)

Every field has a Code (its internal short name — letters, digits, underscores and spaces; immutable once created), a display label (editable, and overridable per screen), and a data type (also immutable). Fields can be marked audited (every change stamped with who and when) and GDPR-searchable.

The fifteen field data types

Type (as you pick it)HoldsSettings
TextFree text — names, notes, references.Optional max length (1–10,000).
Whole numberAn integer, no decimals.Optional min / max.
DecimalA number with decimals — money, rates, measurements.Min / max; decimal places (0–10, default 2).
DateA calendar date, with a picker.Allow past / future; default fixed or TODAY±N.
Date & timeA date plus a time of day.As Date, plus an optional @HH:mm default.
TimeA time of day on its own.Optional default time.
Yes / No dropdownThree states: blank, Yes, or No.Optional default.
CheckboxTwo states — ticked or not.Default checked / unchecked.
DropdownA list of predefined values (each a Code + Description, plus any extra columns).Up to 20 extra option columns; values managed separately.
Scripted (computed)A read-only value calculated from other fields.A script expression + a declared return type.
Case linkA link to a case of another type; its fields are reachable in templates & scripts.The linked case type.
CorrespondentA "holder" pointing at one correspondent of a chosen type.The linked correspondent type.
TableA repeating sub-table on the case — you define its columns.Member columns (see below).
Time recordA start/stop time log, with optional extra columns.Member columns (see below).
Embedded documentA document slot on the case, with a template document.File uploaded in Data manager.

Tables (iteration tables)

  • A Table field holds no value of its own — it's a repeating sub-table. You open it and add columns, which are ordinary fields (Text, Number, Decimal, Date/DateTime/Time, Yes/No, Checkbox, Dropdown, or a Time record).
  • You can't nest a Table, a Case link, a Correspondent or a Scripted field inside a table.
  • You can add a Document column, so each row holds its own file. The grid stays readable: a row shows a small icon only when it actually has a document, and clicking the icon previews it. Upload, replace and remove live in the add/edit-row dialog and stage like every other cell — they work on brand-new unsaved rows too, apply when you save the case, and Cancel undoes them.
  • A document attached to the column is a starting point, not a fallback: rows begin empty, and a row gets its own copy only when someone uses Start from template in the row dialog or an automation puts one there. Each row's copy is independent afterwards — changing the column's template never rewrites rows already made from it. The copy is the file as-is; tokens inside it are not merged from the row.
  • Deleting a row deletes the file that row held, and replacing a row's document deletes the file it replaced once the save commits — the case keeps only what its rows actually point at. Document columns are iteration tables only — not Time records.
  • On a case it renders as a grid with a row count and an Add row button.

Time records

  • A Time record field logs work time. Each entry is one Start / Stop run, carrying start, end, a computed duration, who recorded it, and a note.
  • You can add extra columns (e.g. billable, hourlyRate, reason) — templates can sum these for invoicing.
  • A Time record can even be a column inside a Table, so each row owns its own start/stop log.

Scripted (computed) fields

  • A Scripted field's value is never stored — it's recalculated every time the case is read, so it's always current and read-only. You give it a script expression and declare what type it returns.
  • It reads other fields with get('{FieldName}'); a field named with spaces uses its exact name, e.g. get('{Original Debt}'). It can also read globals, linked-case fields and table rows.
  • A Scripted field can produce its own value but can't write to other fields, and it never breaks a case read — a broken formula just yields blank. The field editor has a Preview button to try a formula against a real case.

Example — a "Balance Outstanding" scripted field (return type Decimal)

get('{Original Debt}') - get('{Total Paid}')

Example — a "Total Paid" scripted field summing a table's rows

(get('{Payments[]}') || []).reduce(function (sum, row) {
  return sum + (Number(row['Amount']) || 0);
}, 0)

Links & embedded documents

  • A Case link stores the linked case's reference; templates and scripts "walk through" it to read the linked case's own fields.
  • A Correspondent field is a holder pointing at one correspondent of a chosen type (a "Main Doctor" holder accepting any Doctor). Its details merge into templates and are reachable in scripts.
  • An Embedded document field carries a template document that renders on every case of the type, and can hold a per-case document.

Constraints (max length, min/max, decimal places, allowed past/future dates) can be relaxed or tightened later; the field's Code and data type stay fixed for the life of the field.

4 · The Screen studio

Lay the fields out the way the work reads.

Where: Manage → Screen studio (needs screens.manage)

A screen is a data-entry form bound to one case type; a case type can have many, shown as tabs on the case. You build a screen by dragging tiles onto a grid. The tile kinds are:

TilePlaces
FieldA case field as an editable input. Also used for linked fields, embedded documents and correspondent holders.
LabelStatic text — with a chosen size and colour.
ButtonA button that runs an automation. Style: Primary / Secondary / Danger.
TableA sub-table (add / edit / delete rows). Also the tile used to place a Time record's start/stop log.
Web viewerAn embedded web page (an http(s) URL).
Image viewerA static uploaded image.
Correspondent attributeOne detail of a linked correspondent (name, email, address…).
GlobalA tenant global variable — editable (saving writes back to the global for the whole organisation).
Case controlA system property (reference, status, dates…) — always read-only.
AssignmentA detail of the user in an assignment slot — always read-only.

Draft & publish

  • Screens are edited as a draft. While you edit, everyone working cases keeps seeing the last published layout — your work-in-progress never leaks onto live cases.
  • Save & Publish makes the new layout live; Discard changes snaps back to the last published version.

Per-screen field settings

  • Label override, read-only on this screen, required on this screen — set per placement, so the same field can behave differently on different screens.
  • Display format — for Date / Date & time / Number / Decimal, picking a format renders the value as read-only formatted text; "None" keeps it editable. For Dropdown fields it picks which column the list shows.
  • Multi-line — turn a Text field into a wrapping text area.
  • On change automation — an automation that runs the moment a user has changed this field and moved on (tabs or clicks away, saves, switches screen or closes the case) — never per keystroke, and only for edits typed on a screen. It runs before anything is saved: get() reads the value just typed, and stop('why') rejects it — the value stays on screen outlined red with the reason, and Save waits until it is changed. Use it for checks like "the phone number must have 11 digits". Separate from the field's own on-change automation (Database management), which runs when the case is saved.
  • Visible from level / Editable from level — per placed field (and per table tile and correspondent block): users below the "visible from" level get an empty space where the tile would be; users below the "editable from" level see it read-only. Same 0–99 scale as a screen's authorisation level, checked against the user's security level; blank = everyone. Collaborators count as level 0.

Who sees a screen

  • Authorisation level — a numeric level a user must meet to see the screen (use it to keep, say, a "Costs" screen to supervisors).
  • Visibility rules — optionally show a screen only when the case's data meets a condition (e.g. a "Payment Plan" screen appears only once the stage is past "Pre-action"). The comparators are Equals, Not equals, Contains, Greater / Less (and or-equal), Is blank, Is not blank.

Nesting screens

  • A screen's settings can name a parent screen (same case type), nesting it under that screen in the workspace's screens list — up to two levels below top (top → child → grandchild). The parent stays a normal, fillable screen; nesting is grouping, not a folder. The picker only offers parents the two-level limit allows.
  • Or just drag it. In the Screens list, drop a screen onto the middle of another row to nest it inside — the row lights up as you hover. Drop it on a row's top or bottom edge and it lands there instead, beside that row, at that row's level: a line shows exactly where, indented to the level it will land at. That is how a nested screen comes back out — drop it on the edge of any top-level row — and how one nests at a chosen position rather than last. Dragging a nested screen also shows a “move to top level” strip under the list, and the row's menu has Move to top level. A landing spot the two-level limit forbids greys out and is refused, never silently re-ordered.
  • Long lists scroll as you drag. Hold a screen near the top or bottom of the window and the list scrolls, so moving one from 2nd to 90th is a single drag.
  • A hidden parent hides its group: when a parent fails its visibility rule or authorisation level, its nested screens hide with it — one rule on the parent governs the group. A screen that must always show belongs at top level.
  • Client Hub sharing stays per-screen. Ticking a nested screen without its parent still shows it to the collaborator — at top level (or under its nearest shared ancestor). An explicit grant always wins.
  • Deleting a parent promotes its children one level; nothing nested is deleted with it. In Design-as-Code the parent travels as parentScreenCode in the screen's file — declarative, so an absent key moves the screen back to top level.

A screen can also bind an automation to run on submit — used together with the screens.open(...) verb (section 9).

5 · Quick View & case controls

A pinned summary, and the built-in properties every case has.

  • Quick View is a side rail shown on every case of the type — the fields that matter most, visible from every view. Configure it on the case-type detail page. It can hold case fields (including scripted fields and whole tables / time records), global variables, and case controls. It doesn't show correspondents, links or assignments.
  • Case controls are the built-in properties every case has, usable wherever fields are and in templates: Reference, Title, Status, Case type, Created, Created by, Last modified, Closed, Case ID.
  • Assignment slots are the case type's named roles (Case Worker, Supervisor…), each a user-picker that can be restricted to a role or team. Templates and scripts read them as {assignment:slot.attribute}.

The case title pattern

Nobody types a case title. Each case type carries a title pattern — set it in the case-type editor under Case title — and every case of that type is titled from it, on create and again on every save. Write plain text and use Insert field to drop in tokens: Injury claim - {client_surname}.

  • A title is re-rendered inside ordinary case saves, so its vocabulary is deliberately narrower than a document's: case fields (with formatters), case controls, {system.today} and correspondent attributes work. Scripted fields, linked-case drills, globals, assignment slots, tables and table views render blank — the picker hides the ones it can.
  • {case.title} is refused: it is the value being built.
  • Leave it blank and cases are titled with the case type's name, so a list never shows an empty row. A pattern whose tokens are all still empty falls back the same way, and a dangling separator is trimmed — a brand-new Injury claim - {client_surname} case reads Injury claim until the surname arrives.
  • Three kinds of case keep a fixed title and never follow the pattern: cases that existed before you set one, cases an automation created with cases.create({ title }), and rows imported with a Title column. What the author wrote wins, permanently.
  • The pattern travels in Design-as-Code as titleTemplate on case-type.json, and Where-used on a field lists the case title, so renaming a field rewrites it.

6 · Lifecycle & field triggers

Bind an automation to a moment in a case's life.

Where: the case-type editor (lifecycle) · the field editor (on-change)

A case type has five lifecycle triggers, each an optional binding to an automation on the same case type:

TriggerFiresCan block?Can prompt?
On createRight after a new case is created — prefill values, greet the creator.NoYes (interactive create)
On openWhen a case is opened in the workspace (once per load).NoYes
On updateAfter each successful save of field values.A cancelled prompt stops the saveYes
On close requestedWhen the user clicks to close the case tab — before it closes.Yes — a throw or cancelled prompt keeps it openYes
On closedWhen marking the case closed / dead.Yes — gates the closeYes

Separately, any field can bind an automation to run on change — it fires when that field's value changes on save (in field order). And any screen can bind an automation to run on submit. All three are just ways to fire an automation; the automation itself is the same kind of script described next.

Not every trigger can pause to ask a question. Field on-change is always quiet — no prompts, and a failure doesn't undo the save. On create is quiet on the plain create path too (only the interactive New-case dialog can prompt). The other lifecycle triggers — On open, On update, On close requested, On closed — are driven from the screen and can prompt; and On update, On close requested and On closed can block the save or the close when the script fails or a prompt is cancelled. Keep ask.* prompts out of field on-change automations.

7 · Automations: the model

An automation is a small JavaScript program that reads and changes a case.

Where: Manage → Playbooks → the case type → Automations

This changed — read this first. Automations used to be built from a visual step/rule builder. They are now code-first: the automation is a JavaScript script. There are wizards and a field picker that write the code for you, and conditions are ordinary JavaScript if / else — but the script is the real thing. Any older documentation describing a "visual rule builder" inside an automation is out of date.

Each automation belongs to one case type and has a Code (how it's referenced), a Name, an optional description, its Script, an Active flag (inactive automations never fire) and a Library flag (section 10).

  • The script runs in a sandbox. Ordinary JavaScript is available (if, loops, variables, Math, JSON, Date, String…), but there is no file system, no fetch, no require — only the platform verbs described here.
  • A script never changes data directly. Each verb queues what you want to happen (a field write, a task, a letter). When the run finishes successfully, the queue is applied in one go. So a half-finished run leaves the case untouched.
  • Within a run, a field you put() and then get() reads back the new value. (Whole tables are the exception — see the next section.)

8 · Reading & writing data (get / put)

Two verbs do all of it, keyed by a token in braces.

get('{token}')          // read a value  (an array for a {…[]} token)
put(value, '{token}')   // write a value (queued until the run ends)

The token in braces is the same field-picker token you see in templates. Reads come back properly typed — a number is a number, a Yes/No is a boolean. Here are the token forms:

TokenWhat it addressesRead / write
{field}A field on this case (the token is its name).read + write
{table[]}A whole Table, as an array of row objects.read + write
{bucket[]}A whole Time record log, as rows.read + write
{view:code[]}A table view's computed rows.read only
{Holder.attr}A detail (email, name, address…) of the correspondent in a link field.read only *
{global.key}A tenant global variable.read + write
{assignment:slot}The user in an assignment slot.read + write
{case.ref}, {case.status}, {system.today}Case & system controls.read only
{field|formatter}A field rendered as formatted text.read only
{link->leaf}Drill through a Case link into the linked case (chainable, up to 10 hops).read + write

* To change who a correspondent holder points at, write the correspondent's id to the bare holder token: put(id, '{Client}').

Working with tables

A {table[]} read gives you an array of row objects. Each row is keyed by the column's exact name (spaces and capitals kept) plus a hidden _id. Writing the array back is a full replace:

  • Rows that keep their _id are updated in place.
  • Rows with no _id are inserted.
  • Rows you leave out are deleted. put([], '{table[]}') clears the table. The array order becomes the table order.

So to add a row you read, push, and put back — as in the worked example in section 11.

Changed: the old typed sub-namespaces are gone. Use the token forms above instead of the removed calls: case_.fields.get/setget/put; case_.iteration_tables.* and case_.time_records.* → the {table[]} / {bucket[]} read-modify-write pattern; globals.get/setget/put('{global.key}'); cases.find(...)query.named('CODE'). The editor's Check button flags any removed call.

9 · The verb reference

Everything a script can do, grouped.

Read, write & log

get('{token}')Read a value (an array for a {…[]} token).
put(value, '{token}')Write a value; queued until the run ends.
log.info / warn / error(msg)Write to the run log (visible in history and the Test Run tab). console.log(...) works too.

This case & the run context

case_.id / .reference / .titleThis case's identity (read-only). For its status use get('{case.status}').
actor.name / actor.emailThe person running the automation.
correspondent, template, phaseIn a template hook: the recipient, the template being actioned, and whether this is the "Before" or "After" phase.
inputArguments passed in when another automation calls this one.

Talking to the operator (interactive runs only)

ask.confirm(label)Yes/No question → a boolean.
ask.text / number / date(label)Prompt for a value.
ask.choice(label, [options])Pick one of a list → the chosen string.
ui.message(text)Show a note and wait for acknowledgement.
ui.viewDocument(attachmentId)Show a case attachment, then resume.
ui.openCase(refOrId)Navigate the operator to a case once the run ends.
screens.open(name)Open a screen as a data-entry dialog mid-run → true if saved.

Correspondence & tasks

actions.send('CODE', {…})Fire a template (letter / email / memo / call / incoming) exactly like the manual action dialog. Common options: holderField or correspondentId (the recipient), description, preview (halt for review — off by default), attach, sendWithFields, email:{ send, to, cc, bcc }, diary, format.
actions.receiptIncoming('CODE', {…})Record an inbound document under an Incoming-post template.
tasks.create(title, {…})Create a task. Options: dueInDays, actionType (Generic / Letter / Email / Memo / PhoneCall / RunAutomation / IncomingPost), template or automation, assignTo, recipientRole, description.
tasks.cancel({…filters})Delete matching open tasks. Always pass a filter — with none it cancels every open task on the case.

Correspondents, other cases & reuse

correspondents.find(numberOrId)Look up a correspondent → its full details (every attribute plus custom) or null.
correspondents.create('Type', {…fields})Create a directory correspondent of that type and get it back — set any attribute (displayName, email, addressLine1…) plus custom for the type's own. Linking to the case stays a separate put(c.id, '{holder}').
correspondents.update(idOrNumber, {…fields})Edit the directory record (every linked case sees it). Omitted keys stay; null/'' clears; the type never changes. There is no delete.
correspondents.search({type?, name?, email?, …})Find directory correspondents by type, name, email, reference or number — the find-or-create guard before a create.
cases.open(refOrId)Open another existing case → a handle with fields.get/set and save(), or null.
cases.create('CaseTypeName', {reference, sibling?})Create a case → its new reference. It materialises after the run, so populate it via a Case-link field and a drilled put. Those writes land before the case exists, so its title pattern renders against them first time. Pass title only to override the pattern — an authored title is then fixed for the life of the case.
cases.close({caseRef?, date?})Close this case (or another).
automation.run('CODE', args?)Run another saved automation inline (it reads args as its input). The drilled form automation.run('{link->automation:CODE}') runs it on the linked case.

Finding cases (read-only)

query.named('CODE', {take?, params?})Run a saved query by code — the way to find cases by their data. Returns matching cases with their values.
query.tasks / history / attachments({…})Read this case's open tasks, history events, or attachment list.

Files, web, dates & helpers

attachments.bundleIntoZip([ids], name)Zip existing attachments into one new attachment.
http.get / post / put / patch / delete(url, …)Call an outside REST service — only hosts on the tenant's allow-list (Organisation settings).
dates.today / now / addDays / addMonths / diffDays / format(…)Date maths and formatting.
helpers.money / pad / upper / lower / trim / isBlankSmall formatting helpers.

With the Accounts add-on, an accounts.* family lets scripts raise invoices, bills and credit notes — see section 26.

10 · When automations run

Eight ways to fire the same script.

Fired byHow you set it upPrompts?
A case-type lifecycle triggerBind it on the case type (on create / open / update / close-requested / closed).Some (see §6)
A field's on-changeBind it on the field; fires when the value changes on save.No
A screen buttonPlace a Button tile; it runs the automation you pick.Yes
A screen's on-submitBind it on the screen; runs after a screens.open submit.
A Run-automation taskCreate a task of kind "Run automation"; actioning it runs the script.Yes
A template Before / After hookBind it on a template; Before can cancel the action, After is best-effort.Yes
A scheduled Auto-routine jobA background job runs a saved query and runs the automation on each matching case.No
By hand — Run / Bulk run / Test runThe case's Automation button, the editor's Bulk run (up to 200 cases), or the Test Run tab.Run & Test: yes; Bulk: no

Interactive vs quiet runs

Some contexts can pause to ask the operator a question; some can't. Interactive runs — a screen button, the Run dialog, a Run-automation task, template hooks, most lifecycle triggers (on open / update / close-requested / closed) and Test runs — support ask.*, ui.message and previews. Quiet runs — field on-change, the plain on-create path, auto-routine jobs and bulk runs — can't prompt, so an ask.* there is skipped and logged. Write quiet automations to decide everything from the data.

Library automations

Tick Library to make an automation a reusable helper. A library automation can only be called from another script via automation.run('CODE') — it's hidden from run buttons, task pickers and bulk run. Use it to share logic across several automations on the case type.

11 · Writing, testing & examples

The editor writes code for you — and never lets you save broken code.

The editor

  • Add a step opens a palette of platform actions (send a letter, put a value, create a diary task, create a case, run a query, ask the user, run another automation, raise an invoice…). Each wizard writes the exact code at your cursor — you can then edit it freely. (Ordinary logic — if, loops, comments — is just JavaScript; the palette only covers the platform actions you couldn't type yourself.)
  • The field picker inserts a get('{token}') for any field, correspondent, global, assignment or whole table; a Snippets menu drops in common patterns (read/put a field, if/else, table read-modify-write, send, a diary date, an HTTP call…).
  • Check runs the parser and the linter against your case type's real design. Anything that would break — an unknown field token, a template code that isn't on this case type, a tasks.cancel with an unknown filter — is a hard error that blocks Save. Softer issues (an option a call ignores) are warnings.
  • Test Run runs the script against a real case reference (or a synthetic test case) without saving anything — prompts and previews behave exactly as live, and the result panel shows the log and the fields it would set.
  • Bulk run runs it across up to 200 cases of the type, committing each case independently.
  • Find — the editor's find bar highlights every match in the open script. To search across automations, the Automation search page (in Playbooks) scans every automation's code case-insensitively, lists per-automation match counts, and opens the editor with the matches highlighted so you can fix things in place.
  • Where used — available on automations (and on templates, saved queries, screens, global variables and correspondent types) — lists every place that references the thing: screen buttons, triggers, other automations, templates… each with click-through. Check it before renaming or deleting anything.

Example — a button that sends a letter, then leaves a reminder

// Interactive run button. Sends the welcome letter to the case's Client
// correspondent, with an operator preview, then leaves a chase reminder.
if (ask.confirm('Send the welcome letter to the client?')) {
  actions.send('LTR-WELCOME', {
    actionKind: 'Letter',        // builder metadata; the engine resolves by code
    holderField: 'Client',       // the Correspondent field on the case
    description: 'Welcome letter to {Client.displayName}',
    preview: true                // halt so the operator can review before it commits
  });

  tasks.create('Chase signed engagement letter', {
    dueInDays: 14,
    actionType: 'Generic',
    assignTo: 'case_worker',     // an assignment-slot code
    description: 'Call the client if the signed letter has not been returned.'
  });
}

Example — a field on-change that raises a task when the status flips

// Bound to the 'Status' field's on-change. When the case moves to
// "Awaiting Medical Report", raise a diary task addressed to the Solicitor.
var status = get('{status}');

if (status === 'Awaiting Medical Report') {
  tasks.create('Request medical report', {
    dueInDays: 7,
    actionType: 'Letter',
    template: 'LTR-MED-REQUEST',   // must exist on this case type
    recipientRole: 'Solicitor'     // a Correspondent holder field on the case
  });
  put(dates.today(), '{report_requested_on}');
}

Example — updating a table and logging time

// Apply a 5% uplift to every non-void row of the Disbursements table,
// drop voided rows, add a new line, then record the time spent doing it.

// --- table: read, change the array in JavaScript, put the FINAL array back ---
var rows = get('{disbursements[]}') || [];
var kept = [];
for (var i = 0; i < rows.length; i++) {
  var row = rows[i];
  if (row['Status'] === 'Void') continue;                 // left out = deleted on put
  row['Net Amount'] = (row['Net Amount'] || 0) * 1.05;    // EXACT column name
  kept.push(row);                                         // keeps _id = update in place
}
kept.push({ 'Description': 'Admin fee', 'Net Amount': 25 }); // no _id = insert
put(kept, '{disbursements[]}');

// --- time record: append one entry to the Attendance log ---
var att = get('{attendance[]}') || [];
att.push({
  started_at: dates.now(),
  ended_at: dates.now(),
  note: 'Fee uplift run'
});
put(att, '{attendance[]}');

log.info('Uplifted', kept.length, 'disbursement rows');

Token names such as disbursements, Status, Client and LTR-WELCOME are examples — they must match your case type's real field / column names and template codes, which is exactly what Check verifies.

12 · Merge tokens & formatters

Placeholders that fill themselves from the case.

Where: Manage → Playbooks → the case type → Templates

Templates (letters, emails, memos) merge case data through single-brace tokens. Token matching is case-insensitive and ignores spaces inside the braces; a field named with spaces uses its snake-case token (Date Of Birth{date_of_birth}). An unknown token renders as nothing — never as literal braces.

To merge…Write
A case field{field_name}
A global variable{global.key} — e.g. {global.firm_name}
A case property (control){case.ref}, {case.title}, {case.status}, {case.created}
A correspondent's detail{Holder.attribute} — e.g. {Client.email}, {Client.address_block}
Through a case link{linkedMatter->claim_amount} (chainable, up to 10 hops)
An assignment slot's user{assignment:case_worker.name}
A table / time-record total{timesheet.count}, {timesheet.sum.hours}, {timesheet.latest.note}
The client-money ledger (Solicitor accounts){ledger.client_balance}, {ledger.uncleared}; statement rows {ledger[].date} / .detail / .dr / .cr / .balance
Today / now / the sender{system.today}, {system.now}, {system.user.name}

Correspondent attributes include displayName (alias name), organisationName, contactPerson, email, phone, mobile, addressLine1/2, address_block (a multi-line postal block), city, postcode, fullAddress, plus any custom field on that correspondent type.

Changed: the merge grammar is single-brace ({claim_amount}), globals use a dot ({global.key}), case properties are the case. family, and correspondents merge through their holder field ({Client.email}). Older docs showing double-brace {{case.field}} or a control: / global: prefix describe the legacy quick-memo engine, not designed templates.

Formatters

Append a pipe to format a value: {token|formatter}. Formatters fold left to right, so {name|trim|upper} is upper(trim(name)). A large selection is built in, including:

  • Text: upper, lower, title, sentence_case, trim, initials, first_word.
  • Dates: date_uk_slash (dd/MM/yyyy), date_long_uk (1 January 2026), date_with_day_name, month_year, date_iso, and many more; plus date-part formatters (day_ordinal, month_name, year_4). A date with no formatter (and no format picked on the field itself) renders dd/MM/yyyy — the UK default everywhere on the platform.
  • Times & date-times: time_24h, time_12h, datetime_uk_slash, datetime_long_uk.
  • Numbers & money: value_with_commas, value_in_words, value_2dp, currency_gbp (or pounds), pounds_in_words, currency_usd, currency_eur.
  • Durations & booleans: duration_hhmm, duration_hours_decimal, yes_no, ticked.
  • For a Dropdown field, {status|code} / {status|description} / a custom column name picks which column shows (Description is the default).

13 · Conditional blocks

One template that adapts to the case's data.

Wrap text in an inline condition so it only appears when the case matches. Blocks can nest, and the first matching branch wins:

{#if claim_amount >= 10000}
   …multi-track paragraph…
{#elseif claim_amount >= 1000}
   …fast-track paragraph…
{#else}
   …small-claims paragraph…
{#endif}

A marker on its own line is removed cleanly (no blank line left behind), and this works in plain text, HTML email/letter bodies, and Word documents — where a block may span whole paragraphs or table rows and the winning branch keeps its formatting — and in Excel cells.

The expression language

A condition is an SQL-like expression (keywords are case-insensitive). Reference a field by name ([Date Of Birth] in brackets if it has spaces), and combine with and / or / not and parentheses:

  • Comparisons: =, !=, >, <, >=, <=.
  • field is empty / field is not empty.
  • field in ('a','b') / field not in (…); field between x and y.
  • field contains 'x' / starts with 'x' / ends with 'x'.
  • Right-hand values: quoted text, numbers, true/false, empty, relative dates today/now (with +N/-N), the current user me, a runtime parameter :name, or another field.

A malformed marker is left as inert text rather than throwing, and a broken expression evaluates to false (so dubious content is simply not shown). Save-time validation flags the first bad expression.

14 · Table views in documents

Turn a case's rows into a table in a letter — including running totals.

A Table View is a named, reusable definition scoped to a case type. It reads a case's real rows (read-only) — from a Table or Time record field, filtered, sorted and with extra computed columns, or from a small script that returns rows. You reference it in a template with the {view:code…} token family:

TokenGives
{view:code} or {view:code.count}The row count.
{view:code.sum.col} / .avg. / .min. / .max.An aggregate over a column.
{view:code.first.col} / .last.colThe first / last row's cell.
{view:code[N].col}The Nth row's cell (1-based).
{view:code[].col}Repeating — put this in one table row in Word/Excel and the engine clones it per row.

Example — a billable-items table in a Word letter

| Date                                           | Description                        | Amount                                     |
| {view:billable[].work_date|date_uk_slash}      | {view:billable[].description}      | {view:billable[].amount|currency_gbp}      |

Items: {view:billable.count}     Total: {view:billable.sum.amount|currency_gbp}

Put the {view:billable[].…} tokens in a single template row inside the Word table; the engine repeats that row for every row in the view. A view can set what happens when it's empty (keep the header, remove the table, or drop in a fallback paragraph), and has a row cap (up to 500) that fails loudly rather than truncating silently.

15 · Template kinds, versions & hooks

One template library per case type.

KindBacked by
MemoRich text authored inline — an internal note-to-file.
LetterA Word (.docx), Excel (.xlsx) or PDF-form file, rendered per case.
EmailRich-text / HTML body, sent through the user's Outlook (or a shared mailbox).
LetterheadA Word file used as the masthead on letters and emails.
Phone callNo body — records a call note (incoming / outgoing / both).
PDF formAn uploaded fixed PDF a Letter fills in (see next section).
Incoming postA named inbound correspondence type.

Versioning

Every save mints a new immutable version; the template points at the current one, and you can revert to any earlier version with no data loss. Crucially, a generated document keeps the version it was made from — editing a template later never alters documents already produced.

Send-with, embed & diary

  • Send-with documents — attach an Embedded-document field's file alongside the correspondence (using the case's per-case copy where present).
  • Embed into — drop the rendered output into an Embedded-document field on the case as well as into history. Tick Lock "Embed into" on the template to fix that destination: the send dialog then shows it read-only and the server refuses a change (automation embedInto overrides stay allowed — they are designer-authored).
  • Diary follow-up — any kind can schedule a follow-up task on action (a number of days out, with an action of your choice); the send dialog lets the user adjust or suppress it.
  • Save as draft — mid-compose, any action (memo, letter, email, call, incoming post) can be parked with one click and resumed later — even after signing out, and even mid-Word-edit (the edited document is kept). Drafts are personal; they live on the case's Drafts panel and on the global Drafts page, and are deleted automatically once sent.

Before / After hooks

A template can run an automation before it's actioned (a failure there cancels the action — useful for validation) and after (best-effort — a failure is logged but never rolls back the correspondence). Point each at a saved automation on the Automation tab. These hooks are the one place a script sees the action's correspondent, template and phase.

16 · Letterheads & PDF forms

Letterheads

A Letterhead is a Word document holding your firm's header and footer (logo, address bar, page setup). Any letter or email template can pick one; at render time Rangeen transplants the letterhead's header/footer onto the letter and inherits its page size and margins, leaving your authored body untouched — "apply the branding, keep my content".

PDF forms

A PDF form template is a fixed PDF (a court or insurer form) you fill from the case. Two ways to fill it:

  • AcroForm fields — if the PDF has form fields, map each one to a token (e.g. a field to {claim_amount}); at send the tokens resolve and the fields fill.
  • Overlay boxes — drop styled text boxes onto specific pages, each bound to a token or literal, with font, size, bold/italic and alignment. At send, every box is editable in a preview so the operator can adjust before committing.

Either way the case data does the filling, and the finished PDF lands on the case.

17 · Global variables

Organisation-wide values, defined once.

Where: Manage → Global variables (edit needs globals.manage)

  • A global is a tenant-wide value — a standard rate, a firm name, a fee amount, an address. Each has a Key (immutable), a label, a type (the same set as case fields) and a value. Mark one as a secret to mask it in the UI (scripts still see the real value).
  • Reference a global in a template as {global.key}, and in an automation as get('{global.key}') / put(value, '{global.key}').
  • Placed on a screen as a Global tile, a global is editable — and saving it changes the value for the whole organisation, not just the case.
Changed: the old globals.get('key') / globals.set(...) script calls are gone — use the get/put('{global.key}') token form.

18 · Lists

Saved questions over the caseload — build once, run forever.

Where: Manage → Manage lists (build) · Lists in the top bar (run) — build needs queries.manage

A query targets one case type and combines criteria — a field, a comparator and a value — joined with And / Or and nested groups, so you can build shapes like (A or B) and (C or D). Besides the case type's own fields you can filter and sort on five built-in columns: Reference, Title, Status, Created and Last modified.

Comparators

Equals, Not equals, Contains, Starts with, Ends with, Greater than, Less than, Greater-or-equal, Less-or-equal, Between, Is blank, Is not blank, In list, Not in list.

Smart values

A value beginning with @ is resolved when the query runs:

Smart valueResolves to
@today, @today+N, @today-NToday's date (± N days).
@now, @now+N, @now-NThe current time (± N minutes).
@me (or @currentuser)Whoever is running the query.
@field:NameThe value of another field on the same case.
@param:NameA parameter the runner is prompted for.

Spelling is forgiving on the date forms: spaces are fine (@today - 90), and on ordered date comparisons (greater/less/between) the bare today ± N without the @ resolves too. On text comparators the literal word "today" stays literal. Comparing a date against a value that isn't a date is simply no match — it never falls back to alphabetical comparison.

Parameters

Declare typed parameters so one query serves every variation — types are Text, Number, Date, Date & time, Time, Yes/No, Option (a dropdown drawn from an Option field) and User. Each can carry a label, a default and a required flag.

Columns, sort, run & export

Choose the result columns and the sort order (multi-field), and whether to include closed cases. Run it from the Lists page, filling any parameters; then export to CSV or Excel (all rows or just the ones you select), or apply a bulk update to the selection. Running needs queries.run; a query never returns a case the runner isn't allowed to see.

Example — "Overdue high-value matters for a fee earner" (Litigation)

One User parameter (owner) and one Number parameter (threshold, default 50,000), reading:

Assignee   equals            @param:owner
AND Target Date  less than    @today
AND Claim Value  >=          @param:threshold
AND ( Status equals 'Open'  OR  Status equals 'On hold' )

Sort by Target Date ascending; show Reference, Title, Claim Value, Target Date, Assignee. (The last group is equivalent to a single Status in list "Open, On hold" criterion.)

19 · Insights

A query, presented properly.

Where: Manage → Manage insights (build) · Insights in the top bar (run) — build needs reports.manage

A report binds a saved query and adds presentation:

  • Columns to project, and an optional inline filter that narrows the query without forking it.
  • Grouping — one or more levels; each level gives a band header per bucket (with its count) and, when you turn on subtotals, a subtotal row per bucket.
  • Roll-ups — Count, Sum, Average, Min or Max on chosen fields, shown as a grand total (and as the per-group subtotals).
  • A chart — Bar, Pie or Line, computed over the whole result set so it never disagrees with the table.
  • Output — a PDF (portrait or landscape, with a subtitle, header and footer), an Excel workbook, or a CSV; plus a max-rows cap.

Whoever runs it fills the query's parameters. Running needs reports.run; building needs reports.manage. A report can be scheduled to run and email the file automatically (next section).

Example — a report on the query above

Group by Assignee (show count + subtotals); roll up Claim Value as Sum, Average and Count; add a Bar chart of summed Claim Value per fee earner; output a landscape PDF. Running it produces one band per fee earner with a subtotal row, a grand total, and the chart above the table.

20 · Routines

Scheduled work, defined and watched in the open.

Where: Manage → Routines (needs jobs.manage)

You can create three kinds of job:

  • Auto routine — run a saved query on a schedule and run an automation on every matching case (with a dry-run option to preview which cases would be hit).
  • Scheduled report — render a report and email the file to a list of recipients.
  • Task digest — the daily "here's what's due" email, per person (or a global digest).

Each job has a Code, name, a cron schedule (five fields, in UTC — with preset buttons like "Daily at 08:00" or "Weekdays 08:00"), an enabled switch, and Run now. A blank schedule means on-demand only. Expanding a job shows its run history: started, trigger (scheduled or manual), status, and match / success / failure counts. The Accounts and Data Export add-ons run their own jobs through this same machinery.

21 · Correspondents & types

The tenant-wide directory of everyone cases deal with.

Where: Manage → Correspondents / Correspondent types (needs correspondents.manage)

  • Correspondent types are yours to define — Solicitor, Hospital, Expert, Court, Insurer, Client — each with a Code, name, icon, and up to 30 custom fields. There's no fixed built-in list.
  • A correspondent belongs to one type and carries a standard set of details (display name, organisation, contact person, full address, email, phone, mobile, website, notes) plus that type's custom fields. Each gets a tenant-wide number.
  • Cases point at correspondents through Correspondent fields on their screens — that's how a case "addresses" a letter or email, and how a correspondent's details merge into templates.
  • A correspondent's page lists the cases they appear on; the same directory feeds every picker in the system.

22 · People, teams & roles

Who's in, how they're grouped, what they may do.

Where: Manage → People / Teams / Roles

People

  • Create a user with an email, name and exactly one role. They receive a branded invitation with a single-use set-your-password link and finish their own setup — the invitation never carries a password.
  • One Microsoft directory backs every organisation a person belongs to, so the same person uses one credential across tenants. "Reset password" re-issues a set-password link; it doesn't change their existing password until they redeem it.
  • People are never deleted (their name is needed in history) — they're disabled. Every account holding a licence counts against your allowance — including accounts used only for background automations — and disabling a user does not free their licence (unassign it to free the seat). External collaborators are not licensed users; they're billed separately. You can't disable your own account or the last enabled administrator.
  • Custom user fields (configured in Organisation settings) add your own attributes to every user — hourly rate, branch, and so on — available to automations and reporting.

Teams

A team groups users for assignment and visibility. A team can carry its own permission grants, which are added to each member's permissions. New users can auto-join a default team.

Roles & how permissions combine

A user's effective permissions are: their one role's grants, plus the grants of every enabled team they're in, plus any extra permissions granted to them individually, minus any revoked from them (a revoke always wins). There are no hidden defaults — the role is the only baseline.

  • Three roles are seeded and can't be deleted (but can be renamed): Tenant Administrator (all permissions), Standard User and Read-only User.
  • The Tenant Administrator role is a wildcard — it holds every permission — and the system always keeps at least one enabled user in it.

Permissions are enforced on the server for every sensitive operation — the menu hiding you see is just convenience on top of that.

23 · Permission reference

The keys you assign to roles and teams.

GroupKeys
Casescases.view, cases.create, cases.edit, cases.assign, cases.close, cases.reopen, cases.delete, cases.bulk, cases.share, cases.protect, cases.protect.manage
Playbooksworkflow.action.email, workflow.action.letter, workflow.action.memo, workflow.action.phone, workflow.action.incoming, workflow.template.manage
Correspondentscorrespondents.view, correspondents.manage
Triagetriage.view, triage.release, triage.archive
To-dostasks.view, tasks.create, tasks.complete, tasks.reassign, tasks.reschedule, tasks.delete.own, tasks.delete.other
Lists & reportsqueries.run, queries.manage, reports.run, reports.manage
Configurationcasetypes.manage, screens.manage, fields.manage, automations.manage, globals.manage, database.manage, jobs.manage
Administrationteams.manage, users.manage, roles.manage, settings.manage, collaborators.manage
Outlookoutlook.connect, outlook.shared.manage
AI Add-onai.use, ai.case, ai.draft, ai.author_templates, ai.author_automations (only when the AI add-on is enabled)
Accounts Add-onaccounts.invoices.view, accounts.invoices.manage, accounts.integration.manage
Data Export Add-ondataexport.run, dataexport.manage

A few notes: rescheduling a task is its own key, separate from creating one; task deletion is split into "own" and "others'"; correspondents, roles, settings and collaborators management are each grantable independently. The AI, Accounts and Data Export groups only appear when the matching add-on is enabled.

Case passwords come as a pair: cases.protect is the lock action inside a case — set a password, change or remove it from in there. Everyone (the setter included) then enters that password each time they open the case, and the server refuses the case's contents until they do. cases.protect.manage is the oversight power: the Protected cases screen listing every protected case with who set its password and when, able to reveal, change or remove any of them without knowing the current one — the recovery path when a password is forgotten. Passwords are deliberately rule-free (whatever was typed, verbatim), stored encrypted, kept out of history entries, and every set / change / removal is written to the case's audit trail.

24 · Organisation settings

Organisation-wide switches, in one place.

Where: Manage → Organisation settings (needs settings.manage)

  • Organisation name — shown in the header, browser title and outbound system emails.
  • Idle sign-out — minutes of inactivity before sign-out; 0 means never (the default, so a daily-use office app doesn't log people out). When set, it's 5–1440 minutes.
  • Default case-list page size (10–500).
  • Case numbering — start from — the floor for the shared six-digit sequence; the next case is this value + 1. It only ever moves forward.
  • Email matcher fields — per case type, the extra fields inbound email is matched on beyond the case reference (including matching on a correspondent's attribute).
  • Quick search — per case type, up to 8 fields the toolbar case search matches in addition to reference and title (which are always matched). Scripted fields and table-member fields can't be chosen. Search covers open and closed cases, never archived ones.
  • Custom user fields — the extra attributes on every user record.
  • Allowed hosts for automation HTTP — the hostnames scripts may call with http.get/http.post. Empty (the default) means outbound HTTP is off; internal / private addresses are refused.

25 · Import, export & design versions

Bulk cases

  • CSV import creates cases in bulk for one case type: a header row naming the fields (plus an optional Title column), then one case per row, up to 5,000 rows. Unknown columns are skipped and logged. This is an administrator operation via the system API.
  • CSV export produces every case of a case type — reference, title, status and every field.
  • For everyday extracts, use Lists → Export and report outputs — those are self-service.

Design versions — restore, export & clone a case type

Rangeen keeps automatic versions of a case type's whole design (its fields, screens, templates, automations and settings) as you change it. From the case type you can:

  • List versions and see who changed what, when.
  • Preview and restore an earlier version — a restore is confirm-gated for anything it would delete, and takes a fresh snapshot first so you can undo the undo.
  • Export a version (or the live design) as a zip, and import a zip to clone it into a brand-new case type — a clean way to move a design between environments or start a new type from a proven one.

26 · Accounts (Xero) Add-on

Invoices, bills and credit notes — kept beside the cases, posted to Xero.

Where: Manage → Accounts (when the add-on is enabled)

Xero-only. The Accounts add-on runs entirely on Xero — there is no built-in ledger to maintain inside Rangeen, no chart of accounts to keep here, and no QuickBooks option. Accounting statements (VAT, P&L, balance sheet, aged reports) live in Xero, not in Rangeen. Enabling "Accounts" simply means this organisation is on Xero.
  • Connect once under Accounts → Xero, pick your Xero organisation, and it stays connected — the connection is kept alive automatically and won't lapse from a quiet month. Only revoking it in Xero or disconnecting here ends it.
  • Documents: raise invoices (money in) and bills (money out) — two directions of one thing — and credit notes. Each flows Draft → Approve, and can be Disputed / cleared / Voided. Push to Xero, and payment status flows back (amount paid, amount due, Xero status).
  • Per-case financials: a money-in / money-out / paid-vs-outstanding rollup for each matter.
  • Mapping: default sales/purchase accounts and tax types, tracking categories, contact matching, and auto-push on approval — configured under Xero settings.

Automations can post accounting documents too, through the same service as the UI — so a script-raised invoice is identical to a hand-raised one:

// Raise a fixed-fee invoice for this case and approve it
var inv = accounts.createInvoice({
  contactName: get('{Client Name}'),
  contactEmail: get('{Client Email}'),
  reference: 'Fixed fee - ' + case_.reference,
  dueInDays: 30,
  approve: true,
  lines: [
    { description: 'Professional services', amount: 750, taxType: 'OUTPUT2' }
  ]
});
log.info('Raised invoice handle ' + inv);

The accounts.* family also includes createBill, createCreditNote, approve, dispute, void, attach and documents().

27 · AI assistants Add-on

Optional writing and answering help — never an actor on its own.

The AI add-on provides focused assistants: a per-case assistant that answers questions about the open case from its own data and history (read-only — it can tell you, never change anything); Write with AI in rich-text editors and a template assistant that drafts letter and email templates; a document generator for producing template documents; and Code AI in the automation editor, which writes and explains automation script against the real API. Usage draws on a monthly credit pool, and an AI usage page shows the balance and recent activity. Everything an assistant writes lands in an editor for a person to review and save — AI can never do anything the signed-in user couldn't do by hand. (Earlier "Designer AI" / "Pro assistant" surfaces have been removed; documentation mentioning them is out of date.)

28 · Client Hub Add-on

External parties see exactly what you share — nothing else.

Where: Manage → Client Hub (admin) · the Share option on a case

  • External users are invited by email and sign in at <workspace>-collaborator.rangeen.com with their own credentials — never with staff accounts.
  • A share grants one external user access to one case. You choose the screens, each set to Hidden, Read-only or Editable, and within an editable screen you can hide or lock individual fields. The share is a frozen snapshot — editing the case type's profile later does not widen existing shares unless you re-snapshot them.
  • Per share you can set an expiry, whether it survives case closure, and the attachment rights (upload / download / see others' uploads). A named collaborator profile per case type gives you a reusable default.
  • In their portal, collaborators get My cases and the shared screens as tabs. Where allowed they edit values (including tables and time records) and upload/download attachments, and they see a filtered activity feed of the correspondence-type events that concern them. They can't see tasks, notes, internal entries (plain to-do completions included), unshared screens, or any other case. Everything they do lands on the case history under their name.
  • A documents conversation — a simple back-and-forth of files per (case, collaborator) — is available on the case's Client Hub view; it's deliberately kept out of the audit/fields history.

29 · Security model

How the system keeps the right people in and everyone else out.

  • Isolation — each organisation's data lives in its own database schema; separation is structural, and the tenant is resolved from the subdomain on every request.
  • Identity — staff sign in with Microsoft Entra (one directory backing many organisations); external collaborators use a separate credential system confined to their portal.
  • Authorisation — role/team permissions are enforced server-side on every sensitive operation; the front-end menu gating is convenience only.
  • Case passwords — a password-protected case's contents are refused by the server (not just hidden by the page) until the user enters its password, and the unlock is dropped again when they leave the case. Reports and lists still count such cases; only opening them is gated. Passwords are stored encrypted and are revealable only on the permission-gated Protected cases screen.
  • Accountability — per-field audit and an immutable case history attribute every change to its actor — person, automation or collaborator — and disabled users are retained so their name persists.
  • Hardening — automation outbound HTTP is off by default and host-allow-listed (with private-address guards), CSV export is protected against formula injection, secrets are masked in the UI, and licence caps and last-admin protection are enforced server-side.
  • Transport & hosting — TLS on every address including each organisation's own subdomain; hosted on Microsoft Azure with staged, health-checked releases.

30 · Design a case type end to end

Putting it together: a "Debt Recovery" case type from nothing to a working workflow.

Step 1 — Create the case type

Configuration → Case types → New. Code DEBT, name "Debt Recovery", multi-user access on.

Step 2 — Define the fields

Data manager, on the DEBT type:

FieldType
Debtor NameText
Original DebtDecimal (2 dp)
Interest RateDecimal (2 dp, 0–100)
Date InstructedDate (default TODAY)
Payment Due DateDate (default TODAY+30d)
StageDropdown — Pre-action / LBA sent / Claim issued / Judgment / Enforcement
Assigned SolicitorCorrespondent (type = Solicitor)
PaymentsTable — columns: Payment Date (Date), Amount (Decimal), Method (Dropdown)
Total PaidScripted → Decimal (sums the Payments table)
Balance OutstandingScripted → Decimal (Original Debt − Total Paid)
Time On MatterTime record (extra columns: billable, hourlyRate)

Total Paid = (get('{Payments[]}') || []).reduce(function (s, r) { return s + (Number(r['Amount']) || 0); }, 0); Balance Outstanding = get('{Original Debt}') - get('{Total Paid}').

Step 3 — Lay out the screens

Screen studio, on DEBT:

  • Overview — Case-control tiles (Reference, Status), the debtor & Stage, Original Debt, Balance Outstanding (read-only), the Assigned Solicitor, Payment Due Date, and an assignment tile for the Case Worker.
  • Payments — a Table tile on Payments, plus Balance Outstanding and a Global tile for the firm's default interest rate.
  • Time & Costs — a Table tile on Time On Matter (its start/stop log), with an authorisation level so only supervisors see it.

Give the Payments screen a visibility rule of Stage is not "Pre-action" so it only appears once action has started.

Step 4 — Add templates

Playbooks → DEBT → Templates: a Letter Before Action (Word letter to the debtor, using {Debtor Name}, {Original Debt|currency_gbp} and a conditional paragraph on {#if [Balance Outstanding] > 5000}), and a Statement of Account letter using a {view:payments…} table view of the Payments table.

Step 5 — Add an automation

Playbooks → DEBT → Automations → New, code SEND-LBA. It sends the Letter Before Action to the debtor, sets the stage, and diarises a chase:

actions.send('LTR-LBA', {
  actionKind: 'Letter',
  holderField: 'Debtor',
  description: 'Letter Before Action to {Debtor Name}'
});
put('LBA sent', '{Stage}');
tasks.create('Chase response to LBA', {
  dueInDays: 14,
  actionType: 'PhoneCall',
  assignTo: 'case_worker'
});

Run Check, then Test Run against a real DEBT case, then place a Primary button "Send Letter Before Action" on the Overview screen that runs SEND-LBA.

Step 6 — Wire triggers

On the case type, bind On create to an automation that sets Stage = "Pre-action" and Payment Due Date = TODAY+30d, and bind On close requested to one that blocks closing while there's a balance:

if (Number(get('{Balance Outstanding}')) > 0) {
  ui.message('Cannot close — a balance of ' +
             get('{Balance Outstanding|currency_gbp}') + ' is still outstanding.');
  throw new Error('Balance outstanding');   // a throw here keeps the case open
}

Step 7 — Add queries & a report

Build a query "Overdue debts" (Payment Due Date < @today AND Balance Outstanding > 0, including a Stage parameter), and a report on it grouped by Stage with a Sum of Balance Outstanding and a bar chart. Optionally add an Auto-routine job that runs "Overdue debts" every morning and fires a "chase" automation on each match.

That's a complete case type — data, screens, letters, automation, lifecycle rules and reporting — built from the pieces in this handbook. Every change you made was captured as a design version you can restore.