Skip to content

notify node templates can't resolve formula/lookup fields — renders blank silently #3426

Closed

Description

@os-zhuang

Summary

A notify node's message templates (title / body) cannot traverse lookup or formula fields of the triggering record. {record.full_name} (a formula field) and {record.account.name} (a lookup hop) render as empty strings, silently producing notifications like "New lead to assign: " with the name missing.

Environment

  • @objectstack/* 16.1.0 (open edition), single-tenant, sqlite-wasm driver

Steps to reproduce

  1. On an object, define a formula field, e.g. crm_lead.full_name = F\record.first_name + " " + record.last_name``.
  2. In a record_change flow's notify node, use it in a template:
    title: 'New lead to assign: {record.full_name}',
    body: '{record.full_name} from {record.company}',
  3. Trigger the flow (insert a lead) and read the resulting sys_inbox_message.

Actual behaviour

  • The inbox message title is "New lead to assign: " — {record.full_name} resolved to empty.
  • Same for a lookup hop like {record.crm_account.name}.
  • Non-computed scalar columns on the same record ({record.company}, {record.first_name}) interpolate correctly.

Expected behaviour

The record seeded into a record-change run should expose the same computed/related fields the record has when read via the data API — at minimum formula fields, ideally single-hop lookups. If traversal genuinely isn't supported, templates referencing an unresolvable path should be documented as unsupported and ideally warn at build time, rather than silently emitting blanks.

Impact

Medium. Notifications and alerts are a primary flow output; silently blank names/values make them look broken to end users and there's no design-time signal.

Workaround

Recompose from raw scalar columns in the template, e.g. {record.first_name} {record.last_name} instead of {record.full_name}. Works for formula fields built from local columns; doesn't help for lookup traversal.

Activity

  1. self-assigned this
    on Jul 24, 2026
  2. os-zhuang commented on Jul 24, 2026

    @os-zhuang
    ContributorAuthor

    进展:formula 字段部分已修复(#3445 已合并)

    已合并 PR #3445(squash 进 main,1dc94f0),解决了本 issue 的 formula 公式字段 部分。

    根因:formula 是读取时(read-time)虚拟字段——引擎只在 find/findOne 取数后计算它,写入路径既不计算也不落库。而 record-change 触发器喂给流程的种子 record 直接来自生命周期钩子的原始行(ctx.result 叠加输入 doc),这一行根本不含公式字段,所以 {record.full_name} 被插值成空字符串。

    方案:RecordChangeTrigger(record 的生产者)在组装种子记录后,对 afterInsert/afterUpdate 以系统身份回读刚写入的记录,让种子 record 与「数据 API 默认读取」拿到相同的计算字段。修复在触发器层,因此整条流程(开始条件、所有节点、notify 的 title/body)都受益,而非只给 notify 打补丁。刻意保守:原始字段优先合并、只补齐缺失的公式虚拟字段、任何失败都回退原始记录。

    改动文件:packages/triggers/trigger-record-change/src/record-change-trigger.ts(核心为新增的 hydrateComputedFields);测试见 record-change-trigger.test.ts 与新增的 formula-context.test.ts。


    仍待跟进(留给后续 agent)

    本 issue 暂不关闭,以下两点尚未处理:

    1. lookup 跨表遍历 {record.account.name}

    有意未在本次一并做:默认数据 API 读取本身也不展开关联;若在种子记录里默认展开,record.account 会从标量外键 id 变成对象,反而破坏那些把它当 id 用的模板/条件(例如 #1872 的 {record.target_channels.0})。可选方向:

    • A(推荐先定方向):在 flow 节点上做 opt-in 的 expand 配置(显式声明展开哪些关联),回读时带上 expand 传给 ObjectQL.findOne(引擎已支持 expand map,见 packages/objectql/src/engine.ts 的 expandRelatedRecords)。
    • B:在模板引擎里对 record.<lookup>.<field> 做惰性解析(需给 template.ts 的插值器注入数据引擎句柄)。

    2. 构建期告警(issue 里的兜底诉求)

    对模板引用了不可解析路径(尤其是 lookup 跨表)的情况,在 flow lint / publish gate 里加校验,把当前的静默空白变成显式告警。此项独立于 1,可先落地。

    备注:formula 的旧 workaround({record.first_name} {record.last_name} 重组)在 #3445 后已不再需要。


    Generated by Claude Code

  3. os-zhuang commented on Jul 25, 2026

    @os-zhuang
    ContributorAuthor

    Resolved — all three asks landed on main

    Closing: every part of this report is now fixed.

    Ask Fix PR
    {record.full_name} (formula field) renders blank Trigger hydrates read-time formula virtuals via a system re-read of the just-written row #3445
    No design-time signal for unresolvable template paths New os validate rule validateFlowTemplatePaths — warns on unknown-field (flow-template-unknown-field) and lookup-traversal (flow-template-lookup-traversal) refs instead of a silent blank; plus re-read guards (skip when no formula field, memoize per write) #3472
    {record.account.name} (lookup hop) renders blank Opt-in single-hop expansion: a flow declares expand: ['<lookup>'] on its start node and the engine re-reads the relation as the run's own identity (runAs:'user' ⇒ the triggering user, RLS/FLS enforced), grafting it onto the record so the template resolves #3482 (closed #3475)

    Notes:

    Multi-hop traversal and a typed start-node schema for expand discoverability are possible future enhancements, out of scope here.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions