Configuration & Technical Handbook

This is the complete reference for the people who design and configure CaseOnGo — 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 Designer The automation language Templates & tokens Queries · Reports · Background jobs Users · 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 CaseOnGo, 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 (the Database Management).
  • Screens — how those fields are laid out (Screen Designer).
  • Templates — the letters, emails and memos it can produce (Workflow).
  • Automations — the scripted routines that run for it (Workflow → Automation).
  • Tasks & Queries, assignment slots, quick-view config, collaborator profiles, and its trigger bindings.

A few things are organisation-wide rather than per case type: the correspondent directory and its types, global variables, people / 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 and Reports. Section 31 walks that path end to end.

Every configuration surface named in this handbook lives in the sidebar, under its grouped heading. Entries appear only for people holding the matching permission (section 23).

CaseOnGo 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: Sidebar → Configuration → 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: 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 List pickers but keeps its existing cases and history. A case type can't be deleted while any case, screen, workflow, field or List still references it.
  • Numbering is organisation-wide. Case types don't carry their own prefix or sequence — every case, whatever its type, takes the next number from one shared six-digit sequence (see Tenant settings). Matters (sibling cases) are numbered under their root as 000123.001.
  • The case-type editor is also where you bind the seven trigger automations (section 6), configure Quick View and assignment slots (section 5) and keep collaborator profiles (section 29).
Deleting cases is permission-gated (cases.delete) and deliberately heavy: the confirm dialog requires typing the case's reference, and deleting the root of a family warns, names every matter, and then deletes the whole family together — rows, history and every stored file, permanently. There is no recycle bin.

3 · Fields & data types

The data model is yours — defined field by field in the 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, and any field can bind an on-change automation (section 6).

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 the Database Management.

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. Each column has an Editable toggle in the tile's table settings: switched off, the column shows greyed-out with a lock in the add/edit-row dialog — people can't type it, but field defaults, automations and the API still fill it. Use it for machine-written columns like a computed rate or a stamped date.

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. Scripted fields also work in Queries, templates and Quick View.

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. Dates are entered and shown dd/MM/yyyy platform-wide; a blank date box saved over a stored date clears it.

4 · Screen Designer

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, listed in the case workspace's Screens panel. 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…).
GlobalAn organisation-wide global variable — editable (saving writes back to the global for everyone).
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 panel — 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). On phones (<600px) a designed screen re-flows automatically into a single column in reading order — you don't design a separate mobile layout.

5 · Quick View & case controls

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

  • Quick View is the strip of Label: Value chips in the case bar, visible whatever view the user is in. 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}; tasks can be assigned to a slot so they follow whoever holds it.

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 · Case & field triggers

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

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

A case type has seven trigger bindings, each an optional link to an automation on the same case type. The labels below are exactly what the editor shows:

TriggerFiresCan block?
When a case is being createdRight after a new case is created — prefill values, greet the creator. The interactive New-case path can prompt; other create paths run quietly.No
When a case is openedWhen a case is opened in the workspace — once per open (not re-fired while the case stays in the open-cases rail).No
Before SaveBefore a save commits. get() still reads the OLD values; changes.next() reads what's being saved. stop('reason') rejects the whole save.Yes — blocks the save
After SaveAfter the save has committed — get() now returns the new data. Good for follow-on work; it cannot undo the save.No
When the user clicks CloseWhen the user closes the case's card in the open-cases rail — before it closes.Yes — a failed run or cancelled prompt keeps it open
Before marking the case as deadWhen marking the case dead (closing it for good).Yes — a throw or cancelled prompt prevents it
On accounts document paidWhen a linked accounting document is paid (CaseOnGo Accounts add-on). Always unattended; the document's details arrive as input.No

Separately, any field can bind an automation to run on change — it fires after the save commits when that field's value changed. Inside it, stop('reason') reverts that field alone to its pre-edit value while the rest of the save stands. A field that already fired doesn't re-fire in the same pass, and a save caps out at 50 on-change invocations — so chains can't loop forever. And any screen can bind an automation to run on submit. All of these are just ways to fire an automation; the automation itself is the same kind of script described next.

Whether a trigger can ask questions depends on who started the run, not on the trigger. Runs driven from someone's browser — opening, saving, closing, pressing a button — are interactive: ask.* and ui.message pause and show to that person (including collaborators on shared screens). Unattended runs — scheduled Background jobs, bulk runs, API saves, accounts-paid events — can't prompt: an ask.* there is skipped and logged. Guard with user.isSignedIn when one script serves both.

7 · Automations: the model

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

Where: Sidebar → Workflow → the case type → Automation tab

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, and the editor never lets a broken one save. 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, declared Parameters (its "function signature" — see section 9), an Active flag (inactive automations never fire, even from a trigger) 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, no browser globals — only the platform verbs described here. Limits: about 30 seconds per interactive run, 5 million statements, tables up to 10,000 rows.
  • 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.)
  • When a prompt pauses the run, everything queued so far is checkpointed; on answer the script re-runs from the top with earlier answers replayed deterministically — a question is never asked twice, and an http.* call is never repeated.

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}An organisation-wide global variable.read + write
{assignment:slot}The user in an assignment slot.read + write
{case.ref}, {case.status}, {system.today}Case & system controls (also case.title, case.casetype, case.created, case.modified, case.closed, case.id; the longhand case.reference also works).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.

Writing a Dropdown field accepts the option's code or its description (the canonical code is stored); an unknown value fails with the valid choices listed. put(x, '{status|description}') matches against a specific column.

9 · The verb reference

Everything a script can do, grouped. The editor's Help tab carries the same reference, generated from the live API.

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.

Detecting changes (Before Save / After Save only)

changes.any()Did this save change anything?
changes.has('{token}')Did this field change? For a {table[]} token: was any row added, edited or deleted?
changes.prev('{token}') / changes.next('{token}')The value before / after the save. In Before Save, get() still returns the old value — next() is what's being written.
changes.fields()The list of changed tokens. Empty outside the two save triggers.

This case & the run context

userWho's running it: user.id (null in unattended runs — the safe gate), .name, .email, .isSignedIn, .isInternalUser, .isExternalUser (a collaborator), .hasRole(...) / .inTeam(...) (staff only), .externalRole().
actor.name / actor.emailShorthand for the 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.
args / inputargs.Name reads a declared parameter, typed and defaulted (a missing required parameter fails the run before it starts); input is the raw value a calling automation passed.

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.
stop('reason')A clean, deliberate halt — not an error. Before Save: rejects the save. A field's on-change: reverts just that field. A template's Before hook: skips the action. In a Routine: skips that one case. The reason shows as a toast and is recorded on the case's audit trail. Work already queued before the stop is not undone; a throw is a crash logged red, by contrast.

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 operator review — off by default, skipped unattended), attach, embedInto, sendWithFields, attachHistory, email:{ send, to, cc, bcc } (the object's presence turns real Outlook sending on; omit it for record-only), diary (false, or a follow-up override), targetCase, format ('pdf' | 'docx' | 'inline').
actions.receiptIncoming('CODE', {…})Record an inbound document under an Incoming-post template.
tasks.create(title, {…})Create a task. Options: dueInDays, actionType ('Generic' | 'Memo' | 'Letter' | 'Email' | 'PhoneCall' | 'RunAutomation' | 'IncomingPost'), template or automation, assignTo (a user's email, or an assignment-slot code — a slot binds the task to whoever holds the slot), recipientRole (a Correspondent field name), description.
tasks.cancel({…filters})Delete matching open tasks. Filters: actionType, recipientRole, templateCode, assigneeEmail, assignmentSlot, titleContains, caseRef. 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 (or a sibling case with sibling: true) → its new reference. The case materialises after the run ends, so cases.open can't reach it mid-run — populate it via a Case-link field and drilled put calls (put(newRef, '{link}'), then put(v, '{link->field}')). Those drilled 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 (depth cap 5); the callee reads args. The drilled form automation.run('{link->automation:CODE}') runs it on the linked case — its writes, tasks and sends land there.

Finding cases (read-only)

query.named('CODE', {take?, params?, caseType?})Run a saved List by code — the way to find cases by their data. params answers its questions; caseType runs a List defined on another case type. Default 500 rows, cap 10,000. Rows: { caseId, reference, title, status, modifiedAt, 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 organisation's allow-list (Tenant settings → Outbound HTTP). Each request runs exactly once per run; the response is { status, body, data, headers, truncated }, and failures are catchable with try/catch.
dates.today / now / addDays / addMonths / diffDays / format(…)Date maths and formatting.
helpers.money / pad / upper / lower / trim / isBlankSmall formatting helpers.

With CaseOnGo Accounts, an accounts.* family lets scripts raise invoices, bills and credit notes — see section 27.

10 · When automations run

Many ways to fire the same script.

Fired byHow you set it upInteractive?
A case-type triggerBind it on the case type — the seven bindings of section 6.When a person drove it
A field's on-changeBind it on the field; fires after the save commits when the value changed.When a person saved
A screen buttonPlace a Button tile; it runs the automation you pick — for staff, and for collaborators on shared screens.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 (stop), After is best-effort.Yes
A scheduled Auto routineA Routine pairs a saved List with an automation on a cron schedule (section 20).No — always quiet
By hand — Run / Bulk run / Test RunThe case's Automation action (needs cases.edit), the editor's Bulk run (needs cases.bulk), or the Test Run tab.Run & Test: yes; Bulk: no

Interactive vs quiet runs

The rule is simple: if a signed-in person's browser drove the run, prompts work; if nothing did (a schedule, a bulk run, an API call), they don't — an ask.* there is skipped and logged with a clear error. Write quiet automations to decide everything from the data, and gate mixed-audience scripts with user.isSignedIn / user.isExternalUser.

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 List, 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 a typo'd filter key — is a violation that blocks Save, exactly like a syntax error. Softer issues are warnings and never block. Only literal strings are checked, so runtime-built values can't false-positive.
  • 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. Past test runs are kept per automation, newest first.
  • Bulk run runs it across cases of the type, committing each case independently (needs cases.bulk).
  • Find & Automation search — the editor's find bar highlights matches in the open script. To search across automations, the Automation search entry in the Manage menu scans every automation's code case-insensitively, lists per-automation match counts, and opens the editor with the matches highlighted.
  • 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, Background jobs… each with click-through. Check it before renaming or deleting anything.
  • Code AI Add-on — with CaseOnGo AI, an assistant in the editor writes and explains automation script against the real API. You still read, test and save it yourself.

Example — escalate when a value crosses a threshold (bind to After Save)

if (changes.has('{claim_value}')) {
  var v = changes.next('{claim_value}');
  if (v > 25000) {
    put('supervisor@yourfirm.co.uk', '{assignment:supervisor}');
    tasks.create('Review high-value claim', {
      dueInDays: 2,
      actionType: 'Generic',
      assignTo: 'supervisor'          // an assignment-slot code
    });
  }
}

Example — a scheduled chase letter (run by an Auto routine)

// Quiet run: no prompts. stop() skips just this case in the routine.
var due = get('{report_due_date}');
if (helpers.isBlank(due) || dates.diffDays(due, dates.today()) < 7) {
  stop('Not yet 7 days overdue.');
}
actions.send('LTR-CHASE', { actionKind: 'Letter', holderField: 'Client' });
put(dates.today(), '{last_chased}');

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');

Example — calling a webhook (host must be allow-listed)

try {
  var res = http.post('https://hooks.example.com/case-events', {
    reference: get('{case.ref}'),
    status: get('{case.status}')
  });
  if (res.status >= 300) { log.warn('Webhook responded ' + res.status); }
} catch (e) {
  log.error('Webhook failed: ' + e.message);
}

Token names such as disbursements, Status, Client and LTR-CHASE 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: Sidebar → Workflow → the case type → Memos / Letters / Emails…

Templates (letters, emails, memos) merge case data through single-brace tokens. Every editor has an Insert field button so you rarely type tokens by hand. 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
An embedded document's details{contract|filename}, {contract|size_human}, {contract|download_url}
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.

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.

This is the same condition language used by screen visibility rules and List criteria — learn it once, use it in all three places. 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 · Tables in documents

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

A Table View is a named, reusable definition scoped to a case type (Workflow → Table views). 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. Reference it 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.
{#table:code}Email/HTML bodies: expands into a complete, styled HTML table. {#table:code|colB,colA} picks and orders columns.

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. In an email body, {#table:billable} drops the whole table in one token. Rendering order is fixed: conditionals expand first, then tables, then tokens — so a table inside a losing {#if} branch never renders. A view can set what happens when it's empty (keep the header, remove the table, or drop in a fallback paragraph — the fallback may itself contain tokens), 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 — the Workflow tabs.

TabBacked by
MemosRich text authored inline — an internal note-to-file.
LettersA Word (.docx), Excel (.xlsx) or PDF-form file, rendered per case.
EmailsRich-text / HTML body, sent through the user's Outlook (or a shared mailbox).
Letter headsA Word file used as the masthead on letters.
Phone callsNo body — records a call note (incoming / outgoing / both).
FormsAn uploaded fixed PDF a Letter fills in (see next section).
Incoming PostA named inbound correspondence type — recording one can fire automation hooks.
AutomationThe case type's automations (sections 7–11).
Table viewsThe reusable row-set definitions of section 14.

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. Binary templates (Word/Excel) are edited round-trip: open locally, save, re-upload to land a new version.

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 stop('reason') 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. These hooks are the one place a script sees the action's correspondent, template and phase.

Senders

Email templates deliberately carry no send-from setting — the sender is chosen at send time: the operator's own connected mailbox or a shared mailbox they can send from. For unattended sends by Background jobs, the sender comes from the job server's Sends email as setting (section 20).

16 · Letter heads & PDF forms

Letter heads

A Letter head is a Word document holding your firm's header and footer (logo, address bar, page setup). Any letter template can pick one; at render time CaseOnGo transplants the letter head'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 Form 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: Configuration → Global variables (edit needs globals.manage)

  • A global is an organisation-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}'). Queries can filter on global.key too.
  • 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.

18 · Queries

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

Where: Queries → Manage queries (build, needs queries.manage) · Queries (run, needs queries.run)

A List targets one case type and has an immutable Code, a name, criteria, display columns, a multi-field sort, an Include closed cases flag (closed cases are excluded by default) and an Active flag — inactive Queries are hidden from the run page. Criteria combine a field, a comparator and a value, joined with And / Or (And binds tighter), grouped with brackets to any depth, and any criterion or group can be negated with Not — so shapes like not (A or B) and C are expressible.

You can author criteria two ways — the click-builder or the typing view — and switch between them freely. The typed grammar is the same condition language as template conditionals and screen visibility rules (section 13).

Comparators

Equals, Not equals, Contains, Starts with, Ends with, Greater than, Less than, Greater-or-equal, Less-or-equal, Between, Blank, Not blank, In list, Not in list (comma-separated, case-insensitive). Ordered comparisons need both sides present: a case with a blank field never matches >= / Between, and a date compared against non-date text is simply no match — never an alphabetical accident.

What you can filter, show and sort on

Beyond the case type's own fields (scripted fields included — evaluated per case at run time), criteria, columns and sort can use an extended vocabulary:

FormMeaning
case.ref, case.title, case.status, case.casetype, case.created, case.createdby, case.modified, case.closedThe built-in case controls.
assignment:slot.attrThe user in an assignment slot — name, email, job title, or a custom user field.
Holder.attrA correspondent attribute through a holder field — client.city, solicitor.postcode
global.keyAn organisation-wide global.
@field:NameAnother field on the same case (field-vs-field comparison).

Smart values

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 List — and @me.email, @me.name, @me.job_title or a custom user field compare an attribute instead, so one shared "my open cases" List works for everyone.
@param:NameA question the runner answers (typed :name in the text view).

Spelling is forgiving on the date forms: spaces are fine (@today - 90), and on ordered date comparisons the bare today ± N without the @ resolves too. Under Equals, the literal word "today" stays literal.

Questions asked at run time

Declare typed questions so one List serves every variation — answer types are Text, Number, Date, Date & time, Time, Yes/No, Option (a dropdown drawn from an Option field) and User; each carries the wording shown to the runner, a required flag and an optional default. A Detect button auto-declares keys the criteria already reference.

Optional-question semantics — worth knowing exactly. A required question left blank refuses to run. An optional question left blank drops its criterion from the List before any case is tested — it doesn't match-all or match-nothing, it simply isn't used to filter. A Between with one bound answered degrades to the one-sided comparison; a group whose every child dropped is dropped whole; if everything drops, the List runs unfiltered.

Running, exporting & bulk actions

The run page shows Case key, Title and Status plus the List's columns — every header sorts; pages are 25–500 rows. Results are access-filtered: a List never returns a case the runner isn't allowed to see, and neither do its exports. Export to Excel or CSV (all matches or the selection, up to 10,000 rows), apply a Bulk edit (set one field across the selection — needs cases.bulk), or Run automation across the matches. Automations reach the same engine with query.named (section 9) — including Queries on other case types.

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

One User question (owner) and one Number question (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. Leave threshold optional and a blank answer simply stops filtering by value.

19 · Reports

A List, presented properly.

Where: Reports → Manage reports (build, needs reports.manage) · Reports (run, needs reports.run)

An Insight binds a saved List and adds presentation:

  • Columns to project, and an optional inline filter that narrows the List without forking it (always AND-combined).
  • 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 (default 5,000, up to 50,000).

Whoever runs it answers the List's questions. An Insight can be scheduled as a Routine (next section) that renders the file and emails it to a recipient list — the first recipient in To, the rest blind-copied so the list never leaks. One caveat for schedules: @me has no meaning in an unattended run (there's no signed-in user), so a scheduled Insight built on a "my cases" List matches nothing — give the List a User question instead and fix its value on the schedule.

20 · Background jobs & job servers

Scheduled work, defined and watched in the open.

Where: Sidebar → Background jobs (needs jobs.manage) — two tabs: Jobs and Servers

The Jobs tab

You can create three kinds of job (the editor offers them exactly so):

  • Auto routine — saved query + automation: on each run, execute a saved List and run an automation on every matching case. A Dry-run by default checkbox, and a per-row Dry run button, list the matched cases without executing — dry run exists only for Auto routines (a "dry" report or digest would email people for real, so the server refuses it).
  • Scheduled report — render and email: render an Insight and email the file (report default / PDF / Excel / CSV) to a comma-separated recipient list, with an optional subject and question answers (supporting @today / @today-N).
  • Task digest — daily task email: the "here's what's due" email — per-recipient (only their own tasks) or firm-wide, optionally including unassigned tasks.

Every job has a Code (immutable), name, an enabled toggle, and a cron schedule (five fields, evaluated in UTC, with quick-picks like "Daily at 08:00" and "Weekdays 08:00"). A blank schedule means on-demand only. Run now queues the job immediately on the free on-demand lane and toasts the outcome ("3 matched, 3 succeeded, 0 failed"). Run history shows the newest 50 runs — started, trigger, which server it ran on, duration, status, matched/OK/failed — and each run expands to a per-item log. Deleting a job asks you to type its code and removes its history with it.

Accounting sync and Data export jobs are created by their add-ons — you can rename, reschedule and enable/disable them, nothing more.

The Servers tab

  • Scheduled jobs run on your organisation's job servers — capacity that comes with your plan. Each server runs one job at a time; jobs queue behind each other. The On-demand lane card is always there: Run-now clicks and system jobs ride it for free and never occupy a server.
  • With no servers on the plan, scheduled jobs won't run — the page says so plainly and Run now still works. Adding servers is a plan change ("contact us to add one to your plan").
  • Each server card shows what's running (with elapsed time and a Cancel), what's queued, and its email sender. The settings dialog holds a Display name ("Compliance server") and Sends email as — the Outlook mailbox unattended email steps send from on this server. Candidates are enabled users with a connected Outlook mailbox; the page recommends a dedicated account (e.g. automations@yourfirm) so unattended email stays separate from personal mailboxes. Note that such an account is an ordinary licensed user (section 22).
  • A job can pin to a server with the editor's Runs on picker, or take Next available server. An Auto routine's email steps send from the server the run lands on; with no sender configured anywhere, unattended email steps fail with a clear error rather than sending as nobody.

21 · Correspondents & types

The organisation-wide directory of everyone cases deal with.

Where: Sidebar → Correspondents · types under Configuration (needs correspondents.manage to edit)

  • 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 an organisation-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 · Users, teams & roles

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

Where: Sidebar → Configuration → Users / Teams / Roles

Users

  • Create a user with an email, name and exactly one role (plus optional job title, case-worker flag, and any custom user fields your organisation defined). They receive a branded invitation with a single-use set-your-password link (valid 7 days) 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 workspaces. Reset password emails a 24-hour set-password link and signs the user out everywhere; their existing password keeps working until the link is redeemed.
  • One licence type. An account either holds a licence or it doesn't — there are no tiers, and creating an account never consumes a seat by itself. The Users page band reads "Licenses: X of Y in use · Z available"; assigning is blocked when nothing's available ("All licences are in use. Ask your IT team to arrange additional licences."). Every licensed account counts — including accounts that exist only to run background Background jobs. An account without a licence can't sign in.
  • Users are never deleted (their name is needed in history) — they're disabled, which ends their sessions on the very next request. Disabling does not free the licence; unassign it to free the seat. You can't disable your own account or the last enabled administrator. If more licences are assigned than the plan includes (it can happen after plan changes), the availability shows negative and the band turns amber — nothing breaks, but new assignments wait until the pool grows.
  • Clone copies a user's role, teams, permission overrides and settings onto a fresh name and email — the quick way to onboard look-alike accounts.
  • Custom user fields (Tenant settings → User fields) add your own attributes to every user — hourly rate, branch — available to automations, Queries (@me.hourly_rate, assignment:slot.branch) and reporting.
  • Task visibility is user-controlled: each person grants named colleagues per-capability access to their own tasks (view / action / reschedule / cancel / delete) from the user dialog's Task visibility tab.

Teams

A team groups users. A team can carry its own permission grants, which are added to each member's permissions; one team can be the default (new users auto-join); and a case type's assignment slots can restrict who's pickable to certain roles or teams. Teams don't hide cases — visibility is a permission matter.

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: Tenant Administrator (all permissions, can be renamed but never deleted — and at least one enabled user always holds it), Standard User (day-to-day case work) and Read-only User (view-only: see cases, correspondents, Triage Inbox, tasks, and run Queries and Reports — change nothing).
  • Delegated administration is escalation-proof: someone with Manage users who isn't an administrator can't assign the administrator role, can't grant a permission they don't hold themselves, and can't raise anyone's security level above their own.

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
Workflowworkflow.action.email, workflow.action.letter, workflow.action.memo, workflow.action.phone, workflow.action.incoming, workflow.template.manage
Correspondentscorrespondents.view, correspondents.manage
Triage Inboxtriage.view, triage.release, triage.archive
Taskstasks.view, tasks.create, tasks.complete, tasks.reassign, tasks.reschedule, tasks.delete.own, tasks.delete.other
Queries & 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
CaseOnGo AI Add-onai.use, ai.case, ai.draft, ai.author_templates, ai.author_automations
Accounts Add-onaccounts.invoices.view, accounts.invoices.manage, accounts.integration.manage
Data Export Add-ondataexport.run, dataexport.manage

A few notes: cases.assign lets someone fill assignment slots without broader edit rights; cases.share lets a case worker share the case in front of them through Collaborate without being a Hub administrator (that's collaborators.manage); rescheduling a task is its own key, separate from creating one; task deletion is split into "own" and "others'". The add-on groups appear in the matrix only when the matching add-on is enabled for your organisation (enabling add-ons is a plan matter, not a setting).

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 · Tenant settings

Organisation-wide switches, in one place.

Where: Configuration → Tenant settings (needs settings.manage)

The page is tabbed:

  • General — the organisation's Tenant name (shell header, browser title, system emails); the default case-list page size (10–500); Case numbering — start from (the floor for the shared six-digit sequence; it only ever moves forward); Collaborator sharing — the "shareable up to authorisation level" that lets any sharer offer screens up to that level regardless of their own; and the Simple-mode cards when that add-on is on.
  • Email matcher — 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 top-bar 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.
  • User fields — the custom attributes on every user record (label, code, type, required).
  • Collaborators Add-on — the month-by-month "Collaborator accounts used" view behind your Collaborators invoice line (section 29).
  • Outbound HTTP — the hostnames automation scripts may call with http.*. Empty (the default) means outbound HTTP is off; internal / private addresses are always refused.
  • Design workspace — the API keys for the design CLI (section 26).
Two things people look for here that are not settings: session length is fixed platform policy (you stay signed in while active; sign-out comes after 4 days of inactivity), and the one-signed-in-place-per-account rule is part of your plan — ask us to turn it on or off.

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 — usually part of onboarding.
  • CSV export produces every case of a case type — reference, title, status and every field.
  • For everyday extracts, use Queries → Export and Insight outputs — those are self-service.

Design versions — restore, export & clone a case type

CaseOnGo 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 · The design workspace CLI

Your whole tenant design as files — edit, review, apply, roll back.

Where: Tenant settings → Design workspace (keys) · the caseongo-design command (your machine)

Everything you can design in the portal — case types, fields, screens, templates, automations, Queries, Reports — can also be pulled to your computer as plain files, edited (by you, or by an AI coding assistant), diffed, and applied back. That gives you version control with git, reviewable changes, and a clean way to develop a design against a test tenant and release it to production.

The two-key model

  • Keys are minted in Tenant settings → Design workspace. Each key carries scopes: read (pull the design), plan (compute a dry-run diff) and apply (write changes). The plaintext is shown once — copy it then; revoke any key any time. Minting plan/apply keys additionally requires the Manage case types permission.
  • Work with two keys: a day-to-day key with read + plan (safe to give an AI assistant — it can see and propose, never change), and an apply key that stays with a human and is used only at the moment of applying.

Setting up a design project

npm install -g <the caseongo-design package we supply>
caseongo-design --version             # proves the command is on your PATH

mkdir C:\work\firm-design ; cd C:\work\firm-design

# the tenant you EDIT AGAINST (your test/UAT tenant) — the default target:
caseongo-design init --tenant uat  --url https://api.caseongo.com --key <uat read+plan key>  --save-key
# every tenant you distribute to:
caseongo-design init --tenant prod --url https://api.caseongo.com --key <prod read+plan key> --save-key

caseongo-design pull                  # the design arrives as files
git init
# put the line  .design/targets.json  in .gitignore — keys never enter git
git add -A ; git commit -m "baseline"

The daily loop

caseongo-design pull            # start from the live design
# …edit files (or let your AI assistant propose edits)…
caseongo-design validate        # structural checks, offline
caseongo-design plan            # exact dry-run diff against the tenant — read it
caseongo-design test ONB SETSTART --case ONB00012    # try an automation, no writes
caseongo-design apply --key <owner apply key>        # land it
git add -A ; git commit -m "escalation feature"
  • Release to production: plan --tenant prod, read the diff (prod may legitimately differ), then apply --tenant prod with the prod apply key. Deletions only run with explicit --confirm tokens the plan prints.
  • Drift safety: if someone edited the design in the portal since you pulled, the apply refuses — pull, review their change in git, and re-plan. caseongo-design status answers "is my workspace current?"
  • Rollback: every apply takes restore points first — caseongo-design rollback undoes the last apply (per case type or whole-tenant scope), and design versions (section 25) remain your in-portal safety net.
The CLI never touches case data — it moves design only. Fixed correspondents on templates and designated senders are cleared on cross-tenant applies (they're data); re-pick them in the target tenant's portal once.

27 · CaseOnGo Accounts (Xero) Add-on

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

Where: Sidebar → Documents (Xero) / Xero settings (when the add-on is enabled)

Xero-only, by design. CaseOnGo Accounts runs entirely on Xero — there is no built-in ledger to maintain inside CaseOnGo, no chart of accounts to keep here. Accounting statements (VAT, P&L, balance sheet, aged reports) live in Xero, where your accountant already works.
  • Connect once under Xero settings, pick your Xero organisation, and it stays connected — the connection is kept alive automatically and won't lapse from a quiet month.
  • Documents: raise invoices (money in) and bills (money out) 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). A case type can bind the On accounts document paid trigger to react when money lands (section 6).
  • 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 - ' + get('{case.ref}'),
  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().

28 · CaseOnGo AI Add-on

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

CaseOnGo AI 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 the email and letter compose dialogs; a template assistant that drafts letter and email templates in the Workflow editors; 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 & credits page in the Manage menu 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, and each surface has its own permission (section 23).

29 · Collaborate portal Add-on

External parties see exactly what you share — nothing else.

Where: Sidebar → Collaborators (admin) · the Share action on a case

Accounts & invites

  • External users are invited by email and sign in at <workspace>-hub.caseongo.com with their own credentials — never with staff accounts. The invite link lets them set their own password (nothing is ever emailed as a password); invites expire after 7 days by default and can be revoked. Admin tools: suspend, reactivate, reset password.
  • Groups (Solicitors, Case Managers…) are labels for organising external users — access is always decided per share, never per group.

Shares

  • A share grants one external user access to one case. In the Share dialog you set each screen to Hidden, Read-only or Editable (with Hide all / All read-only / All editable shortcuts), choose how much case history they see (none / only entries flagged for externals / all), set attachment rights (upload / download / see attachments), an optional expiry, and whether access survives case closure. The share is a frozen snapshot — later design changes don't widen it unless you Re-snapshot.
  • A named collaborator profile per case type (case-type detail page) gives you a reusable preset for all of that; the default profile pre-loads in the Share dialog.
  • Who can share: cases.share covers sharing the case in front of you; collaborators.manage owns the whole Hub (accounts, invites, every share). The "shareable up to authorisation level" setting (section 24) can let sharers offer screens above their own level.

What collaborators experience

  • Their home page is Cases shared with you. A case opens into the same screen renderer staff use: editable screens edit for real (tables and time records included), read-only screens wear a "View only" lock, and edits across screens buffer into one draft with one Save — which runs your Before Save / After Save / on-change automations, prompts included, exactly as for staff. Screen buttons you shared run their automations too (user.isExternalUser tells your script who's driving).
  • Documents is a two-way file conversation per (case, collaborator): they upload, you exchange, they can remove their own file within 24 hours; your side — the case's Collaborators view — shows one thread per person with unread badges and open-receipts. The conversation is deliberately kept out of the audit/fields history.
  • They can't: send correspondence, see tasks or notes, follow case links to other cases, edit globals, share onward, or reach any case or screen you didn't share. Everything they do lands on the case history under their name, and files they upload wear an External badge.
  • Sessions last 8 hours (renewing while active); single-signed-in-place can be enforced on Hub accounts as part of your plan.

Billing transparency

Your invoice carries one line for collaborator usage: the number of external accounts that actually signed in and did something during the month. Unused invites and idle accounts cost nothing; failed logins and password resets don't count. Tenant settings → Collaborators itemises exactly who was counted, month by month — the same calculation the invoice uses, so the two can never disagree.

30 · 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 workspace is resolved from the subdomain on every request.
  • Identity — staff sign in with Microsoft Entra (one directory backing many organisations); collaborators use a separate credential system confined to their portal.
  • Sessions — a licence is required to hold a session; disabling a user ends their sessions on the next request, everywhere; sign-out in one tab signs out all of them; inactivity past 4 days ends the session; and your plan can enforce one signed-in place per account (staff, Hub users, or both).
  • Authorisation — role/team permissions are enforced server-side on every sensitive operation; the menu gating you see is convenience only. Delegated admin can never grant beyond what it holds.
  • 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.

31 · 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

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

Step 2 — Define the fields

Manage → Database Management, 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
DebtorCorrespondent (type = Debtor)
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

Manage → Screen Designer, on DEBT:

  • Overview — Case-control tiles (Reference, Status), the debtor & Stage, Original Debt, Balance Outstanding (read-only), 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. Configure Quick View on the case type — Stage, Balance Outstanding, Payment Due Date — so the numbers ride the case bar.

Step 4 — Add templates

Manage → Workflow → DEBT → Letters: 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

Workflow → DEBT → Automation → 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 When a case is being created to an automation that sets Stage = "Pre-action", and bind Before marking the case as dead 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 a List, an Insight and a Routine

Build a List "Overdue debts" (Payment Due Date < @today AND Balance Outstanding > 0, with an optional Stage question), and an Insight on it grouped by Stage with a Sum of Balance Outstanding and a bar chart. Then add an Auto routine — Manage → Background jobs → New job — that runs "Overdue debts" every morning at 08:00 and fires a "chase" automation on each match; press Dry run once to see exactly which cases it would touch.

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, and the whole design can live as files under git with the design workspace CLI.