You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Writes to fields declared readonly: true are silently dropped — including writes issued by the platform's own automation engine (flow update_record / create_record nodes). The only signal is a WARN in the server log (Field 'X' is read-only — ignoring incoming change (#2948)); the flow step still reports status: "success" and the run completes "successfully" while the intended data change never happened.
Declare a field readonly: true (e.g. crm_opportunity.approval_status, crm_lead.converted_account, crm_case.is_sla_violated).
Have a flow write it via an update_record node (e.g. the approval flow stamping approval_status: 'approved', or a lead-conversion flow stamping converted_account).
Run the flow and inspect the flow run + the target record.
Actual behaviour
Server logs: WARN Field 'approval_status' is read-only — ignoring incoming change (#2948).
The flow run’s step shows {"nodeId":"mark_converted","status":"success"} — no error, no partial-failure marker.
The field retains its old value. Downstream logic that depends on it (approval entry conditions, "converted" list filters, SLA violation reports) silently misbehaves.
Expected behaviour
Pick one and make it consistent + documented:
(preferred)readonly should govern the external API / UI surface only, and not block trusted server-side writers (flow engine, hooks running as system, seeds). This matches the mental model most apps have — "users can't edit this, but automation maintains it."
OR, if readonly is meant to be absolute, a flow update_record that targets a readonly field should fail the node (surface an error), not silently succeed — so authors discover the mismatch at design time instead of shipping a no-op automation.
Either way, a dropped write should never be reported as a successful step.
Related: flow step reports success on failure
Separately observed: a create_record node that actually failed (crm_contact is required) still surfaced its error object stringified as "[object Object]" in one code path, and the enclosing run summary’s per-step status wasn’t always a reliable indicator of the node outcome. Step status should be authoritative and errors should serialize to a readable message.
Impact
High. This turns whole categories of automation (approval status tracking, lead-conversion stamping, SLA flags, campaign rollups) into silent no-ops. Because validate/build can’t see it and the run reports success, it only surfaces by manually reading server WARN logs or noticing wrong data days later.
Workaround
Remove readonly from any field an automation writes and enforce edit-protection with a validation rule instead (record.is_converted == true && record.x != previous.x → error). Works, but loses the field-level UI affordance and is easy to forget.
可观测性部分已交付(本单「a dropped write should never be reported as a successful step」这一诉求):#3407 → PR #3413(已合并)让引擎在 readonly(#2948)/ readonlyWhen(#3042)剥离时给步骤挂 advisory warnings[] + 结构化 droppedFields 输出,并随 run history 持久化;objectui #2802(已合并)让 Studio 的 Runs 面板把它渲染成琥珀色 warning(状态仍为 success,不降级)。所以「节点报 success 但写入没落且完全沉默」这一条,从数据层到 Studio 已经打通、不再沉默。
changed the title [-]readonly fields silently drop writes from the flow engine; failed step still reports success (#2948)[/-][+]spec: formalize `readonly` write semantics — keep user-context strip, add design-time lint for flow writes to readonly fields (#2948 follow-up)[/+]on Jul 24, 2026
D1 — new @objectstack/lint rule validateReadonlyFlowWrites, wired into os validate and os compile/os build. Static readonly write from a runAs:'user'update_record → build error; readonlyWhen → warning. Skips create_record (INSERT is engine-exempt), runAs:'system', and dynamic object/field shapes to stay false-positive-free. Verified red on a crafted app-crm flow, clean on all example apps.
D2 — the formal readonly contract (user/API surface strips; runAs:'system' / hooks / seeds maintain) documented in the objectstack-data and objectstack-automation skills.
The create_record"[object Object]" error-serialization item remains split out as #3450.
readonly semantics + design-time discovery → feat(lint,cli): flag flow update_record writes to readonly fields at design time (#3425) #3465 (merged, 4340f1319): formalized the contract (user/API surface strips; runAs:'system' / hooks / seeds maintain) and added the validateReadonlyFlowWrites build gate to os validate / os build, with the contract documented in the objectstack-data / -automation skills.
Remaining item (create_record failure serializing as "[object Object]") tracked independently in #3450.
Cross-reference — the "INSERT (all callers): exempt" row of the 2026-07-24 ruling table is superseded (2026-09-03)
Director seat, session session_01ShyhexkB2d1AeRZ85tgAAe. Maintainer ruling C on #14147 (recorded there today, provenance in that comment; verbatim reply 「同意」 to a batch item presented explicitly as overturning this row): static readonly is now stripped from non-system INSERT payloads inside engine.insert, the same way engine.update already strips it; seeding readonly columns at create time is done under system context, which this table already passes. The other rows of the table stand unchanged. Readers of this thread, of packages/lint/src/validate-readonly-action-writes.ts's "BY DECISION" comment, and of the skills/objectstack-data / skills/objectstack-automation prose should follow #14147.
Activity
os-zhuang commentedon Jul 24, 2026
分诊一下这条与近期工作的关系:
可观测性部分已交付(本单「a dropped write should never be reported as a successful step」这一诉求):#3407 → PR #3413(已合并)让引擎在
readonly(#2948)/readonlyWhen(#3042)剥离时给步骤挂 advisorywarnings[]+ 结构化droppedFields输出,并随 run history 持久化;objectui #2802(已合并)让 Studio 的 Runs 面板把它渲染成琥珀色 warning(状态仍为success,不降级)。所以「节点报 success 但写入没落且完全沉默」这一条,从数据层到 Studio 已经打通、不再沉默。但本单还有两个独立诉求没被 #3413 覆盖,建议保留跟进(或拆两条子单):
readonly的策略拍板(本单「Expected behaviour」首选项):readonly是否应只管外部 API / UI 面、放行受信任的服务端写入方(流程引擎 / 系统 hook)?现状:runAs: 'system'的流程本就跳过剥离,但runAs: 'user'的流程写readonly字段仍被剥离 —— feat(automation): update_record/create_record 步骤对被静默剥离的写入字段挂 warning(#3407) #3413 只是让它可见,没改变行为。要不要让「自动化可维护、用户不可编辑」成为readonly的正式语义,是个 spec 级拍板题。create_record失败时错误序列化成"[object Object]"(本单「Related」小节):这是独立的错误序列化 bug(step 错误对象没走.message),与剥离无关,feat(automation): update_record/create_record 步骤对被静默剥离的写入字段挂 warning(#3407) #3413 未触及。如果同意,我可以把本单标题收敛到上面第 1 点(readonly 策略),把第 2 点拆成一条独立的「create_record 错误序列化」bug 单。要我来拆吗?
Generated by Claude Code
[-]readonly fields silently drop writes from the flow engine; failed step still reports success (#2948)[/-][+]spec: formalize `readonly` write semantics — keep user-context strip, add design-time lint for flow writes to readonly fields (#2948 follow-up)[/+]os-zhuang commentedon Jul 24, 2026
拍板 + 开发计划(分诊定稿)
沿上面分诊评论的两个遗留点,结论如下。标题已按此收敛;
[object Object]序列化已拆出为独立子单 #3450。决策:不采纳「flow 引擎豁免 readonly」(本单原首选方案),维持现状语义并升格为正式契约
理由:
runAs:'user'(默认)的 flow 语义是「以触发用户的身份行事」——RLS 与 grants 都按该用户强制(service-automation: runAs:'user' runs data ops with a credential-less user — trigger context never carries the actor's permission sets/positions (follow-up to #1888) #3356)。若 readonly 对 flow 引擎整体豁免,任何用户可触发的 flow 都成为 readonly 绕过 gadget:用户控制的输入(触发记录字段、screen 输入)流进update_record即可改写审计戳/provenance/系统计算值。security: 服务端 readonly 字段在 UPDATE 时未强制(可被用户上下文覆盖) #2948 是安全修复,豁免等于打穿它。runAs:'system'的语义——盖approval_status章、stampingconverted_account本就该以系统身份执行,而非以「恰好触发流程的那个人」的身份。本单复现场景的正确修法是 flow 声明runAs:'system',而不是改平台语义。readonly改用 validation rule 挡编辑)方向相反,不推荐;文档将明确此点。正式契约(现状行为,零变更、零破坏,只是文档化):
readonlyreadonlyWhenrunAs:'user'(默认)droppedFields)runAs:'system'/ system hooks / seeds已交付,不再重复开发
「dropped write 不得报 clean success」已由 #3407 → PR #3413(引擎侧
warnings[]+droppedFields持久化)+ objectui #2802(Studio Runs 面板琥珀色警告)打通。本单不再覆盖可观测性。待开发
D1(P1)· 设计时 lint 门 —— 本单剩余的开发主体。
@objectstack/lint新增validateReadonlyFlowWrites(stack),接进os validate/os compile现有管线(cli/src/commands/validate.ts已串接的validate*系列):update_record节点中字面可知的字段名 × 目标对象readonly:true× flowrunAs !== 'system'→ error(该组合下写入 100% 无效,无假阳性空间);命中readonlyWhen字段 → 最多 warning(条件性,静态不可判定);{$record}展开、模板变量作字段名)静态不可知 → 跳过,不产假阳;object/objectName、data/keyValue两种形态,参照 feat(automation): publish configSchemas for the keyValue-capable nodes (designer parity, #3304) #3327 configSchemas),认不全即漏报;这把发现时机从「运行时琥珀警告」提前到「保存/构建时」,直接闭掉本单「validate/build 看不见、只能事后翻 WARN 日志」的核心抱怨。
D2(P1,随 D1)· 语义契约文档化:objectstack-data skill(readonly 字段处)与 objectstack-automation skill(
runAs处)写入上表,推荐模式定为「自动化维护的字段 → flow 声明runAs:'system'」,并把 validation-rule workaround 标注为不推荐。明确不做
update_record的strict/onDroppedFields:'fail'旋钮(lint 门 +runAs:'system'已覆盖全部合理场景,多旋钮徒增语义噪音)。拆单
create_record失败错误序列化"[object Object]"→ automation: step error objects can surface as "[object Object]" in run-history consumers (split from #3425) #3450(main 上服务端路径已.message化,剩余嫌疑在StepLogEntry.error结构化对象的消费方,详见该单)。Generated by Claude Code
os-zhuang commentedon Jul 24, 2026
D1 + D2 delivered in #3465:
@objectstack/lintrulevalidateReadonlyFlowWrites, wired intoos validateandos compile/os build. Staticreadonlywrite from arunAs:'user'update_record→ build error;readonlyWhen→ warning. Skipscreate_record(INSERT is engine-exempt),runAs:'system', and dynamic object/field shapes to stay false-positive-free. Verified red on a crafted app-crm flow, clean on all example apps.readonlycontract (user/API surface strips;runAs:'system'/ hooks / seeds maintain) documented in the objectstack-data and objectstack-automation skills.The
create_record"[object Object]"error-serialization item remains split out as #3450.feat(lint,cli): flag flow update_record writes to readonly fields at …
os-zhuang commentedon Jul 24, 2026
Closing — all in-scope requests delivered:
warnings[]+droppedFields) + objectui 调查(核心):评估“服务不可用”的重试语义 #2802 (Studio Runs amber warning).readonlysemantics + design-time discovery → feat(lint,cli): flag flow update_record writes to readonly fields at design time (#3425) #3465 (merged,4340f1319): formalized the contract (user/API surface strips;runAs:'system'/ hooks / seeds maintain) and added thevalidateReadonlyFlowWritesbuild gate toos validate/os build, with the contract documented in the objectstack-data / -automation skills.Remaining item (
create_recordfailure serializing as"[object Object]") tracked independently in #3450.fix(automation): flow string templates serialize object tokens readab…
fix(lint,cli): os lint 不再放行另外两个命令拒绝的 flow (#4345 后续) (#4394)
test(cli): pin that all three commands run the reference-integrity su…
os-project-manager commentedon Sep 3, 2026
Cross-reference — the "INSERT (all callers): exempt" row of the 2026-07-24 ruling table is superseded (2026-09-03)
Director seat, session
session_01ShyhexkB2d1AeRZ85tgAAe. Maintainer ruling C on #14147 (recorded there today, provenance in that comment; verbatim reply 「同意」 to a batch item presented explicitly as overturning this row): staticreadonlyis now stripped from non-system INSERT payloads insideengine.insert, the same wayengine.updatealready strips it; seeding readonly columns at create time is done under system context, which this table already passes. The other rows of the table stand unchanged. Readers of this thread, ofpackages/lint/src/validate-readonly-action-writes.ts's "BY DECISION" comment, and of theskills/objectstack-data/skills/objectstack-automationprose should follow #14147.Generated by Claude Code