Contract intelligence — an Obscura module, not an Icertis integration
Status: design, awaiting review · Date: 2026-08-15
Companion research: docs/research/2026-08-15-neo4j-graph-research.md (why the entity
layer is plain Postgres tables, and what would ever justify a graph engine).
The doctrine
A contract is a document that carries obligations, and today Obscura forgets the
obligations the moment the ink dries.
Obscura already does the ceremony of contracts well: templates, variables, approval
workflows, e-sign with Peruri, retention. What it does not hold anywhere is what the signed
document commits the organisation to — who the counterparty is, when it expires, what
must be done by which date, what it is worth. Those live in people's heads and personal
calendars, which is why renewals get missed.
The module makes them first-class: extracted by the AI pipeline that already reads every
document, stored as structured rows, surfaced as deadlines through the notification system.
Icertis is the reference for the feature SET — not a partner. It is a full CLM platform with
its own repository, workflows, approvals and e-sign, all of which Obscura already has, so
integrating it means paying enterprise CLM licensing for capabilities we own and handing a
competitor the customer relationship. (It does offer on-premise and hybrid deployment, so
the air gap is a factor, not the argument.)
What ships (and what deliberately does not)
Ships: a Contract profile per document (parties, dates, value, governing law, type), an
obligation register with deadline reminders, clause-type findings, an expiring-contracts
view, and Ask-the-Archive answers that can use all of it.
Does not ship (v1): clause drafting/redlining (the office module edits, this module
reads), risk scoring (a number nobody can audit), negotiation workflows (the workflow
engine already exists — a contract approval is just a workflow), and any auto-action on
extraction (see the hard rule below).
🔴 The one hard rule: extraction SUGGESTS, a person CONFIRMS
The precedent is already in the product twice: the AI classifier only suggests
registry levels, and AI tags are not auto-selected. Same reason, higher stakes here — a
hallucinated renewal date that silently feeds reminders is worse than no reminder, because
it teaches people to trust a calendar that lies. So:
- Extracted fields land as
suggested; anyone with Editor confirms or corrects them.
Confirmation is one click on a diff-style panel, not re-typing. - Only confirmed obligations generate reminders. An unconfirmed profile shows a quiet
"needs review" badge on the document, nothing more. - Extraction never changes classification, retention, status or ACLs. It writes only its
own tables.
Data model (numbers to be re-checked against ls go/migrations | tail at build time)
-- One profile per document; the extraction's home and its review state.
CREATE TABLE contract_profiles (
document_id uuid PRIMARY KEY REFERENCES documents(id) ON DELETE CASCADE,
contract_type text NOT NULL DEFAULT '', -- NDA | service | lease | employment | other
counterparty text NOT NULL DEFAULT '', -- display name; parties table holds the rest
effective_date date,
expiry_date date,
auto_renews boolean NOT NULL DEFAULT false,
notice_days int, -- termination-notice window, when stated
value_amount numeric, -- NULL ≠ 0: "no value stated" is not "free"
value_currency text NOT NULL DEFAULT '',
governing_law text NOT NULL DEFAULT '',
status text NOT NULL DEFAULT 'suggested', -- suggested | confirmed | dismissed
extracted_at timestamptz,
confirmed_by text,
confirmed_at timestamptz
);
CREATE TABLE contract_parties (
id uuid PRIMARY KEY,
document_id uuid NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
name text NOT NULL,
role text NOT NULL DEFAULT '', -- pihak pertama/kedua, vendor, client…
-- entity_id uuid: reserved for the knowledge-graph phase; the SAME extraction pass
-- feeds both, which is what makes the graph cheap later.
entity_id uuid
);
-- The register: one row per dated commitment.
CREATE TABLE contract_obligations (
id uuid PRIMARY KEY,
document_id uuid NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
kind text NOT NULL, -- renewal | termination_notice | payment | deliverable | review | other
description text NOT NULL,
due_date date NOT NULL,
recurrence text NOT NULL DEFAULT '', -- '' | monthly | quarterly | yearly
status text NOT NULL DEFAULT 'suggested', -- suggested | confirmed | done | dismissed
remind_days int NOT NULL DEFAULT 30, -- days before due to start reminding
assignee text NOT NULL DEFAULT '' -- user id; '' = the document owner
);
CREATE INDEX contract_obligations_due ON contract_obligations (status, due_date);
CREATE TABLE contract_clauses (
id uuid PRIMARY KEY,
document_id uuid NOT NULL REFERENCES documents(id) ON DELETE CASCADE,
clause_type text NOT NULL, -- from the admin registry below
excerpt text NOT NULL, -- the sentence(s), for the review panel
page int
);
-- Admin-owned registry of clause types worth flagging (non-compete, indemnity, auto-renewal,
-- exclusivity…). Seeded with a sensible set; editable like the classification registry.
CREATE TABLE contract_clause_types (
key text PRIMARY KEY,
label text NOT NULL,
description text NOT NULL DEFAULT '',
enabled boolean NOT NULL DEFAULT true
);
How each piece rides existing rails
| Need | Existing rail | Delta |
|---|---|---|
| Extraction | AI enrichment (DocEnrichNow, provider config mig 00146, daily budget) |
one more structured-output prompt per contract-typed doc; strict JSON schema, reject-don't-repair on parse failure |
| Privacy gate | DLP allow_ai_processing (mig 00163) |
none — the gate already sits on the enrichment path; a Secret contract is simply never extracted |
| Reminders | scheduler (internal/scheduler, 30s RunDue) + notify pipeline |
one sweep task: confirmed obligations entering their remind_days window → notification per assignee, dedup-marked like the disposition sweep. Delivery is now observable per recipient (mig 00193) — a missed renewal reminder is a queryable fact, not a mystery |
| Which docs are contracts | doc_type + the classifier |
extraction runs when doc_type maps to a contract kind, or on demand from the Contract tab |
| Review UI | document detail tabs | a Contract tab: suggested-vs-confirmed diff panel, obligation list, clause findings with excerpts |
| Org view | Reports page | an Expiring contracts section: next 90 days, confirmed-only, with the unconfirmed count stated honestly beside it |
| Licensing | module system (KnownModules) |
new module contracts; alias-safe (no existing licence names it). Commercial call: price as % of core like other modules |
| Knowledge graph | contract_parties.entity_id |
phase 2 of the graph research: same extraction pass, entity tables, one-hop expansion for Ask-the-Archive |
Language reality
Prod contracts are Indonesian (NDA-PDS, perjanjian kerja sama). The extraction prompt must be
bilingual id/en — dates like "31 Desember 2026", values like "Rp 500.000.000", roles like
"PIHAK KEDUA". The clause-type seed list ships with Indonesian labels alongside English. This
is a first-class requirement, not a translation pass at the end.
Failure honesty
- A parse failure or a low-confidence extraction stores nothing — no profile beats a
wrong one. The Contract tab offers "run extraction" with the failure reason. value_amountNULL means not stated; the UI never renders it as 0 (money is already
formatted in three places; use the shared formatter).- Recurrence math uses civil dates in the org timezone; a
yearlyobligation due 29 Feb
falls to 28 Feb on non-leap years, stated in the code comment, not discovered in prod.
Slices (each independently shippable)
- Migration + module gate + extraction into
suggestedprofiles (no UI beyond the badge). - Contract tab: review/confirm/dismiss, manual add/edit of everything extraction gets wrong.
- Obligation sweep + reminders through notify (confirmed only).
- Reports: expiring-contracts section.
- Clause-type registry (admin) + clause findings in the tab.
- Graph phase-1 entity tables fed from the same pass (per the research doc), Ask-the-Archive
retrieval expansion.
Open questions for review
- Who confirms — anyone with Editor on the document, or gate it behind a new
contracts.reviewpermission? (Recommendation: Editor; a permission per action is how the
catalogue got to 59.) - Backfill — run extraction over the existing corpus on module enable (with the daily AI
budget as the throttle), or only on new/opened documents? (Recommendation: on-demand +
trickle backfill, budget-capped.) - Does
contractsrequireai? Extraction needs a provider, but manual profiles are
useful alone. (Recommendation: module works standalone with manual entry; extraction
lights up whenaiis licensed too.)