Skip to content

Commit e577445

Browse files
claude[bot]claude
andauthored
docs(services): state the notify template locale the delivery path resolves (#12505)
The `notify` node's `template` path resolves `(name, locale)` with ONE locale for the whole notification: `payload.locale`, interpolated once before fan-out, else the deployment default (`II18nService.getDefaultLocale()`). `sys_user` carries no locale column and no request exists at async delivery time, so there is no per-recipient source to read; a per-user locale is deferred until measured pull (maintainer ruling, 2026-08-13). `packages/spec` was corrected to say so. The same retired promise survived in five sites outside it, two of them the ones an app author actually reads: - the `template` field's `configSchema` description — the Studio form text — which said the row is "resolved by (name, recipient locale) ... and rendered per recipient"; - `content/docs/automation/email-templates.mdx`, the only site to state the conclusion outright: "so one node mails each person in their own language"; - the messaging channel-registration log line; - two internal comments in `notify-node.ts` (the execute-time guard and the payload the outbox snapshots) and the sibling comment in its test. Text only — no schema, delivery behaviour or wire value moves. A new pin asserts the form description names `payload.locale` and the deployment default and refuses a bare "recipient locale". Part of #12446 Claude-Session: https://claude.ai/code/session_0157mMVAq9fjGe2kaSD2aJC8 Co-authored-by: Claude <noreply@anthropic.com>
1 parent 3dafd8c commit e577445

5 files changed

Lines changed: 93 additions & 11 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
'@objectstack/service-automation': patch
3+
'@objectstack/service-messaging': patch
4+
---
5+
6+
The `notify` node's Studio form and the messaging registration log now state the locale the delivery path actually resolves — one per notification, not one per recipient
7+
8+
`NotifyConfigSchema` was corrected in `packages/spec` to say that the `template`
9+
path resolves `(name, locale)` with **one** locale for the whole notification.
10+
The same retired promise survived outside the spec file, in the places an app
11+
author is most likely to read it:
12+
13+
- `service-automation/src/builtin/notify-node.ts` — the `template` field's
14+
`configSchema` description, i.e. the text rendered in the **Studio form** the
15+
author fills in. It said the row is "resolved by (name, recipient locale) at
16+
delivery time and rendered per recipient".
17+
- `content/docs/automation/email-templates.mdx` — the only site that stated the
18+
conclusion outright rather than merely licensing it: "so one node mails each
19+
person in their own language".
20+
- `service-messaging/src/messaging-service-plugin.ts` — the channel-registration
21+
log line, which advertised "resolve sys_email_template per recipient locale".
22+
- Two internal comments in `notify-node.ts` and one in its test, describing the
23+
payload the outbox snapshots as carrying a per-recipient-locale resolution.
24+
25+
None of that is what the delivery path does. `payload.locale` is interpolated
26+
**once, before fan-out**, so it is a single value for the whole notification, and
27+
its fallback is the deployment default (`II18nService.getDefaultLocale()`). The
28+
platform has no per-user locale to read — `sys_user` carries no locale column,
29+
and request-scoped locale does not exist at async delivery time — so recipients
30+
whose personal languages differ all receive the same template row. A per-user
31+
locale is deferred until measured pull (maintainer ruling, 2026-08-13) and layers
32+
in as an override at that same seam when it lands; the corrected wording dates
33+
the deferral so it reads as a decision with provenance rather than an oversight.
34+
35+
The gap was worth correcting because the wording licensed exactly one action —
36+
convert `notify` nodes on the belief that non-English recipients get non-English
37+
mail — and that action is a **net regression**: `TEMPLATE_*` failures classify
38+
`permanent` and dead-letter, and the inbox channel starts requiring an email
39+
service with `renderTemplate()` where inline text needed none.
40+
41+
Text only: no schema accepts or refuses anything it did not before, no delivery
42+
behaviour moves, and no wire value changes. A new pin in `notify-node.test.ts`
43+
asserts the form description names `payload.locale` and the deployment default
44+
and refuses a bare "recipient locale", so a later edit cannot quietly restore the
45+
promise.

content/docs/automation/email-templates.mdx

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,9 +162,15 @@ the localizable path:
162162
}
163163
```
164164

165-
- `template` names the bundle. The delivery path resolves
166-
`(name, recipient locale)` **per recipient, at delivery time**, so one node
167-
mails each person in their own language.
165+
- `template` names the bundle. The delivery path resolves `(name, locale)`
166+
against `sys_email_template` at delivery time and renders subject/body from
167+
that row. **The locale is one value for the whole notification, not one per
168+
recipient**: `payload.locale` if the producer set one — interpolated once,
169+
before fan-out — else the deployment default (`II18nService.getDefaultLocale()`).
170+
The platform has no per-user locale (`sys_user` carries no locale column), so
171+
recipients whose personal languages differ all receive the same row. A per-user
172+
locale is deferred until measured pull (maintainer ruling, 2026-08-13); it
173+
layers in as an override at that same seam when it lands.
168174
- Inline `title` / `message` are the **non-localizable** path: raw strings sent
169175
to every recipient verbatim. The two paths cannot be combined on one node —
170176
the schema refuses the ambiguous shape rather than letting a runtime

packages/services/service-automation/src/builtin/notify-node.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,31 @@ describe('notify (baseline node)', () => {
7676
);
7777
});
7878

79+
it('describes `template` with the locale the delivery path actually resolves, not a per-recipient one', () => {
80+
// This description is rendered in the Studio form an author fills in, so
81+
// it is the shortest path from wording to an authoring mistake. It used
82+
// to say the row is "resolved by (name, recipient locale) ... and
83+
// rendered per recipient", which reads as "each recipient's own language
84+
// selects the row". It does not: `payload.locale` is interpolated once
85+
// BEFORE fan-out and the fallback is the deployment default, so one
86+
// locale serves the whole notification (`sys_user` carries no locale
87+
// column; a per-user locale is deferred by the 2026-08-13 ruling).
88+
// Acting on the old promise is a net regression — TEMPLATE_* failures
89+
// classify `permanent` and dead-letter — so the qualification is pinned
90+
// here, and a bare "recipient locale" is refused, to keep a later edit
91+
// from quietly restoring it.
92+
const engine = new AutomationEngine(createTestLogger());
93+
registerNotifyNode(engine, createCtx());
94+
const schema = engine.getActionDescriptor('notify')?.configSchema as
95+
| { properties?: { template?: { description?: string } } }
96+
| undefined;
97+
const description = schema?.properties?.template?.description ?? '';
98+
expect(description).toMatch(/not one per recipient/);
99+
expect(description).toMatch(/payload\.locale/);
100+
expect(description).toMatch(/deployment default/);
101+
expect(description).not.toMatch(/recipient locale/);
102+
});
103+
79104
describe('with a messaging service registered', () => {
80105
let engine: AutomationEngine;
81106
let messaging: ReturnType<typeof fakeMessaging>;
@@ -229,7 +254,8 @@ describe('notify (baseline node)', () => {
229254
const payload = messaging.emitted[0].payload;
230255
// The reference rides RAW (a static metadata cross-reference); its
231256
// render context is interpolated per run — that pair is what the
232-
// email channel resolves per recipient locale at delivery time.
257+
// email channel resolves at delivery time, against one locale for the
258+
// whole notification (`payload.locale`, else the deployment default).
233259
expect(payload.template).toBe('crm.large_deal_won');
234260
expect(payload.templateData).toEqual({ dealName: 'Acme', dealUrl: '/opps/42' });
235261
// No inline content keys on this path: a channel without template

packages/services/service-automation/src/builtin/notify-node.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -194,7 +194,7 @@ export function registerNotifyNode(engine: AutomationEngine, ctx: PluginContext)
194194
// how requiredness is owned there rather than by the form.
195195
template: {
196196
type: 'string',
197-
description: 'Email template name (sys_email_template.name) — resolved by (name, recipient locale) at delivery time and rendered per recipient. Mutually exclusive with inline title/message.',
197+
description: 'Email template name (sys_email_template.name) — the localizable content path: resolved by (name, locale) at delivery time, rendering subject/body from that row. The locale is ONE value for the whole notification, not one per recipient: payload.locale if the producer set one, else the deployment default (II18nService.getDefaultLocale()). The platform has no per-user locale, so recipients whose personal languages differ all receive the same row (deferred by the 2026-08-13 ruling; it layers in as an override when it lands). Mutually exclusive with inline title/message.',
198198
},
199199
templateData: {
200200
type: 'object',
@@ -285,9 +285,11 @@ export function registerNotifyNode(engine: AutomationEngine, ctx: PluginContext)
285285
const actorId = toStr(interpolate(cfg.actorId, variables, context));
286286

287287
// With a `template` reference the content lives in the template
288-
// bundle, resolved per recipient locale at delivery — no inline
289-
// title to demand (the Zod contract already refused a node carrying
290-
// NEITHER, and one carrying BOTH).
288+
// bundle, resolved by `(name, locale)` at delivery — one locale for
289+
// the whole notification (`payload.locale`, else the deployment
290+
// default), never one per recipient — so there is no inline title to
291+
// demand (the Zod contract already refused a node carrying NEITHER,
292+
// and one carrying BOTH).
291293
if (!title && !template) return { success: false, error: 'notify: title is required' };
292294
if (recipients.length === 0) {
293295
// Name the templates that came up empty (framework#3582). The
@@ -374,8 +376,11 @@ export function registerNotifyNode(engine: AutomationEngine, ctx: PluginContext)
374376
// Content rides in the payload per path (#9205): the inline
375377
// strings, or the template reference + its render context —
376378
// which the outbox snapshots onto each delivery row, so the
377-
// per-recipient-locale resolution happens at delivery time
378-
// in the channel (email-channel.ts reads payload.template).
379+
// template resolution happens at delivery time in the channel
380+
// (email-channel.ts reads payload.template). The locale it
381+
// resolves with is one value for the whole notification —
382+
// `payload.locale`, interpolated once BEFORE fan-out, else the
383+
// deployment default — never each recipient's own language.
379384
// On the template path no inline title/body keys are set:
380385
// channels without template support fall back to the topic,
381386
// which is the honest degraded rendering, not ''.

packages/services/service-messaging/src/messaging-service-plugin.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export class MessagingServicePlugin implements Plugin {
247247
ctx.hook('kernel:ready', async () => {
248248
if (getEmail()) {
249249
service.registerChannel(createEmailChannel({ getEmail, getData, store: templateStore, getDefaultTemplateLocale }));
250-
ctx.logger.info('[messaging] email channel registered (renders sys_notification_template; notify `template` refs resolve sys_email_template per recipient locale)');
250+
ctx.logger.info('[messaging] email channel registered (renders sys_notification_template; notify `template` refs resolve sys_email_template by (name, locale) — one locale per notification: payload.locale, else the deployment default)');
251251
}
252252
});
253253

0 commit comments

Comments
 (0)