From 545daed5065e703ab57c74d0a82752114c7944b2 Mon Sep 17 00:00:00 2001 From: delchev Date: Sat, 15 Aug 2026 15:35:11 +0300 Subject: [PATCH] spec: notify link placeholders - {recordUrl}, {inboxUrl}, {appUrl} A notification that cannot be acted on gets ignored, and the notify block had no way to carry the way back into the application: an author could only paste a route into the body - the layout the intent does not own, correct until whatever renders the application changes it. Add the three reserved link placeholders to the notify block: {recordUrl} (the record the message is about), {inboxUrl} (the recipient's task inbox) and {appUrl} (the origin alone). Normative: the names are reserved at every call site and MUST NOT be shadowed by an entity field; the first two MUST be resolved by the implementation to a complete address, and an intent MUST NOT be required to spell a route. Inside a forEach fan-out {recordUrl} links the ROW, like every other bare path in the block. Appendix A gains its index row. --- versions/1.2.md | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/versions/1.2.md b/versions/1.2.md index f321557..b858cb3 100644 --- a/versions/1.2.md +++ b/versions/1.2.md @@ -1120,6 +1120,40 @@ An optional **`attach: print`** makes the message carry the record's **own docum > **Normative.** A block whose recipient resolves to no address is a **no-op**: the send is skipped and recorded, never an error — a record with nobody to notify must not stall a flow. A `transitions[].notify` MUST NOT be able to fail its transition: the status flip is the transition's contract and is already applied when the message is attempted, so a delivery failure is recorded and the transition still reports success. At the other call sites a delivery failure MAY fail the activity so the platform's own retry applies; a sending process step SHOULD, since the message is that step's whole purpose. +#### Links back to the application: `{recordUrl}`, `{inboxUrl}`, `{appUrl}` + +A notification that cannot be acted on is a notification that gets ignored. "You have an approval +waiting" is only useful if it carries the way back to the record, so `subject` and `body` accept three +reserved link placeholders alongside the field ones: + +| Placeholder | Resolves to | +| --- | --- | +| `{recordUrl}` | the record the message is about, opened in the application | +| `{inboxUrl}` | the recipient's task inbox | +| `{appUrl}` | the application's external base URL - the origin only | + +```yaml + notify: + to: Approver.email + subject: "Approval needed: invoice {number}" + body: "Open it here: {recordUrl}\nEverything waiting on you: {inboxUrl}" +``` + +> **Normative.** The three names are RESERVED at every notify call site: an entity field of the same +> name MUST NOT shadow them. `{recordUrl}` and `{inboxUrl}` MUST be resolved by the implementation to +> a complete address; an intent MUST NOT be required to spell a route, and a conforming implementation +> MUST NOT require one. `{appUrl}` yields the origin alone - it is the escape hatch for addresses the +> other two cannot express, and everything appended to it is authored text. + +Why the intent never writes the path: the routes belong to whatever renders the application, and an +intent that named one would encode a layout it does not own - correct only until that layout changes, +and silently wrong afterwards. `{recordUrl}` states the destination; the implementation states the +address. + +> **Normative.** Inside a [`forEach`](#one-message-per-related-row-foreach) fan-out `{recordUrl}` links +> the ROW, like every other bare path in the block - the row is what that message is about. The anchor +> record is addressable for VALUES only. + #### One message per related row: `forEach` Some sends are per-row rather than per-record: a payroll run mails every payslip to its own employee, @@ -1643,6 +1677,7 @@ One line per construct, linking into the chapters above. | [`widgets`](#widgets--custom-dashboard-tiles) | custom KPI / embedded-page dashboard tiles | | [`notifications`](#notifications) | email on create / update / delete | | [the notify block / `attach: print`](#the-notify-block--and-attach-print-sending-the-document-itself) | send a message about a record - with the record's own document attached - from a process step, a transition or a schedule | +| [notify link placeholders](#links-back-to-the-application-recordurl-inboxurl-appurl) | `{recordUrl}` / `{inboxUrl}` / `{appUrl}` - a message that carries the way back into the application | | [`notify.forEach`](#one-message-per-related-row-foreach) | fan the block out over a related collection: one message per row, every path resolved against the row | | [`schedules`](#schedules) | cron: notify or generate records per matching row | | [`integrations`](#integrations--outbound-http) | outbound HTTP on a data change |