From 6d810e0865a1dc44084eab7b5003e7051d3c92c5 Mon Sep 17 00:00:00 2001 From: delchev Date: Sat, 15 Aug 2026 15:35:35 +0300 Subject: [PATCH] docs: notify link placeholders - {recordUrl}, {inboxUrl}, {appUrl} A notification that cannot be acted on gets ignored, and the notify block had no documented way to carry the way back into the application - so an author's only option was pasting a route into the body, encoding a layout the intent does not own. Mirror the specification's new subsection: the three-placeholder table, the reserved-name rule, "an intent never spells a route", why the path belongs to whatever renders the application, and the fan-out scoping. Adds the reference index row. --- docs/reference.md | 1 + docs/spec/glue.md | 25 +++++++++++++++++++++++++ 2 files changed, 26 insertions(+) diff --git a/docs/reference.md b/docs/reference.md index 8d32a49..b1b6da5 100644 --- a/docs/reference.md +++ b/docs/reference.md @@ -49,6 +49,7 @@ The quick lookup surface: one line and a minimal snippet per construct. For rule | [`inbound`](/spec/glue#inbound-webhooks) | a webhook that creates records | | [the event axis](/spec/glue#the-event-axis-lifecycle-and-process-step-events) | what a reacting glue entry binds to: an entity lifecycle event, or a process step reached / completed | | [`notifications`](/spec/glue#notifications) | email on an event of the axis | +| [notify link placeholders](/spec/glue#links-back-to-the-application-recordurl-inboxurl-appurl) | `{recordUrl}` / `{inboxUrl}` / `{appUrl}` - a message that carries the way back into the application | | [`notify.forEach`](/spec/glue#one-message-per-related-row-foreach) | fan the block out over a related collection: one message per row | | [the notify block / `attach: print`](/spec/glue#the-notify-block-and-attach-print) | send a message about a record - with the record's own document attached - from a process step, a transition or a schedule | | [`schedules`](/spec/glue#schedules) | cron: notify or generate records per matching row | diff --git a/docs/spec/glue.md b/docs/spec/glue.md index 7894a52..aa5af0a 100644 --- a/docs/spec/glue.md +++ b/docs/spec/glue.md @@ -110,6 +110,31 @@ The **render language**: `language:` fixes the print-template language; `languag A recipient that resolves to no address is a **no-op** — recorded and skipped, so a record with nobody to notify never stalls a flow. A `transitions[].notify` can never fail its transition: the status flip is the contract and is already applied when the message is attempted, so a delivery failure is recorded and the transition still succeeds. A sending process step, whose whole purpose *is* the message, fails instead — so the platform's own retry applies. ::: +### 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}" +``` + +All three names are reserved at every notify call site, so an entity field of the same name never shadows them. `{recordUrl}` and `{inboxUrl}` are resolved to a complete address — **an intent never spells a route**. `{appUrl}` yields the origin alone; reach for it only for an address the other two cannot express, and append the rest as authored text. + +::: info Why the intent never writes the path +The routes belong to whatever renders the application. 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. +::: + +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. + ### 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. `forEach:` names a related entity and the block sends one message per row of it; every path (recipient, placeholders, `attach`) then resolves against the **row**.