N NezamDocumentation

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#

bl
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.

PartShape
Namesubscribe VendorAudit
Targeton table Vendor
Operationafter insert, after update, or after delete
Dispatch modeOptional async after the operation
ParametersFunction-style typed parameters
BodyStandard block statements
bl
@audited
subscribe VendorTouched on table Vendor after update async() {
  enqueue_vendor_refresh();
}

Operations#

The grammar supports table events after insert, update, and delete.

bl
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.

bl
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.

bl
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.

bl
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.

Use caseSubscription fit
Audit log entriesGood fit after insert, update, or delete
Search index refreshGood fit, often asynchronous
Integration event publishingGood fit when payload/routing is configured
Required approvalsNot enough by itself; validate before save
Status transition policyNot 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.

Source: packages/business/language/lifecycle-subscriptions.md