Data Model
Verified against source (13)
application/GenericObject.csConfig/Abstract.configTables/Process.sqlTables/Organization.sqlTables/Person.sqlTables/Attachment.sqlTables/AttachmentHistory.sqlTables/Message.sqlTables/StatusHistory.sqlStored Procedures/pPurgeHistoryLoad.sqlStored Procedures/pHistoryTablePurge.sqldbo/TablesConfiguration-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.
Parents: Object and ObjectID
Section titled “Parents: Object and ObjectID”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=1POST /api/message/save?Subject=Invoice dispute&Object=Ledger&ObjectID=17POST /api/message/save?Subject=Missing signature&Object=Attachment&ObjectID=22The same pair filters searches:
GET /api/message/search?Object=Contact&ObjectID=1ListByParent 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.
Templates and instances
Section titled “Templates and instances”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.
Inheritance
Section titled “Inheritance”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=345History
Section titled “History”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=2207GET /api/attachment/selectversion?VersionID=88412GET /api/attachment/statushistorylist?ID=2207Each 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.
Purging history
Section titled “Purging history”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:
pPurgeHistoryLoadrecords a high-water mark (the latest history ID) for every*Historytable that has aRevisionDateorChangeDatecolumn, excluding template history.pHistoryTablePurge @ExpirationInDays = 90finds, for each table, the latest mark that is at least@ExpirationInDaysold 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.