Skip to content

Commit d1d06b7

Browse files
authored
Merge pull request #26 from IntentFile/docs/declared-payload
docs(spec): a declared payload for an outward-facing message
2 parents 4b50df7 + 2fe8fd8 commit d1d06b7

2 files changed

Lines changed: 51 additions & 0 deletions

File tree

‎docs/reference.md‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ The quick lookup surface: one line and a minimal snippet per construct. For rule
4242
| [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 |
4343
| [`schedules`](/spec/glue#schedules) | cron: notify or generate records per matching row |
4444
| [`integrations`](/spec/glue#integrations-outbound-http) | outbound HTTP on a data change |
45+
| [`integrations.payload`](/spec/glue#payload-the-declared-envelope) | the declared envelope a message carries, instead of the record as stored |
4546
| [`inbound`](/spec/glue#inbound-webhooks) | a webhook that creates records |
4647
| [`rollups`](/spec/glue#rollups-denormalised-parent-totals) | counts, sums, balance + status maintenance |
4748
| [`settlements`](/spec/glue#settlements-payment-allocation) | auto-allocation of payments across open invoices |

‎docs/spec/glue.md‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,56 @@ integrations:
206206

207207
The `@config:KEY` sugar resolves to a configuration lookup, so endpoints and secrets stay out of the source.
208208

209+
### payload — the declared envelope
210+
211+
Without a `payload`, the request body is the record as stored. That is only right when the receiver accepts the entity, and it has a cost even then: every column becomes part of a public contract, so adding a field silently changes what the outside world receives. A real integration contract is usually an *envelope* — a type, a version, an idempotency key, a timestamp, an identifier of the sender — which no arrangement of entity columns can produce.
212+
213+
`payload` declares that envelope, key by key:
214+
215+
```yaml
216+
integrations:
217+
- name: requestUserAssignment
218+
event: { onCreate: UserInvitation }
219+
method: POST
220+
url: "@config:ASSIGNMENT_URL"
221+
payload:
222+
type: "user.assignment.requested" # literal
223+
version: 1
224+
messageId: "{uuid}" # minted per message
225+
tenantId: "{tenant}" # execution context
226+
appId: "@config:APP_ID" # configuration
227+
email: email # a field of the record
228+
role: role.name # one hop off a to-one relation
229+
requestedAt: "{now}"
230+
```
231+
232+
The value forms are the ones [`notify`](#notifications) already resolves, deliberately borrowed rather than invented: a **literal**, a **direct field**, or a **one-hop `relation.field`** of a to-one relation, which the generated sender reads from the related record it loads once. `@config:KEY` reads the configuration, as it does in `url`.
233+
234+
The **context tokens** are a closed set of four:
235+
236+
| token | value |
237+
|---|---|
238+
| `{uuid}` | a fresh identifier, minted per message — the idempotency key a receiver deduplicates on |
239+
| `{now}` | the send time, as an ISO-8601 instant |
240+
| `{tenant}` | the tenant the send runs for |
241+
| `{user}` | the user behind the change that raised the event |
242+
243+
::: info Normative
244+
A `payload` value MUST be one whole value in one of the declared forms. Interpolated text (`"Order {id} placed"`), a nested object and a list are NOT payload values and MUST be reported as authoring errors — a payload is a contract, not a template.
245+
246+
A path MUST resolve at most one hop; `a.b.c` MUST be rejected.
247+
248+
An unknown context token MUST be an authoring error, never an empty value in a sent message.
249+
250+
A `payload` MUST be rejected on a method that carries no request body.
251+
252+
Keys MUST be sent in the order they were declared.
253+
254+
A bare word that names no field and no to-one relation of the record is a **literal** — the only way to carry a one-word constant. A value braced as `"{name}"` is a reference and MUST resolve.
255+
:::
256+
257+
Three value forms and four tokens is the cap, and the cap is the point: it expresses a frozen contract without the construct becoming a transformation language. A payload that needs more than this is an algorithm, and belongs in a hand-written handler — the honest hand-off.
258+
209259
## inbound — webhooks
210260

211261
Another system tells us — a webhook that ingests a JSON payload into an entity.

0 commit comments

Comments
 (0)