Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 35 additions & 0 deletions versions/1.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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 |
Expand Down