|
1 | 1 | --- |
2 | 2 | title: Declarative glue |
3 | 3 | description: notifications, schedules, integrations, inbound arrivals (webhook, message, file), roll-ups, keyed aggregates, settlements, expansions, generates, transitions, postings and event-driven row posting - declared in the intent, generated as integration code, never hand-written. |
| 4 | +description: notifications, schedules, integrations, inbound webhooks, outbound departures, roll-ups, keyed aggregates, settlements, expansions, generates, transitions, postings and event-driven row posting - declared in the intent, generated as integration code, never hand-written. |
4 | 5 | --- |
5 | 6 |
|
6 | 7 | # Declarative glue |
@@ -346,6 +347,48 @@ That is why a `folder` source requires its `cron` (and why a `cron` is an error |
346 | 347 |
|
347 | 348 | Conversation-shaped transports — acknowledgements, retries with backoff, certificates — stay [beyond the boundary](/spec/#the-scope-boundary): they have state and failure semantics no one-line declaration should pretend to carry. |
348 | 349 |
|
| 350 | +## outbound — departures on a queue or a topic |
| 351 | + |
| 352 | +The mirror of `inbound`: the application **raises a business event** for something outside it, on a message channel rather than over HTTP. |
| 353 | + |
| 354 | +```yaml |
| 355 | +outbound: |
| 356 | + # the record's own representation, on a queue — one consumer takes each message |
| 357 | + - { name: publishOrder, event: { onCreate: Order }, to: { queue: "orders.outbound" } } |
| 358 | +
|
| 359 | + # a declared envelope, on a topic — every subscriber receives it |
| 360 | + - name: announceActivation |
| 361 | + event: { onUpdate: Order, when: "channel != internal" } |
| 362 | + to: { topic: "order-activations" } |
| 363 | + payload: |
| 364 | + type: "order.activated" |
| 365 | + version: 1 |
| 366 | + messageId: "{uuid}" |
| 367 | + tenantId: "{tenant}" |
| 368 | + reference: number |
| 369 | + customer: customer.name |
| 370 | +``` |
| 371 | + |
| 372 | +`to:` names the channel; `payload:` is the same [declared envelope](#payload-the-declared-envelope) an integration sends, and without it the body is the record as stored — exactly what an integration forwards today. |
| 373 | + |
| 374 | +Use `integrations` when you are calling another system's API and want its answer; use `outbound` when you are announcing that something happened and nobody answers. The two are separate constructs rather than one with a transport switch precisely because their failure semantics differ: a failed call is a failed call, a failed announcement is a missed announcement. |
| 375 | + |
| 376 | +::: info Normative |
| 377 | +An entry MUST declare **exactly one** of `queue` / `topic`. Two channels are two departures wearing one name; none is a promise with nowhere to land — both MUST be reported as authoring errors, mirroring the arrival rule. |
| 378 | + |
| 379 | +A departure binds to the same event axis as every other reacting block, and takes the same `when:` guard. |
| 380 | + |
| 381 | +The message MUST be published **after** the write that raised the event is persisted, and MUST NOT be transactional with it: a failure MUST be recorded and MUST NOT fail that write. |
| 382 | + |
| 383 | +Ordering, exactly-once delivery and an outbox are NOT promised. A conforming implementation MUST state this rather than leave an author to assume otherwise — an author who believes there is an outbox writes a different application than one who knows there is none. |
| 384 | + |
| 385 | +An implementation MUST NOT add fields to a declared payload that the author did not name: the point of declaring the envelope is that adding a column does not change what leaves. |
| 386 | +::: |
| 387 | + |
| 388 | +A destination name in the model is a name the *application* owns. Whether two separate deployments sharing one broker can meet on it is a property of the implementation's isolation between tenants, not of the format — so an implementation that renames destinations per tenant MUST document how an author declares a destination that **is a contract with someone else**, or a departure quietly means less than it reads. |
| 389 | + |
| 390 | +Conversation-shaped transports — acknowledgement protocols, request-reply correlation, backoff policy — stay [beyond the boundary](/spec/#the-scope-boundary), as they are for an integration. |
| 391 | + |
349 | 392 | ## rollups — denormalised parent totals |
350 | 393 |
|
351 | 394 | ```yaml |
|
0 commit comments