Skip to content

Data Model

Verified against source (13)

  • application/GenericObject.cs
  • Config/Abstract.config
  • Tables/Process.sql
  • Tables/Organization.sql
  • Tables/Person.sql
  • Tables/Attachment.sql
  • Tables/AttachmentHistory.sql
  • Tables/Message.sql
  • Tables/StatusHistory.sql
  • Stored Procedures/pPurgeHistoryLoad.sql
  • Stored Procedures/pHistoryTablePurge.sql
  • dbo/Tables
  • Configuration-Administrator/Data-Tuning.md

Each module (Contact, Process, Attachment, …) is backed by a table of the same name, with an {Table}ID key. ID and {Table}ID are interchangeable in requests: /api/contact/select?ID=1 is the same as /api/contact/select?ContactID=1.

Generic modules (Attachment, Message, Decision, ImportForm, Ledger, Score, Process, and others) bind to any parent through a soft key: Object names the parent’s module and ObjectID its ID.

POST /api/message/save?Subject=Payoff request&Object=Contact&ObjectID=1
POST /api/message/save?Subject=Invoice dispute&Object=Ledger&ObjectID=17
POST /api/message/save?Subject=Missing signature&Object=Attachment&ObjectID=22

The same pair filters searches:

GET /api/message/search?Object=Contact&ObjectID=1

ListByParent returns a parent’s children, and ObjectTree returns every related Object/ID pair (see standard operations). Extranet security follows the parent unless the child’s template makes it extranet-enabled in its own right.

Templated modules pair an instance table with a template table: Process / ProcessTemplate, Decision / DecisionTemplate, Attachment / AttachmentTemplate, ImportForm / ImportFormTemplate, ImportFile / ImportFileTemplate, and so on. Passing Template with Object and ObjectID on save applies the template’s repeatability, reusing an existing instance where the template allows only one. See Templates.

Some tables extend a base table through a foreign key, so shared columns and behavior live in one place.

Contact. Contact holds name, address, and latitude/longitude; phone numbers and email addresses are ContactMethod rows on the same ContactID. Organization and Person (QBO users) each carry a ContactID, and industry modules add more, such as court and party records. An organization’s address is its contact’s address.

Process. Process holds what every case shares:

Columns Purpose
Object, ObjectID The parent record
ProcessTemplateID, ProcessType, Status, Priority Kind and state
DateOpened, OpenedReason, DateClosed, ClosedReason, DateDue Life cycle
ClientID, VendorID, AssignedOrganizationID, AssignedPersonID Parties and outsourcing
CurrentDecisionID, CostLedgerID The running workflow and cost ledger

Industry modules add specialized processes that carry a ProcessID, such as litigation, eviction, title, valuation, debt collection, and bankruptcy. A specialized record holds the fields specific to its type; the shared dates, status, assignment, workflow, and costs live on its Process row. Searching Process therefore spans every specialized type, for example everything assigned to one law firm:

GET /api/process/search?AssignedOrganizationID=345

Every core table has a {Table}History twin (AttachmentHistory, ContactHistory, DecisionHistory, …), including template tables. An UPDATE or DELETE trigger copies the previous row into it, adding:

Column Value
{Table}HistoryID Version ID
RevisionDate When the change was made
RevisionPersonID Who made it: the user QBO sets in the database session context for each statement

The trigger uses EXCEPT, so a save that changes nothing writes no history row. Inserts write none either: the first version is the row itself. After two edits, a message has two history rows, holding the original text and the first edit, and the newest RevisionDate matches the row’s UpdatedDate.

Separately, status triggers on insert and update log each status change to StatusHistory (Object, ObjectID, StatusHistory for the old value, StatusCurrent, ChangeDate, ChangePersonID).

Read history through the shared statements every module inherits:

GET /api/attachment/historylist?ID=2207
GET /api/attachment/selectversion?VersionID=88412
GET /api/attachment/statushistorylist?ID=2207

Each requires the matching permission (AttachmentHistoryList, and so on). History tables sit in their own filegroup and are indexed for lookups by record, not for reporting; query the base tables for anything aggregate.

History is purged on a schedule, and every purge policy is configurable per installation: how long history is kept, which tables are purged, and how often the purge runs. The standard policy, unless a contract specifies otherwise, is 90 days for history and log tables. Two mechanisms exist:

Database procedures (installation-wide). Two procedures run on a schedule:

  1. pPurgeHistoryLoad records a high-water mark (the latest history ID) for every *History table that has a RevisionDate or ChangeDate column, excluding template history.
  2. pHistoryTablePurge @ExpirationInDays = 90 finds, for each table, the latest mark that is at least @ExpirationInDays old and deletes every history row written before it, in batches of 3,000.

Change @ExpirationInDays to change retention. Because the marks are by ID, a row is purged at least @ExpirationInDays after it was written.

Leaving template history out is the recommended default for most installations: templates change rarely, their history is small, and it records who changed a workflow or document template and when. Where it isn’t wanted, purge it per template module with PurgeHistory (below), for example /api/attachmenttemplate/queue/purgehistory?Months=12.

Per module (API). PurgeHistory deletes one module’s history older than a number of months:

POST /api/attachment/queue/purgehistory?Months=3
Parameter Default Purpose
Months 120 Delete history with a RevisionDate older than this
BatchSize 4000 Rows per transaction
MaxCount 2500 Maximum batches per call

Queue it (/queue/) or schedule it, since a large purge is long-running. When Months is omitted it defaults to 120, so pass the value your retention policy sets.