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
spec(automation): notify template locale is the deployment default, not per-recipient (#12447)
`NotifyConfigSchema`'s doc block, the `template` field JSDoc/`.describe()`, and
both `superRefine` refusal messages said the delivery path resolves
`(name, recipient locale)` "per recipient" and "renders subject/body per
recipient". The delivery path deliberately does not: `sys_user` carries no
locale column and request-scoped locale does not exist at async delivery time,
so the locale is `payload.locale` (interpolated once, before fan-out) or the
deployment default `II18nService.getDefaultLocale()` — one value for the whole
notification. `service-messaging/src/email-channel.ts` already documents this
honestly; spec was the one place it was unqualified.
Per the maintainer ruling of 2026-08-13 the behaviour is settled (no per-user
locale until measured pull), so the prose moves. All five sites in the file now
name the resolved value and date the deferral. The two test pins that asserted
the old `/recipient locale/` string now assert the qualification and refuse a
bare "recipient locale". Text only — no acceptance, refusal or delivery
behaviour changes.
Claude-Session: https://claude.ai/code/session_01E5LFCYBJ3q2s6yW6oMLxwy
Co-authored-by: Claude <noreply@anthropic.com>
Copy file name to clipboardExpand all lines: content/docs/references/automation/io-node-config.mdx
+1-1Lines changed: 1 addition & 1 deletion
Display the source diff
Display the rich diff
Original file line number
Diff line number
Diff line change
@@ -98,7 +98,7 @@ const result = HttpConfigSchema.parse(data);
98
98
|**recipients**|`string \| string[]`| ✅ | Recipient user id(s) / audience selector(s); `{token}` templates resolve per run |
99
99
|**title**|`string`| optional | Notification title, sent to every recipient verbatim (not localizable — use `template` for per-locale content). Either this or `template` is required; the two are mutually exclusive. |
100
100
|**message**|`string`| optional | Notification body, sent verbatim like `title` (not localizable). Only valid with inline `title`, never with `template`. |
101
-
|**template**|`string`| optional | Email template name (`sys_email_template.name`, e.g. `crm.large_deal_won`) — the localizable content path: the delivery path resolves `(name, recipient locale)` at delivery time and renders subject/body per recipient. Mutually exclusive with inline `title`/`message`, which are the non-localizable path. Read raw — no `{token}` interpolation. |
101
+
|**template**|`string`| optional | Email template name (`sys_email_template.name`, e.g. `crm.large_deal_won`) — the localizable content path: the delivery path resolves `(name, locale)`against sys_email_template at delivery time and renders 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 with different personal languages 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`, which are the non-localizable path. Read raw — no `{token}` interpolation. |
102
102
|**templateData**|`Record<string, any>`| optional | Render context for the referenced template's `{{var}}` placeholders; values interpolate `{token}` templates per run. Only valid together with `template`. |
103
103
|**channels**|`string \| string[]`| optional | Channels to fan out to (default: inbox) |
.describe('Notification body, sent verbatim like `title` (not localizable). Only valid with inline `title`, never with `template`.'),
179
195
/**
180
196
* The localizable content path (#9205): name of a `sys_email_template`
181
-
* bundle. Resolved by `(name, recipient locale)` AT DELIVERY TIME —
182
-
* `IEmailService.sendTemplate({ template, locale })` picks the recipient
183
-
* locale's row with the documented en-US fallback ladder. Read RAW like
184
-
* `topic`/`channels`: a static metadata cross-reference, never interpolated.
185
-
* Mutually exclusive with inline `title`/`message`.
197
+
* bundle. Resolved by `(name, locale)` AT DELIVERY TIME —
198
+
* `IEmailService.sendTemplate({ template, locale })` picks that locale's row
199
+
* with the documented en-US fallback ladder.
200
+
*
201
+
* The `locale` is ONE value for the whole notification, NOT one per
202
+
* recipient: `payload.locale` when the producer set one (interpolated once,
203
+
* before fan-out), else the DEPLOYMENT DEFAULT —
204
+
* `II18nService.getDefaultLocale()`. There is no per-user locale to read
205
+
* (`sys_user` has no locale column); the 2026-08-13 ruling defers one until
206
+
* measured pull, and it layers in as an override when it lands.
207
+
*
208
+
* Read RAW like `topic`/`channels`: a static metadata cross-reference, never
209
+
* interpolated. Mutually exclusive with inline `title`/`message`.
186
210
*/
187
211
template: z.string().optional()
188
-
.describe('Email template name (`sys_email_template.name`, e.g. `crm.large_deal_won`) — the localizable content path: the delivery path resolves `(name, recipient locale)` at delivery time and renders subject/body per recipient. Mutually exclusive with inline `title`/`message`, which are the non-localizable path. Read raw — no `{token}` interpolation.'),
212
+
.describe('Email template name (`sys_email_template.name`, e.g. `crm.large_deal_won`) — the localizable content path: the delivery path resolves `(name, locale)` against sys_email_template at delivery time and renders 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 with different personal languages 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`, which are the non-localizable path. Read raw — no `{token}` interpolation.'),
189
213
/**
190
214
* Render context for the referenced template's `{{var}}` holes. Values are
191
215
* interpolated per run (`{record.x}` resolves), so flow state can feed the
0 commit comments