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.
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:
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.
The mould for each kind of matter.
Where: Sidebar → Configuration → Case types
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.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.The data model is yours — defined field by field in Database Management.
Where: Sidebar → Database Management (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.
| Type (as you pick it) | Holds | Settings |
|---|---|---|
| Text | Free text — names, notes, references. | Optional max length (1–10,000). |
| Whole number | An integer, no decimals. | Optional min / max. |
| Decimal | A number with decimals — money, rates, measurements. | Min / max; decimal places (0–10, default 2). |
| Date | A calendar date, with a picker. | Allow past / future; default fixed or TODAY±N. |
| Date & time | A date plus a time of day. | As Date, plus an optional @HH:mm default. |
| Time | A time of day on its own. | Optional default time. |
| Yes / No dropdown | Three states: blank, Yes, or No. | Optional default. |
| Checkbox | Two states — ticked or not. | Default checked / unchecked. |
| Dropdown | A 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 link | A link to a case of another type; its fields are reachable in templates & scripts. | The linked case type. |
| Correspondent | A "holder" pointing at one correspondent of a chosen type. | The linked correspondent type. |
| Table | A repeating sub-table on the case — you define its columns. | Member columns (see below). |
| Time record | A start/stop time log, with optional extra columns. | Member columns (see below). |
| Embedded document | A document slot on the case, with a template document. | File uploaded in Database Management. |
billable, hourlyRate,
reason) — templates can sum these for invoicing.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.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)
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.
Lay the fields out the way the work reads.
Where: Sidebar → Screen Designer (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:
| Tile | Places |
|---|---|
| Field | A case field as an editable input. Also used for linked fields, embedded documents and correspondent holders. |
| Label | Static text — with a chosen size and colour. |
| Button | A button that runs an automation. Style: Primary / Secondary / Danger. |
| Table | A sub-table (add / edit / delete rows). Also the tile used to place a Time record's start/stop log. |
| Web viewer | An embedded web page (an http(s) URL). |
| Image viewer | A static uploaded image. |
| Correspondent attribute | One detail of a linked correspondent (name, email, address…). |
| Global | A tenant global variable — editable (saving writes back to the global for the whole organisation). |
| Case control | A system property (reference, status, dates…) — always read-only. |
| Assignment | A detail of the user in an assignment slot — always read-only. |
A screen can also bind an automation to run on submit — used together with
the screens.open(...) verb (section 9).
A pinned summary, and the built-in properties every case has.
{assignment:slot.attribute}.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:
| Trigger | Fires | Can block? | Can prompt? |
|---|---|---|---|
| On create | Right after a new case is created — prefill values, greet the creator. | No | Yes (interactive create) |
| On open | When a case is opened in the workspace (once per load). | No | Yes |
| On update | After each successful save of field values. | A cancelled prompt stops the save | Yes |
| On close requested | When the user clicks to close the case tab — before it closes. | Yes — a throw or cancelled prompt keeps it open | Yes |
| On closed | When marking the case closed / dead. | Yes — gates the close | Yes |
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.
ask.* prompts out of field
on-change automations.An automation is a small JavaScript program that reads and changes a case.
Where: Sidebar → Workflow → the case type → Automations
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).
if, loops, variables, Math, JSON,
Date, String…), but there is no file system, no
fetch, no require — only the platform verbs described
here.put() and then get() reads
back the new value. (Whole tables are the exception — see the next section.)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:
| Token | What it addresses | Read / 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}').
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:
_id are updated in place._id are inserted.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.
case_.fields.get/set → get/put;
case_.iteration_tables.* and case_.time_records.* →
the {table[]} / {bucket[]} read-modify-write pattern;
globals.get/set → get/put('{global.key}');
cases.find(...) → query.named('CODE'). The editor's
Check button flags any removed call.Everything a script can do, grouped.
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.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.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.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.find(numberOrId)Look up a correspondent → its details or null.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.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.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.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 (Tenant 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.
Eight ways to fire the same script.
| Fired by | How you set it up | Prompts? |
|---|---|---|
| A case-type lifecycle trigger | Bind it on the case type (on create / open / update / close-requested / closed). | Some (see §6) |
| A field's on-change | Bind it on the field; fires when the value changes on save. | No |
| A screen button | Place a Button tile; it runs the automation you pick. | Yes |
| A screen's on-submit | Bind it on the screen; runs after a screens.open submit. | — |
| A Run-automation task | Create a task of kind "Run automation"; actioning it runs the script. | Yes |
| A template Before / After hook | Bind it on a template; Before can cancel the action, After is best-effort. | Yes |
| A scheduled Auto-routine job | A background job runs a saved query and runs the automation on each matching case. | No |
| By hand — Run / Bulk run / Test run | The case's Automation button, the editor's Bulk run (up to 200 cases), or the Test Run tab. | Run & Test: yes; Bulk: no |
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.
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.
The editor writes code for you — and never lets you save broken code.
if,
loops, comments — is just JavaScript; the palette only covers the platform
actions you couldn't type yourself.)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…).tasks.cancel with an unknown filter — is a
hard error that blocks Save. Softer issues (an option a call ignores) are
warnings.// 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.'
});
}
// 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}');
}
// 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.
Placeholders that fill themselves from the case.
Where: Sidebar → Workflow → 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.reference}, {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} |
| 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.
{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.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:
upper, lower, title,
sentence_case, trim, initials,
first_word.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).time_24h, time_12h,
datetime_uk_slash, datetime_long_uk.value_with_commas,
value_in_words, value_2dp, currency_gbp
(or pounds), pounds_in_words,
currency_usd, currency_eur.duration_hhmm,
duration_hours_decimal, yes_no, ticked.{status|code} / {status|description}
/ a custom column name picks which column shows (Description is the default).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.
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:
=, !=, >,
<, >=, <=.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'.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.
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:
| Token | Gives |
|---|---|
{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.col | The 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.
One template library per case type.
| Kind | Backed by |
|---|---|
| Memo | Rich text authored inline — an internal note-to-file. |
| Letter | A Word (.docx), Excel (.xlsx) or PDF-form file, rendered per case. |
| Rich-text / HTML body, sent through the user's Outlook (or a shared mailbox). | |
| Letterhead | A Word file used as the masthead on letters and emails. |
| Phone call | No body — records a call note (take / make / both). |
| PDF form | An uploaded fixed PDF a Letter fills in (see next section). |
| Incoming post | A named inbound correspondence type. |
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.
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.
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".
A PDF form template is a fixed PDF (a court or insurer form) you fill from the case. Two ways to fill it:
{claim_amount}); at send the tokens resolve and
the fields fill.Either way the case data does the filling, and the finished PDF lands on the case.
Organisation-wide values, defined once.
Where: Configuration → Global variables (edit needs globals.manage)
{global.key}, and in an
automation as get('{global.key}') /
put(value, '{global.key}').globals.get('key') /
globals.set(...) script calls are gone — use the
get/put('{global.key}') token form.Saved questions over the caseload — build once, run forever.
Where: Queries → Manage queries (build) · Queries (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.
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.
A value beginning with @ is resolved when the query runs:
| Smart value | Resolves to |
|---|---|
@today, @today+N, @today-N | Today's date (± N days). |
@now, @now+N, @now-N | The current time (± N minutes). |
@me (or @currentuser) | Whoever is running the query. |
@field:Name | The value of another field on the same case. |
@param:Name | A parameter the runner is prompted for. |
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.
Choose the result columns and the sort order (multi-field), and
whether to include closed cases. Run it from the Queries 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.)
A query, presented properly.
Where: Reports → Manage reports (build) · Reports (run) — build needs reports.manage
A report binds a saved query and adds presentation:
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.
Scheduled work, defined and watched in the open.
Where: Sidebar → Background jobs (needs jobs.manage)
You can create three kinds of job:
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.
The tenant-wide directory of everyone cases deal with.
Where: Sidebar → Correspondents · types under Configuration (needs correspondents.manage)
Who's in, how they're grouped, what they may do.
Where: Sidebar → Configuration → Users / Teams / Roles
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.
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.
Permissions are enforced on the server for every sensitive operation — the menu hiding you see is just convenience on top of that.
The keys you assign to roles and teams.
| Group | Keys |
|---|---|
| Cases | cases.view, cases.create, cases.edit, cases.close, cases.reopen, cases.delete, cases.bulk |
| Workflow | workflow.action.email, workflow.action.letter, workflow.action.memo, workflow.action.phone, workflow.action.incoming, workflow.template.manage |
| Correspondents | correspondents.view, correspondents.manage |
| Triage | triage.view, triage.release, triage.archive |
| Tasks | tasks.view, tasks.create, tasks.complete, tasks.reassign, tasks.reschedule, tasks.delete.own, tasks.delete.other |
| Queries & reports | queries.run, queries.manage, reports.run, reports.manage |
| Configuration | casetypes.manage, screens.manage, fields.manage, automations.manage, globals.manage, database.manage, jobs.manage |
| Administration | teams.manage, users.manage, roles.manage, settings.manage, collaborators.manage |
| Outlook | outlook.connect, outlook.shared.manage |
| AI Add-on | ai.use, ai.ask, ai.draft, ai.author_templates, ai.author_automations, ai.design, ai.pro… (only when the AI add-on is enabled) |
| Accounts Add-on | accounts.invoices.view, accounts.invoices.manage, accounts.integration.manage |
| Data Export Add-on | dataexport.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.
Organisation-wide switches, in one place.
Where: Configuration → Tenant settings (needs settings.manage)
http.get/http.post. Empty (the default) means outbound
HTTP is off; internal / private addresses are refused.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.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:
Invoices, bills and credit notes — kept beside the cases, posted to Xero.
Where: Sidebar → Accounts (when the add-on is enabled)
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().
Optional help answering about a case and building the system.
The AI add-on adds a few assistants: a per-case assistant that answers questions about the case you have open; a Pro / Builder assistant for cross-case work and building; and a Designer AI pinned to a case type that helps construct that case type. Usage draws on a monthly credit pool, and an AI usage page shows the balance and recent activity. AI is gated by permission and credits, and its changes are subject to your approval — but it can never do anything a user couldn't do by hand. (This handbook keeps AI coverage light on purpose; the assistants are optional and self-explanatory in use.)
External parties see exactly what you share — nothing else.
Where: Sidebar → Collaborators (admin) · the Share option on a case
How the system keeps the right people in and everyone else out.
Putting it together: a "Debt Recovery" case type from nothing to a working workflow.
Configuration → Case types → New. Code DEBT, name "Debt Recovery",
multi-user access on.
Database Management, on the DEBT type:
| Field | Type |
|---|---|
| Debtor Name | Text |
| Original Debt | Decimal (2 dp) |
| Interest Rate | Decimal (2 dp, 0–100) |
| Date Instructed | Date (default TODAY) |
| Payment Due Date | Date (default TODAY+30d) |
| Stage | Dropdown — Pre-action / LBA sent / Claim issued / Judgment / Enforcement |
| Assigned Solicitor | Correspondent (type = Solicitor) |
| Payments | Table — columns: Payment Date (Date), Amount (Decimal), Method (Dropdown) |
| Total Paid | Scripted → Decimal (sums the Payments table) |
| Balance Outstanding | Scripted → Decimal (Original Debt − Total Paid) |
| Time On Matter | Time 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}').
Screen Designer, on DEBT:
Give the Payments screen a visibility rule of Stage is not "Pre-action"
so it only appears once action has started.
Workflow → 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.
Workflow → 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.
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
}
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.