diff --git a/versions/1.2.md b/versions/1.2.md index f321557..8cf5173 100644 --- a/versions/1.2.md +++ b/versions/1.2.md @@ -1116,7 +1116,7 @@ An optional **`attach: print`** makes the message carry the record's **own docum language: bg # optional print-template language ``` -> **Normative.** `attach`'s only value is `print`. A block declaring it MUST be about an entity with a **line-items child** — the shape a print template is generated for; a generator MUST reject `attach: print` on any other entity rather than send a message without the document it promised. `language` names one of the document's print-template languages; when omitted, the default language is used. The attachment MUST be produced from the record's own data through the same path the interactive print takes, so a document mailed and a document printed are the same document. +> **Normative.** `attach` is `print` - the record the block is about - or, inside a fan-out, [`recordPrint`](#one-document-many-recipients-attach-recordprint). A block declaring `print` MUST be about an entity with a **line-items child** — the shape a print template is generated for; a generator MUST reject `attach: print` on any other entity rather than send a message without the document it promised. `language` names one of the document's print-template languages; when omitted, the default language is used. The attachment MUST be produced from the record's own data through the same path the interactive print takes, so a document mailed and a document printed are the same document. > **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. @@ -1141,6 +1141,44 @@ resolves against the **row**. > generator MUST reject both rather than send to a silently wrong set of recipients. `attach: print` > then refers to the ROW's document, and the row MUST satisfy the document requirement above. +> **Normative.** `forEach` is authored on a `transitions[].notify` or a `serviceTask`'s `args.notify`. +> A `schedules[].notify` already runs once per matched row and a `notifications[]` entry is about the +> event record, so a `forEach` on either MUST be rejected rather than ignored - an accepted declaration +> that changes nothing sends a different message than the one that was written down. + +#### One document, many recipients: `attach: recordPrint` + +The mirror shape: the related rows are only the **recipient list** and the document belongs to the +record they hang off - a request for quotation mailed to each invited supplier, an agenda mailed to +each participant. `attach: print` cannot express it (it renders the row, which is nobody's document); +`attach: recordPrint` renders the fan-out's **anchor record** - the record the block is about - once, +for everybody. + +```yaml + notify: + forEach: InvitedSupplier # the rows: the recipient list + to: Supplier.email # the ROW's supplier - the rows ARE the recipients + subject: "RFQ {record.number}" # {record.} = the ANCHOR RECORD's field + body: "Dear {Supplier.name}, please quote by {record.deadline}." # bare = the ROW + attach: recordPrint # the RECORD's document, rendered once +``` + +> **Normative.** `attach: recordPrint` is only meaningful inside a fan-out and MUST be rejected without +> one - outside a fan-out `attach: print` already renders that very record. The **anchor record** MUST +> satisfy the document requirement; the row need not. `language` / `languageFrom` then select the +> anchor's render language, read off the anchor, because there is exactly one render: a conforming +> generator MUST render the document ONCE per fan-out and attach the same result to every message, +> never once per recipient. + +> **Normative.** Inside a fan-out a **bare** path - the recipient, `{field}`, `{Relation.field}` - +> resolves against the **ROW**, and the reserved prefix **`record.`** is the only way to address the +> anchor record: `{record.}` names ONE field of it, and a longer path MUST be rejected. The +> recipient MUST NOT be record-scoped: the rows are the recipients, so a record-scoped address would +> send the same message to the same address once per row. `record.` outside a fan-out MUST be rejected +> too, since there every bare path already resolves against the record. Which record a path reads is +> therefore always written down and never inferred - nothing in a rendered message reveals that the +> wrong one was read. + > **Normative.** A fan-out is **fail-soft per row at every call site**, including the ones that > otherwise fail: a row with no recipient is skipped, a delivery failure is recorded, and the activity > completes with a per-row summary. A fan-out MUST NOT fail its activity, because a retry would resend @@ -1643,7 +1681,8 @@ 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.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 | +| [`notify.forEach`](#one-message-per-related-row-foreach) | fan the block out over a related collection: one message per row, every bare path resolved against the row | +| [`attach: recordPrint`](#one-document-many-recipients-attach-recordprint) | in a fan-out: attach the ANCHOR record's document, rendered once, to every recipient's message (`{record.}` addresses that record) | | [`schedules`](#schedules) | cron: notify or generate records per matching row | | [`integrations`](#integrations--outbound-http) | outbound HTTP on a data change | | [`inbound`](#inbound--webhooks) | a webhook that creates records | @@ -1673,9 +1712,3 @@ The following are parsed (or reserved) but not yet materialised by a generator; - **Cross-model status names and stage scopes** — a nomenclature owned by another model is seeded there, so its stages and names cannot be resolved from the referencing file; such references are rejected with the numeric-id fallback named. - Event-driven **document** generation (produce a whole document, rather than the mapped rows of [`posts`](#posts--derived-rows-on-an-event), on an event), a declarative state machine, and shadow audit-history entities (audit *columns* via `audit: true` ship today). - Arbitrary resolver-path task assignment beyond `assignee: personal`. -- **A fan-out that attaches the RECORD's document rather than the row's.** `forEach` + `attach: print` - attaches each row's own document, which is what a per-row document (a payslip) needs. The mirror - shape - one document sent to many recipients, where the rows ARE the recipients and the attachment - belongs to the record they hang off (a request for quotation mailed to each invited supplier) - has - no syntax yet; a conforming generator therefore rejects `attach: print` when the row entity is not - itself a document, rather than guessing which record was meant.