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
49 changes: 41 additions & 8 deletions versions/1.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand All @@ -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.<field>} = 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.<field>}` 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
Expand Down Expand Up @@ -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.<field>}` 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 |
Expand Down Expand Up @@ -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.