Lifecycle subscriptions#
Subscriptions attach executable handlers to table lifecycle events. Use them for side effects that follow a persisted change, such as audit records, integration events, search indexing, or cache invalidation.
Declaration shape#
subscribe VendorAudit on table Vendor after insert(event: Vendor) {
write_audit_event("vendor.inserted", event.vendor_id);
}A subscription declaration has a name, a target table type, one lifecycle operation, an optional dispatch mode, an optional parameter list, and a block body. The parameter parentheses are required. The parameter list inside them is optional. The table target is a type expression.
Subscriptions may have annotations, but they do not take access modifiers. If present, async appears after the operation and before the required parameter parentheses.
| Part | Shape |
|---|---|
| Name | subscribe VendorAudit |
| Target | on table Vendor |
| Operation | after insert, after update, or after delete |
| Dispatch mode | Optional async after the operation |
| Parameters | Function-style typed parameters |
| Body | Standard block statements |
@audited
subscribe VendorTouched on table Vendor after update async() {
enqueue_vendor_refresh();
}Operations#
The grammar supports table events after insert, update, and delete.
subscribe VendorCreated on table Vendor after insert(event: Vendor) {
publish_vendor_event(event.vendor_id, "created");
}
subscribe VendorChanged on table Vendor after update(event: Vendor) {
publish_vendor_event(event.vendor_id, "changed");
}
subscribe VendorDeleted on table Vendor after delete(event: Vendor) {
publish_vendor_event(event.vendor_id, "deleted");
}Use subscriptions for reactions to lifecycle events, not for deciding whether a change is allowed. Put required preconditions in validation, command logic, rules, or configuration-driven policy resolution before saving the record.
Asynchronous dispatch#
Add async when the handler should run as asynchronous dispatch.
subscribe VendorSearchIndex on table Vendor after update async(event: Vendor) {
enqueue_search_index_update(event.vendor_id);
}An asynchronous subscription should still fail closed at its own boundary. If it needs tenant-specific routing, destination names, retry policy, or event filtering, resolve that configuration explicitly from tables.
Parameters#
Subscription parameters use the same typed parameter shape as functions.
subscribe InvoicePosted on table Invoice after update(invoice: Invoice) {
notify_invoice_posted(invoice.invoice_id, invoice.company_code);
}Keep parameter names specific to the event payload. Avoid relying on implicit globals when the handler can receive the changed record directly.
Policy boundary#
Subscriptions are a good place for operational side effects. They are not a replacement for policy checks.
public function post_invoice(invoice_id_param: uuid): Invoice {
let invoice = load_invoice(invoice_id_param);
let policy = resolve_posting_policy(invoice.company_code, invoice.document_type);
if (policy is null) {
raise message posting_policy_required(invoice.company_code, invoice.document_type);
}
invoice.status = "posted";
save invoice;
return invoice;
}In this model, post_invoice resolves policy before save invoice. A subscription can then publish or index the result after the update has happened.
Recommended uses#
| Use case | Subscription fit |
|---|---|
| Audit log entries | Good fit after insert, update, or delete |
| Search index refresh | Good fit, often asynchronous |
| Integration event publishing | Good fit when payload/routing is configured |
| Required approvals | Not enough by itself; validate before save |
| Status transition policy | Not enough by itself; resolve policy before save |
Subscriptions should not hide missing configuration. Missing routing, destination, retry, template, or policy configuration should raise a declared message-backed failure in the handler or in the command that schedules the effect.