Skip to content
objectstack-aiPublic

About

Contract lifecycle management on ObjectStack — buy-side, sell-side and everything in between, as typed metadata. Self-serve intake, a clause playbook, a data-driven approval matrix, e-signature and execution formalities, obligations and payment schedules: the whole lifecycle in one readable repository.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

HotCLM

HotCLM

Contract lifecycle management on ObjectStack — buy-side, sell-side and everything in between, as typed metadata. Self-serve intake, a clause playbook, a data-driven approval matrix, e-signature and execution formalities, obligations and payment schedules: the whole lifecycle in one readable repository.

基于 ObjectStack 的合同全生命周期管理。 业务自助发起、条款库与偏离、审批矩阵、电子签与执行形式、履约义务、收付款计划 —— 全部是类型化元数据。

Status: M1–M3 are in. The model and the permission layer, intake, the approval ladder, signing and execution formalities, the post-signature reminder layer, the analytics and both locale bundles all land in this tree. M4 is what remains — e-signature, the HotCRM hand-off, the AI skills and the release: DESIGN.md §11 is the milestone table, and cards 12 · 13 · 14 in docs/backlog are what is left in it. Sibling app of HotCRM: commercial terms stay in the CRM, legal state lives here.

What it is

  • Ironclad-shaped, not OA-shaped. A contract type is a workflow: intake fields, review, approval ladder, signing method, archive rules — configuration, not code.
  • Business users launch, legal controls. One intake form per contract type; the approval matrix decides who signs off.
  • Contracts are data. Obligations, payment schedules, renewals and deviations from the clause playbook are queryable records with reminders, not paragraphs in a PDF.
  • Global by default, local by configuration. English-first UI with full zh-CN; multi-entity, multi-currency, governing law and jurisdiction on every contract; e-signature through DocuSign, Adobe Acrobat Sign or Dropbox Sign; a company seal, notarization or witnessing are execution formalities a contract type can require, not modules.
  • AI is a participant under governance. Intake by chat or MCP, extraction of executed contracts, review memos for approvers, deviation detection against the playbook — every AI step proposes, a person confirms, and the audit trail records both.
  • Industry- and region-neutral by rule — contract types, thresholds, execution formalities, currencies and payment terms live in seed data only.

Quick start

pnpm install
pnpm dev          # REST + Console on http://localhost:3000 — an EMPTY app
pnpm demo         # the same app with the demo group loaded (English)
pnpm demo:zh      # the same rows, in Chinese

pnpm dev is deliberately empty: a standard product does not install somebody else's contract book into a fresh deployment. pnpm demo is the opposite intention — a fictional multi-entity group, six months of negotiation and a book of contracts already in force, so every list has something on its first screen. Sign in with admin@objectos.ai / admin123, or as one of the demo staff with the same password: a business requester such as requester1@example.com is an account whose 我的合同 the demo fills.

pnpm demo also turns on the platform's scheduled work, which is off by default, so the daily reminder sweeps run on the demo book; a deployment that wants those reminders in the inbox sets OS_AUTOMATION_SCHEDULED_WORK_ENABLED=true.

Point the two locales at different databases; they key their rows on localised titles, so seeding one on top of the other gives you both books.

Every metadata change is gated:

pnpm validate        # protocol schema + CEL predicates + bindings
pnpm lint            # data-model conventions (reserved vocabulary, titles, master-detail)
pnpm typecheck
pnpm lint:i18n-gate  # every zh-CN key present — a missing one renders as English, silently

Layout

Path Contents
objectstack.config.ts defineStack() — the single entry point
src/objects/ clm_* business objects
DESIGN.md Architecture authority — objects, permissions, audiences, automation, milestones
AGENTS.md Conventions for humans and coding agents; read by the PM dispatch loop
src/data/ The demo dataset — demo-en/ (default) and demo-zh/, selected by OS_SEED_LOCALE
docs/backlog/ Dispatch-ready work cards (each file is an issue body)

The demo dataset

pnpm demo loads 1,130 rows: nine contract types, a thirty-clause playbook, a six-rule approval matrix, three signing companies and seven departments, forty counterparties and 120 contracts with their 300 document versions, reviews, deviations, signature rounds, obligations and payment instalments. Every date is relative to boot, so ten contracts always expire within the next thirty days and the arrears lists are always in arrears.

The demo staff — created for you

The people the demo book is dealt to are declared in objectstack.config.ts (demoStaff), and the platform creates them on the demo boot — after the dev admin, before the seed runs — so every seeded row that names a person reaches them on the first pnpm demo, with no step of yours. They are the persona mix of DESIGN.md §10: three business requesters and one account per position of §04, two for legal counsel.

Every one signs in with the development seed password, the dev admin's: admin123 (or whatever OS_SEED_ADMIN_PASSWORD says when it is set). The console does not list them; this table is where they are named.

Account Sign in as Position What it holds in the demo
Business Requester 1 · 2 · 3 requester1@example.com · requester2@example.com · requester3@example.com none — every member holds clm_requester the 120 contracts' owner_id, dealt by counterparty — 43 / 43 / 34 · and the 143 delivery and reporting obligations under them — 50 / 53 / 40 · the 2 business reviews — 0 / 1 / 1
Legal Counsel 1 · 2 counsel1@example.com · counsel2@example.com clm_legal_counsel receives contracts on submission (round-robin by open load) and owns review · the legal_owner of every contract legal has accepted — 34 / 34 · all 56 compliance obligations — 29 / 27 · the decided_by of all 17 decided deviations — 10 / 7 · the reviewer of their contracts' 34 legal reviews — 16 / 18, the 7 still pending among them — 2 / 5
Head of Legal legal.head@example.com clm_legal_head rung 2 of the approval ladder whenever route_legal_head is stamped · the reviewer of 8 reviews: the legal review of the contract handed back to the queue, and the 7 finance reviews on contracts not yet approved
Finance Controller finance@example.com clm_finance_controller rung 2/3, reads every contract with route_finance, and the only position that may edit bank details · the reviewer of the 16 finance reviews on approved-or-later contracts
Executive executive@example.com clm_executive rung 4, and reads every contract with route_executive
General Manager gm@example.com clm_general_manager rung 5, and reads every contract with route_gm
Records Manager records@example.com clm_records_manager execution formalities, the executed copy and the archive number
CLM Administrator clm.admin@example.com clm_admin maintains the configuration objects and holds full reach over every CLM object: the only position that may write clm_contract_type and clm_approval_rule, and the only one the app's Administration group is served to

A business requester is not a position. Every member of the organization holds clm_requester by default (DESIGN.md §04): it is the app's default permission set, bound to the platform's everyone anchor, so a requester needs no position and no grant. They launch contracts and see their own. The seven positions hold the same set the same way, on top of their own.

Signed in, each requester lands on 我的合同 › 我发起的 (Launched by Me) with their contracts, and finds their obligations under 我负责的履约 (My Obligations); each lawyer lands on 法务工作台 and finds their contracts under 我的审查 (My Reviews); finance lands on 财务, records on 执行与档案 and the CLM administrator on 管理. A decided deviation's Decided By shows its lawyer on the contract page's Deviations grid.

Three things about where these accounts exist:

  • On a local development boot only. os dev creates them; a production boot, a migration run or a server whose auth base URL is not a loopback address creates none and logs why. os build leaves the declaration out of the artifact, so no deployment boots with them. They are not seeded users: no fixture carries a sys_user row, and §10's 「用户不可种子」 stands.
  • With the demo only. The declaration is gated on CLM_DEMO_SEED, the switch that loads the dataset, so pnpm dev stays empty of people as well as of contracts: it signs in the dev admin and nobody else.
  • Once. Running pnpm demo again finds every address already there and leaves that account exactly as it is — name, password, manager — adding only a position it is missing. No account is duplicated.

Every dataset is an upsert, so pnpm demo also runs again on a database that already holds the demo, after the daily jobs have run and after you have worked in it. A re-run slides every boot-relative date forward, puts back any demo row that has been deleted, and refreshes the rest of the fixture.

It does not turn the clock back. The columns the app itself writes once a row exists are declared applyOn: 'create' in the dataset (src/data/): the seed writes them when it creates the row and leaves them alone after that. They are status on contracts, obligations and payment instalments, the flag F12 sets, the stage timestamps and routing flags, the termination reason, the archive number, and the receipt recorded on an instalment. So what the daily jobs did stays done. An instalment F11 moved to overdue stays overdue, a contract F13 expired stays expired, and F12 does not send a renewal notice twice. The same goes for a contract someone has terminated or archived, or an instalment finance has marked paid. Measured after one run of every daily job plus those four desk actions: the re-run refused no row and dropped none.

Three limits. Any other column you edited, such as a summary or a reassigned legal owner or a review's reviewer, goes back to the fixture's value. A deviation, signature round or review you decided is not covered yet: the re-run puts its fixture status or decision back, and for a deviation or a signature round the state machine may refuse that. The lifecycle columns also keep the dates they were stamped with, while the fixture's own dates (start, end, due and planned dates) move forward on each re-run. To get back to the corpus exactly as shipped, start from a clean database: rm -rf .objectstack/data && pnpm demo.

One obligation is deliberately left with no owner and is not a name you have missed. F10 (obligation_due) reminds an obligation's owner and takes a "nobody to tell" edge when there is none — the demo keeps exactly one unowned row, due seven days after boot, so that edge stays exercised. src/data/plan-children.ts proves it reachable and says why one is both the minimum and the maximum.

The dev admin is deliberately none of them. Like every member it holds clm_requester, so it sees 我的合同 and 分析 (GET /api/v1/meta/app/clm serves it exactly those two groups), but it holds no position and no other clm_* set: clm_legal.access gates the legal workbench away, and the finance, records and administration groups stay hidden the same way. A row parked there still lands on the wrong account — as a contract's legal owner it sits behind a workbench the dev admin cannot open, and as a business owner or an obligation's owner it fills the setup account's 我的合同 instead of the requester's — and its reminders go to somebody who is not going to act on them.

The 60 reviews are dealt to the desk whose stage they are. A legal review is the contract's lawyer's, or the head of legal's on the one contract handed back to the queue. A finance review is the finance controller's once its contract is approved, and the head of legal's before that, because finance edits a contract (and so its reviews) only from approved onward. A business review is the requester's who launched the contract. A compliance review would be the head of legal's; the demo deals none. src/data/keys.ts (reviewerOf) says why each.

Rung 1 of the approval ladder is a manager

Every contract sent for approval opens on rung 1: the direct manager of the contract's owner — the business requester who launched it, not the lawyer who pressed Send for Approval — read from that account's sys_user.manager_id. No seed, position or permission set fills that column, and neither do the demo staff: a demoStaff entry carries a position and permission sets and nothing else, so it cannot name a manager. On a fresh pnpm demo no requester has one, and no contract gets past rung 1 until you set it — the one step of the demo that is still yours.

Set it in Setup → Users → the user → ⋯ → Set Manager, for each of the three business requesters and for anyone else who launches contracts. Any other account in the organization will do — the persona mix has no line managers of its own, so point all three at one of the demo staff (Executive, say). A second pnpm demo leaves the manager you set where it is. The manager needs nothing more: approve_contract, the gate a rung acts through, is in clm_requester, which every account holds.

Skip it and the request opens on a slot nobody holds — pending_approvers reads manager:undefined (upstream objectstack-ai/objectstack#22558) — and the contract stays locked for approval. Setting the manager afterwards does not reach a request that is already open: its approvers were resolved when the rung opened. Only a platform admin moves it on, from Setup → Approvals → Approvals Inbox → All, with Override Reassign (or Override Approve / Reject).

No rung sends its approver a message when it opens — nothing in their inbox, no email. They see it waiting in the Console: the bell counts the approvals waiting on them and 我的合同 › Waiting on Me lists them. A reminder sent on a request does arrive as a message; the missing one on opening is the platform's, not this app's.

The rung-1 manager reads the contract while the request waits on them, and only then. From @objectstack 18.2.0 the platform gives every person a pending step waits on a read-only view of the one record the request is about (upstream objectstack-ai/objectstack#26180); this app declares nothing for it. So a manager who holds no position at all opens the request from 我的合同 › Waiting on Me and lands on the contract page itself, with 通过 / 拒绝 above it. Every tab opens: the overview, 版本 (the version's PDF opens), 审查与偏离, 审批记录, 履约与收付款, 签署与归档 and 讨论, and each review, deviation, obligation, signature round and version opens on its own page. Nothing can be changed: the page has no 编辑, its ⋯ menu offers only 添加到收藏 and 分享, and a write through the API answers 403 PERMISSION_DENIED.

The read ends with the step. Once the manager approves or rejects, the contract, its child rows and its files are closed to them again — the page they decided on reads 「未找到记录」 straight away, which means the step is over, not that the record is gone. A contract of a report's that is not waiting on them (a draft, or one already past rung 1) stays closed too: on the open edition being the owner's manager grants no read. The scope that would (own_and_reports, DESIGN.md §04 contract_manager_reports) is an enterprise-edition hierarchy scope that the open edition resolves to owner-only. The rungs after rung 1 are positions, and each reads the contracts it approves.

Assigning a position from a script

The demo staff get their positions from the platform. Staffing an organization of your own goes through Setup → Users, and automating that needs one more fact: the assignment binds by the position's name, not by its id. Read that off the declaration, not off the field name — sys_user_position.position is declared text, and its own description says what it carries: "Position machine name (references sys_position.name)." The field name tells you nothing here, because this object mixes both kinds freely: user_id, granted_by, delegated_from and certified_by are all genuine lookups, and only three of its fourteen fields carry an _id suffix at all. The neighbouring object binds the same way: a direct permission-set grant is a sys_user_permission_set row whose permission_set column is text and carries the set's name ({ "user_id": "…", "permission_set": "clm_legal" }) — there is no id-typed column to reach for.

POST /api/v1/data/sys_user_position
{ "user_id": "…", "position": "clm_legal_counsel" }

A name-spelled row grants the permission set that position names (permissionSets in src/sharing/positions.ts). clm_requester does not come from the position but from membership — a lawyer holds clm_legal and clm_requester both, and security/explain attributes the second to the "additive baseline". To read back what an account actually ended up with:

POST /api/v1/security/explain
{ "userId": "…", "object": "clm_contract", "operation": "read" }

object and operation are both required there — a body carrying only userId answers 400 VALIDATION_FAILED.

An id in the position field is accepted, not refused. Nothing resolves that column on write, so a row spelled with the position's id is stored verbatim, matches no position and grants no permission set — and the write still answers 201, with no diagnostic anywhere to say so. The account lands where a plain member does: signed in with clm_requester alone, served 我的合同 and 分析 but none of the position's own groups, with nothing in the response that created it pointing at why. The silent acceptance is the platform's — objectstack-ai/objectstack#16712, still open — so until that lands, the spelling is the whole defence.

A manager is an admin operation, not a data write. Set Manager posts exactly this, as a platform admin (the dev admin is one):

POST /api/v1/auth/admin/set-user-manager
{ "userId": "…", "managerId": "…" }

The data API refuses the column outright: a PATCH /api/v1/data/sys_user/… carrying manager_id answers 403 PERMISSION_DENIED, because it edits only name, image and locale on a user.

What the seeded rows do NOT carry, and why

The demo is loaded as system data, not driven through the flows. Two consequences are worth knowing before you read a number:

  • No approval requests exist. F5 (contract_approval) opens a sys_approval_request on entry to in_approval, and the seed loader suppresses record-change automation — so 42 contracts sit at in_approval or beyond with approval_status set and no request behind them. The approvals inbox is empty, and a dashboard counting approval requests will read zero while the contract list reads 42. The other 48 executed contracts carry is_backfilled: true, which is the model's own word for a contract that reached active without review or approval (DESIGN.md §13 Q8) — they claim no approval history because they have none.
  • Every document is a demo PDF. The 300 contract versions, the templates of six contract types, the executed copy of the 24 completed signature rounds and the evidence of the 90 completed obligations each hold one of seven one-page PDFs shipped in src/data/fixtures/. Each says on its face that it is a demo document and carries no terms, party or signature. The platform stores a separate copy for every row (seedFile, @objectstack 18.2.0), so each one downloads and can be replaced or deleted on its own. Every contract has exactly one current version: the newest negotiation version, never an executed copy unless it is the only version. The 30 contracts in negotiation carry three to five versions with at least one counterparty redline, the 24 negotiated executed contracts add their executed copy, the 48 backfilled ones carry only the executed copy, and every draft, submitted and cancelled contract carries its first draft. That first draft is what lets a seeded draft be submitted. The supplier, lease and amendment types ship without a template, so the launch form asks for an upload on those and starts from the template on the other six. A re-run of pnpm demo stores no file again, and it leaves alone which version is current, a template, an executed copy and evidence once you have changed them.

License

Apache-2.0 — see LICENSE.

About

Contract lifecycle management on ObjectStack — buy-side, sell-side and everything in between, as typed metadata. Self-serve intake, a clause playbook, a data-driven approval matrix, e-signature and execution formalities, obligations and payment schedules: the whole lifecycle in one readable repository.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages