Filed from the objectui side while implementing objectstack-ai/objectui#5840 (record:activity was dropping every sys_activity.type: "scheduled" row). The renderer half is fixed there. This half lands here, because the declaration lives here.
Measured
packages/plugins/plugin-audit/src/objects/sys-activity.object.ts declares type as a select over eleven values:
created updated deleted commented mentioned shared assigned completed login logout system
scheduled is not among them. A shipped producer writes it anyway — objectstack-ai/hotcrm, src/actions/global.actions.ts:
constactivity=awaitctx.api.object('sys_activity').insert({type: EVENT_STATUS==='held' ? 'completed' : 'scheduled',
...registered for crm_lead, crm_contact, crm_account, crm_opportunity and crm_case. Both branches land in the column; only the completed branch was declared.
Why the write is not rejected
This repo already measured the mechanism, in the vocabulary pin's own docblock: every field on sys_activity is readonly: true, and objectql's validateRecord skips readonly fields on both the insert and update branch, so the invalid_option check that enforces a select field's declared options never runs for one. The eleven-value enum is documentation, not a contract — an undeclared value is stored silently.
So this is not "an app broke the rules and got caught". Nothing can catch it. The row is written, stored and queryable under a value the platform never declared.
The general door, not the special case
completed's own census row records that the platform itself forwards author-declared values into this column: ADR-0052 section 5b.2 activityMilestones[].type, applied verbatim by audit-writers.ts (if (milestone.type) activityType = milestone.type). Any metadata author can put any string there and it lands. scheduled is simply the first one someone measured downstream.
That makes the question sharper than one missing enum member: is sys_activity.type a closed platform vocabulary or an author-extensible one? Both readings are currently true of the code, which is the actual defect.
What is at stake downstream
objectui's packages/plugin-detail/src/renderers/recordActivityFeed.ts keys its feed-kind map on this vocabulary. Its test used to pin the key set as set-equal to the eleven declared options. objectstack-ai/objectui#5840 breaks that set-equality on purpose — the map now covers the declaration PLUS the values a shipped producer measurably writes — because pinning to the declaration means dropping stored rows. Whichever way this is ruled, that pin should follow it.
Directions
- Declare it — add
scheduled to the enum and a TYPES_WITH_WRITERS row naming the HotCRM writer. Cheapest, and makes the declaration match reality. Does not address the general door. - Rule the producer non-conformant — HotCRM must use a declared value. Keeps the vocabulary closed, but nothing enforces it, so it is a convention that will be broken again silently.
- Enforce the vocabulary — make readonly select fields validate their options on system-owned writes, so an undeclared value is rejected loudly at the point of writing. Largest, and the only one that stops the class.
- Declare it author-extensible — accept that milestone-declared types pass through, and say so, which makes downstream closed maps the bug.
Recommending 1 as the immediate step (it is reversible and it makes the shipped state honest) with 3 as the direction that actually closes it — but this is a vocabulary ruling and a maintainer call, not an objectui one. Recorded, not acted on.
Generated by Claude Code
Filed from the objectui side while implementing objectstack-ai/objectui#5840 (
record:activitywas dropping everysys_activity.type: "scheduled"row). The renderer half is fixed there. This half lands here, because the declaration lives here.Measured
packages/plugins/plugin-audit/src/objects/sys-activity.object.tsdeclarestypeas a select over eleven values:scheduledis not among them. A shipped producer writes it anyway — objectstack-ai/hotcrm,src/actions/global.actions.ts:registered for
crm_lead,crm_contact,crm_account,crm_opportunityandcrm_case. Both branches land in the column; only thecompletedbranch was declared.Why the write is not rejected
This repo already measured the mechanism, in the vocabulary pin's own docblock: every field on
sys_activityisreadonly: true, and objectql'svalidateRecordskips readonly fields on both the insert and update branch, so theinvalid_optioncheck that enforces a select field's declared options never runs for one. The eleven-value enum is documentation, not a contract — an undeclared value is stored silently.So this is not "an app broke the rules and got caught". Nothing can catch it. The row is written, stored and queryable under a value the platform never declared.
The general door, not the special case
completed's own census row records that the platform itself forwards author-declared values into this column: ADR-0052 section 5b.2activityMilestones[].type, applied verbatim byaudit-writers.ts(if (milestone.type) activityType = milestone.type). Any metadata author can put any string there and it lands.scheduledis simply the first one someone measured downstream.That makes the question sharper than one missing enum member: is
sys_activity.typea closed platform vocabulary or an author-extensible one? Both readings are currently true of the code, which is the actual defect.What is at stake downstream
objectui's
packages/plugin-detail/src/renderers/recordActivityFeed.tskeys its feed-kind map on this vocabulary. Its test used to pin the key set as set-equal to the eleven declared options. objectstack-ai/objectui#5840 breaks that set-equality on purpose — the map now covers the declaration PLUS the values a shipped producer measurably writes — because pinning to the declaration means dropping stored rows. Whichever way this is ruled, that pin should follow it.Directions
scheduledto the enum and aTYPES_WITH_WRITERSrow naming the HotCRM writer. Cheapest, and makes the declaration match reality. Does not address the general door.Recommending 1 as the immediate step (it is reversible and it makes the shipped state honest) with 3 as the direction that actually closes it — but this is a vocabulary ruling and a maintainer call, not an objectui one. Recorded, not acted on.
Generated by Claude Code