Repository navigation
[Decision] A notify node already mails a written-out or computed email address: does ruling 6377842908's 「⛔ 不接受自由文本或表达式拼出的地址」 reach it — refuse both (A), refuse computed addresses only (B), or leave both (C)? #25647
Description
Ruled: 6386629237 · batch #12 item 2 · letter B · 2026-10-24T16:24:48Z (a computed address no user holds fails the node pointing to { emailField }; a written-out address stays an author constant; rides #25571 + a spec describe-text card)
Filing gate ②: only the maintainer can decide this. The dev raised it as an open question on #25610 (report 6384295899, open_questions[2]), and the domain:spec seat 2 review of PR #25645 confirms it. Filed by domain:spec seat 2 (seat post #18549) · sunhaolin · session session_01PC4NPXPgPmpDRG9DdDnyXT. ⛔ Not a claim. Domain and grade are triage's; the seat suggests domain:services (where B and most of A live) and p2 (it limits a path that works today; no new protocol).
Reader who acts: the director seat presents it. After the ruling, the lane that owns the letter acts (see After the ruling).
维护者速读
- 事由: service-messaging: a flow
notifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25444 的裁决给notify加了「收件人取自记录的 email 类型字段」这一新形态(spec(automation): anotifyrecipient form that is an address from anemail-typed field of the triggering record — the protocol half of #25571 (ruling 6377731715, letter B) #25610 / PR feat(spec,lint): a notify recipient form{ emailField }— the address in an email-typed field of the triggering record #25645 正在做),执行条件写着「收件人只能来自记录的 email 类型字段,⛔ 不接受自由文本或表达式拼出的地址」。但notify今天本来就能把邮件发到一个写死的地址('ops@example.com'),也能发到一个表达式算出来的地址,只要那个地址不属于任何用户就原样发出去。裁决这句话管不管这条老路,原文没说。 - 要你定: A 两条老路都关(写死的、算出来的都拒绝,外部地址只能走 email 字段);B 只关「算出来的」(推荐:写死在元数据里的固定地址照发,表达式算出的地址若不属于任何用户就拒绝);C 都不关,裁决只管新形态。
- 为什么现在定: 按地址限速、写消息日志正在 service-messaging: a flow
notifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571 里做。现在定下来,service-messaging: a flownotifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571 就知道这些要套到哪几条路上;B 的运行时检查也可以和 service-messaging: a flownotifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571 落在同一批文件里。
Background
Ruling 6377731715 on #25496 (batch #388 item 4, the maintainer's 「同意」), item 3, verbatim: "#25444 → B. A notify recipient form that is an address taken from an email-typed field of the triggering record — never a free string — rendered through the same templates, subject to the environment's outbound mail configuration, rate-limited per address, written to the messaging log." The governing ruling on the same row, 6377842908 (reconciliation 6377965040), gives the execution condition verbatim: 「收件人只能来自记录的 email 类型字段,⛔ 不接受自由文本或表达式拼出的地址;按地址限速,记消息日志」.
Two paths that exist today put an address no user holds into outbound mail:
- Written out. A string recipient shaped like an email is delivered as a literal address by the email channel:
packages/services/service-messaging/src/email-channel.tsabout:192,if (EMAIL_SHAPE(recipient)) { … return { address: recipient, … } }. The resolver keeps it verbatim when no user holds it (recipient-resolver.tsabout:77, "'<email>'→sys_userlookup by email → id (verbatim on miss)"). The spec docblock names it as an audience selector (packages/spec/src/automation/io-node-config.zod.tsabout:277, "team:ID, an email address"). - Computed. A CEL envelope in
recipientsmay evaluate to any string, so{ dialect: 'cel', source: 'record.contact_text' }over a free-text field reaches the same branch.
PR #25645 does not touch either path; its Acceptance notes record them.
Governing text: the ruling's 「收件人只能来自记录的 email 类型字段」 and 「⛔ 不接受自由文本或表达式拼出的地址」; facet ③ of the director's increment 6377530193, 「#25444 收件人只许记录的 email 字段,不许自由串」. Read literally, the words cover every recipient. Read in context, they are the execution condition on the new form for #25444, where the risk named is a guest-supplied value turning outbound mail into a relay. The text does not settle which reading holds, so it is raised.
Protocol: A narrows the protocol (Clause-②: yes (narrowing), BREAKING). B narrows a runtime behaviour and the spec describe text, not the accepted shape. C changes nothing but the describe text.
Premises, each with its re-check
- The written-out path delivers:
git grep -c "if (EMAIL_SHAPE(recipient))" origin/main -- packages/services/service-messaging/src/email-channel.ts→ 1. Control:git grep -c "verbatim on miss" origin/main -- packages/services/service-messaging/src/recipient-resolver.ts→ 1. - In-repo producers of the written-out path:
git grep -cE "recipients\s*:\s*\[?\s*['\"][^'\"]*@" origin/main -- examples→ 5, all inexamples/app-showcase/src/automation/flows/index.ts(admin@objectos.ai×4, the seeded admin, a user;exec@example.com×1, no user). Control:git grep -c "recipients:" origin/main -- examples/app-showcase/src/automation/flows/index.ts→ 21. - No in-repo envelope computes an address: every
recipientsenvelope inexamples/**reads a user reference (record.assignee,record.owner,task.owner,ownerId,record.account.owner, …). Re-check:git grep -nE "recipients\s*:\s*\[?\s*\{\s*dialect" origin/main -- examplesand read eachsource.
The question
Does the ruling's 「⛔ 不接受自由文本或表达式拼出的地址」 apply to the recipients a notify node already accepts, or only to the new email-field form?
Options
| Letter | What is done | What the customer sees | Workload |
|---|---|---|---|
| A | notify refuses an email-shaped string recipient at publish (spec and lint), and the run fails an envelope whose value is an address no user holds. An external address comes only from { emailField }. |
A flow that mails a fixed mailbox (ops@company.com) is refused at deploy; the author must give that mailbox a user or a record field. The 5 showcase nodes change (4 to the admin's user id). |
domain:spec + domain:services · 2 PRs · BREAKING changeset and migration notes. Nearest precedent: #24983 (refusing template rows for unsupported channels, the 19.0 half of ruling D). |
| B | A written-out address in the metadata keeps working, as an author-chosen constant. A computed address, an envelope that evaluates to an address no user holds, fails the node with a located message pointing to { emailField }. An envelope that evaluates to a user's address still resolves to that user. |
Fixed mailboxes keep working. A flow that builds an address from record text (the relay risk) is told at run time to use an email field. No in-repo producer changes. | domain:services (service-automation's resolveRecipients or the messaging resolver) plus the spec describe text · 1 PR, could ride #25571 · no precedent. |
| C | Both paths stay. The describe text states the two ways to an external address (written out, or { emailField }); #25571's per-address rate limit and log apply to both. |
Nothing changes. A guest-filled text field fed through an expression can still pick the address a flow mails. | domain:spec describe text · S. |
Business meaning: A is a mailroom that only delivers to staff or to an address printed on the customer's own file. B also takes a fixed address the manager wrote on the routing slip, but refuses one the visitor wrote in the margin. C delivers to any address on the slip.
Four axes
- Long-term (leads): B is the shape mainstream platforms settled on. Salesforce email alerts address an outside person through an Email-type field of the record or up to five written-out "additional emails". B makes 「表达式拼出的地址」 false by construction and keeps the authored constant. A goes further than those platforms. C leaves the relay path the ruling names.
- Real business need: the pull behind service-messaging: a flow
notifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25444 is mailing an applicant (hotsrm), and{ emailField }covers it. Mailing a fixed shared mailbox (finance, ops) is a common ask that A would break and B keeps. Nobody in-repo computes an address today, so B refuses nothing anyone has. - Preventing AI mistakes: under B, an AI that writes
record.contact_textas a recipient gets a loud refusal naming the right form. Under C, that config works and opens a relay. Under A, an AI writing an ops alert hits a refusal whose fix (create a user) is heavier than the need. - Startup focus: C costs a describe sentence. B is one runtime check plus that sentence and could ride service-messaging: a flow
notifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571. A is a breaking narrowing with migration notes.
Recommendation: B. Two years out, mainstream workflow platforms reach an outside address through an authored constant or an email field (Salesforce email alerts take exactly those two). Confidence gap: whether "free text" in the ruling also meant the authored constant. If it did, the answer is A, and the seat would not argue it. Not A: it removes a common, low-risk need (a fixed mailbox) that the ruling's risk does not cover. Not C: it keeps the exact path the ruling's second clause names.
Self-check: looking at ① alone, B. Do ②③④ flip it? No.
After the ruling
- A: the
domain:specseat files the narrowing card (Clause-②: yes (narrowing), BREAKING, showcase migration), and adomain:servicescard for the run-time refusal. The PR feat(spec,lint): a notify recipient form{ emailField }— the address in an email-typed field of the triggering record #25645 describe text stands. - B: a
domain:servicescard (or a line in service-messaging: a flownotifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571's scope, if that lane takes it) for the run-time refusal of a computed address no user holds. Thedomain:specseat files a describe-text card that names the written-out address as a constant and drops "an email address" from the audience-selector list. - C: a
domain:specdescribe-text card naming both ways; service-messaging: a flownotifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571 applies the rate limit and log to both.
os-decision-facets
- ① 项目长远合理性:B 与主流平台一致(Salesforce 邮件提醒给外部地址发信只走两条:记录的 Email 字段、最多 5 个写死的附加地址),把「表达式拼出的地址」从结构上关掉,同时保留作者写死的常量;A 比主流更紧;C 留着裁决点名的那条转发路。
- ② 实际业务拉动:service-messaging: a flow
notifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25444 的拉动是给申请人发邮件(hotsrm),{ emailField }已覆盖;发到固定共享邮箱(财务、运维)是常见需求,A 会打断,B 保留;仓库内没有任何表达式算地址,B 不拒绝任何现有配置。 - ③ 防 AI 犯错:B 下 AI 把
record.contact_text写成收件人会被响亮拒绝并指向正确形态;C 下这种配置能跑且开出转发口;A 下 AI 写个运维告警就被拒,改法(建用户)比需求重。 - ④ 创业阶段不扩散:C 只改一句描述;B 一处运行时检查加那句描述,可随 service-messaging: a flow
notifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571 一起落;A 是带迁移说明的破坏性收窄。
Prior rulings read: "notify recipient email address free text expression" → 2 hits (6377731715, 6377842908 on #25496; reconciliation 6377965040; director increment 6377530193); ADR-0012 (recipient resolver), ADR-0030 P1 (the resolver's single home); thread: none (new card)
Dedupe
- Titles of the 100 objectstack-ai/objectstack issues most recently updated since 2026-09-15 (open and closed, repo-scoped
issuesendpoint), matched onliteral,email address,free text,EMAIL_SHAPE,recipient: 3 hits, all this question's lineage (spec(automation): anotifyrecipient form that is an address from anemail-typed field of the triggering record — the protocol half of #25571 (ruling 6377731715, letter B) #25610, service-messaging: a flownotifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571, service-messaging: a flownotifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25444). - Semantic issue search "notify recipient literal email address string free text expression refuse": 2 hits, neither this question (spec(automation): a
notifyrecipient form that is an address from anemail-typed field of the triggering record — the protocol half of #25571 (ruling 6377731715, letter B) #25610; plugin-email: the Send test email refusals tell the admin to pass "to" in the request body — name the page's control instead (same family as #25345) #25424, a test-email refusal message). Control query "service-messaging notify applicant public form": 7 hits, including service-messaging: a flownotifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25444 and service-messaging: a flownotifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571. - New.
Activity
objectstack-fleet commentedon Oct 24, 2026
Triage: first touch —
enhancement·priority:p2·domain:server·area:workflow·needs-user-decision· triage seat ·session_01AavokzJ5DndAwitDXvKy4U· 2026-10-24T13:53Z⛔ Not a claim, ⛔ not a dispatch.
domain:server. The seat suggesteddomain:services, which is now an alias ofdomain:server(lane merge, 2026-10-22). p2, as the seat suggests: it limits a path that works today and adds no key.notifynode can only address system users, so an applicant who came in through a public form never hears "received" or "rejected" #25571's files, plus the describe text.Generated by Claude Code
os-elon-musk commentedon Oct 24, 2026
Ruling: batch #12 item 2 · letter B · maintainer 「25635 25647 25675 同意」 2026-10-24T16:24:48Z
B = 只关「表达式算出来的地址」:
notify收件人若由表达式(CEL 信封)求值得到一个不属于任何用户的邮箱地址,节点失败并给出定位提示指向{ emailField };求值得到某个用户的地址仍解析为该用户。元数据里写死的地址是作者常量,照常发出。裁决 6377842908 的「⛔ 不接受自由文本或表达式拼出的地址」按此读:它管的是运行时拼出的地址,不管作者写死的常量。工作量:
domain:server1 个 PR(service-automation 的resolveRecipients或消息解析器),可并入 #25571 的文件;domain:spec改描述文字,把「写死地址」写成作者常量,并从受众选择器列表里去掉笼统的「an email address」。仓库内无任何表达式算地址的生产者,B 不拒绝任何现有配置。维护者动作已完成:
needs-user-decision→pm:queue;裁不派。批 #12 简报:https://claude.ai/artifact/YLrSzMXcnWVmbkZCxgxcQtGenerated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Pointer ·
domain:specseat 2 · 2026-10-24T16:47Z · the spec half of ruling B6386629237(therecipientsdescribe names a written-out address as an author constant and drops the bare "an email address" selector) is filed as #25696, to land with or after this card's runtime half.Generated by Claude Code
objectstack-fleet commentedon Oct 24, 2026
Claim: PM loop round 1 · rides #25571 under ruling
6386629237(「可并入 #25571 的文件」); the full claim is on #25571Session:
session_01R1q2xdtYgU6FKrZ7LgjPCC(the seat); the card thread issession_01CmnfU7vRbNtSFtBQwyyKpBAccount:
os-project-manager(the seat's linked user asGET /useranswers it; the card's assignee)Branch:
claude/project-thread-e1l64i(shared with #25571; one PR, oneFixesline per card)Domain:
domain:serverSeat:
domain:server#1File surface: as on #25571 (
packages/services/service-automation/src/builtin/notify-node.tsresolveRecipientsfor this card's half). ⛔ Notpackages/spec/**: ruling B's describe half is #25696.Container & model:
M,mode:thread,model: opus(the default ofdispatch-gates --tier),effort: xhighClause-②: no
Metadata-protocol: narrowed
Responsibility: n/a — not a defect card
Thread-read: 6386884557
Premise: holds (2026-10-24T23:07Z: no in-repo
recipientsenvelope computes an address, per the card's re-check; the dev re-runs it)Serial constraints cleared: see #25571's claim.
fix(service-automation): screen a computed address only when the noti…
objectstack-fleet commentedon Oct 25, 2026
Landed with #25571: PR #25721 (
d1e329e96d, 2026-10-25T04:41:00Z) carries ruling B's runtime half. Landing record: #25571 (the seat's comment of 2026-10-25T04:43Z). Card closed byFixes;pm:dispatchedremoved.Generated by Claude Code
objectstack-fleet commentedon Oct 25, 2026
Ruling record · confirmation of ruling
6386629237B's channel scope ·domain:serverseat 1 (seat post #6367) ·session_01R1q2xdtYgU6FKrZ7LgjPCC· 2026-10-25T05:05ZProvenance: who: Leehom (maintainer) · said: chose 「A:只查含 email 通道的节点」 on the question 「#25647 裁决 B 的「表达式算出的地址必须属于用户」只管发邮件的节点吗?」 · when: 2026-10-25T05:04:48Z · where: the server-lane project thread, on a decision card the seat posted.
A = ruling B's refusal of a computed address no user holds applies only to a
notifynode whosechannelsincludeemail. A node oninboxalone (the messaging default) mails nobody, so its computed recipients are not screened. This is what PR #25721 (d1e329e96d) shipped; nothing further is owed. The contract reviews' flag F2 (6392441398,6392736321) is closed by this answer. The describe text on #25696 should say the screen is scoped to the email channel.Generated by Claude Code