--- name: skill-events description: "AL event-driven architecture for Business Central. Use when creating EventSubscribers, IntegrationEvents, BusinessEvents, or implementing publisher/subscriber patterns in extensions." --- # Skill: AL Event-Driven Architecture ## Purpose Discover, design, and implement event-driven patterns in AL Business Central extensions: event discovery via Event Recorder, subscriber/publisher creation, IsHandled patterns, and extensibility design. ## When to Load This skill should be loaded when: - Extending standard BC functionality (posting, validation, master data changes) - Designing custom integration events for a new codeunit or extension - A subscriber is not firing and needs investigation - An architecture requires extensibility points for future consumers - Implementing document posting interventions, workflow triggers, or UI customizations ## Core Patterns ### Pattern 1: Event Discovery with Event Recorder Before writing any subscriber, discover which events are raised during the business process: ``` 1. Open the Event Recorder in VS Code ← VS Code command (not an agent tool) 2. Start recording 3. Execute the business process in BC (e.g., post a sales order) 4. Stop recording 5. Analyze the event list — filter by object or keyword 6. Identify the OnBefore/OnAfter event closest to your extension point ``` Use `al_search_objects` and `al_get_object_definition` to inspect publisher signatures. Use `al_find_references` to see who else subscribes to the same event. ### Pattern 2: Event Subscriber ```al // Subscriber codeunit — name with "Handler" suffix codeunit 50100 "Sales Document Events Handler" { // Attribute breakdown: // ObjectType — Table, Codeunit, Page, Report, etc. // ObjectId — Database::"Sales Header" or Codeunit::"Customer Management" // EventName — exact name of the publisher procedure (e.g., OnBeforeInsert) // ElementName — '' for table/codeunit events; field name for page field triggers // SkipOnMissingLicense — false (recommended: always run) // SkipOnMissingPermission — false (recommended: always run) [EventSubscriber(ObjectType::Table, Database::"Sales Header", OnBeforeInsert, '', false, false)] local procedure OnBeforeInsertSalesHeader( var SalesHeader: Record "Sales Header"; RunTrigger: Boolean) begin ValidateCustomFields(SalesHeader); end; } ``` **Subscriber rules:** - Procedure MUST be `local` - Parameter signature MUST exactly match the publisher (name, type, order) - One subscriber per event per codeunit (keep handler codeunits focused) - Never use `Commit` inside a subscriber unless absolutely necessary ### Pattern 3: Integration Event Publisher with IsHandled The standard extensibility pattern: `OnBefore` allows interception, `OnAfter` allows reaction. ```al codeunit 50101 "Customer Management" { procedure CreateCustomer(var Customer: Record Customer): Boolean var IsHandled: Boolean; begin // 1. Raise OnBefore — subscribers can validate, modify, or skip OnBeforeCreateCustomer(Customer, IsHandled); if IsHandled then exit(true); // 2. Default logic if not Customer.Insert(true) then exit(false); // 3. Raise OnAfter — subscribers can react (logging, integration, etc.) OnAfterCreateCustomer(Customer); exit(true); end; [IntegrationEvent(false, false)] local procedure OnBeforeCreateCustomer( var Customer: Record Customer; var IsHandled: Boolean) begin // Empty body — subscribers provide the implementation // IsHandled = true → skip default logic end; [IntegrationEvent(false, false)] local procedure OnAfterCreateCustomer(var Customer: Record Customer) begin // Empty body — subscribers react after creation end; } ``` **IntegrationEvent attribute:** - `IntegrationEvent(IncludeSender, GlobalVarAccess)` - `IncludeSender = false` — recommended for clean API (no coupling to publisher internals) - `GlobalVarAccess = false` — recommended (prevent subscribers from accessing publisher global vars) ### Pattern 4: Business Event Business events are user-disablable — use for optional, non-critical functionality: ```al [BusinessEvent(IncludeSender)] local procedure OnCustomerCreditLimitExceeded( Customer: Record Customer; RequestedAmount: Decimal) begin // Subscribers may send notification, log, or block — user can disable end; ``` | Attribute | Cannot be disabled | Use for | |---|---|---| | `IntegrationEvent` | Correct | Critical business logic, posting interventions | | `BusinessEvent` | Can be disabled by user | Optional features, external integrations, notifications | ### Pattern 5: Well-Designed Event Parameters ```al // ✅ Good — rich context, var where modification expected, IsHandled for OnBefore [IntegrationEvent(false, false)] local procedure OnBeforePostDocument( var DocumentHeader: Record "Sales Header"; // var — subscriber may modify var DocumentLines: Record "Sales Line"; // var — subscriber may modify PostingDate: Date; // value — read-only context var IsHandled: Boolean) // var — subscriber may skip default begin end; // ✅ Good — results context for OnAfter [IntegrationEvent(false, false)] local procedure OnAfterPostDocument( DocumentHeader: Record "Sales Header"; // value — read-only (already posted) PostedDocumentNo: Code[20]; // result reference PostingResult: Boolean) // success/failure begin end; ``` **Parameter design rules:** - `var Record` — when subscribers should be able to modify the record - Non-var `Record` — when record is read-only context - Always include `var IsHandled: Boolean` in `OnBefore` events - Include result/outcome parameters in `OnAfter` events - Use meaningful parameter names that describe business intent ## Workflow ### Step 1: Discover Events 1. Open the Event Recorder in VS Code (a VS Code command, not an agent tool) 2. Execute the target business process in BC 3. Review recorded events — filter by relevant object 4. Use `al_get_object_definition` to inspect publisher signatures 5. Use `al_find_references` to check existing subscribers (avoid conflicts) ### Step 2: Design Event Architecture For **subscribing to existing events**: 1. Identify the right OnBefore / OnAfter event from Step 1 2. Match the parameter signature exactly 3. Create a handler codeunit with naming: `"{Domain} Events Handler"` For **creating new integration events** in your codeunit: 1. Identify logical extension points in your business logic 2. Add `OnBefore` event with `IsHandled` pattern before key operations 3. Add `OnAfter` event after key operations with result context 4. Follow naming: `OnBefore{Action}` / `OnAfter{Action}` 5. Choose `IntegrationEvent` (critical) or `BusinessEvent` (optional) ### Step 3: Implement Use the **AL: Insert Event Subscriber** VS Code command (not an agent tool) to scaffold subscriber/publisher structures, then fill the body: ```al // Subscriber handler codeunit codeunit 50102 "Customer Validation Handler" { [EventSubscriber(ObjectType::Codeunit, Codeunit::"Customer Management", OnBeforeCreateCustomer, '', false, false)] local procedure ValidateOnBeforeCreate( var Customer: Record Customer; var IsHandled: Boolean) begin ValidateCustomerCreditLimit(Customer); if ShouldSkipDefaultProcessing(Customer) then IsHandled := true; end; } ``` ### Step 4: Verify 1. Build: `al_build` — check for compilation errors (signature mismatches) 2. Set breakpoint inside subscriber 3. Debug: `al_debug` 4. Execute the business process — confirm subscriber fires 5. If subscriber does NOT fire, check: - Signature mismatch (most common cause) - Wrong `ObjectType` / `ObjectId` - Extension not published/active - Another subscriber set `IsHandled = true` before yours - SkipOnMissingLicense / SkipOnMissingPermission attributes ## Common Scenarios | Scenario | Event Source | Pattern | |---|---|---| | Validate before posting | `OnBeforePost*` on Codeunit "Sales-Post" | Subscriber + IsHandled | | Add fields to sales order | `OnAfterInsert` on Table "Sales Header" | Subscriber (set defaults) | | External integration trigger | `OnAfterPostDocument` (custom) | Publisher in your codeunit | | Custom approval workflow | `OnBeforeRelease*` | Subscriber + IsHandled to block | | UI field validation | Page trigger `OnAfterValidate` on field | Subscriber with ElementName | | Master data change audit | `OnAfterModify` on Table "Customer" | Subscriber (log changes) | ## References - [Event Recorder — Microsoft Docs](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-events-discoverability) - [Events in AL](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/devenv-events-in-al) - [IntegrationEvent Attribute](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/attributes/devenv-integrationevent-attribute) - [BusinessEvent Attribute](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/attributes/devenv-businessevent-attribute) - [EventSubscriber Attribute](https://learn.microsoft.com/en-us/dynamics365/business-central/dev-itpro/developer/attributes/devenv-eventsubscriber-attribute) ## Constraints - This skill covers **event discovery, design, and implementation patterns** — it does NOT duplicate the passive rules in `al-events.instructions.md` (auto-applied to all `.al` files) - Do NOT create publishers without an `OnBefore` + `OnAfter` pair at minimum - Do NOT use `Commit` inside event subscribers unless absolutely required - Do NOT set `IsHandled = true` without clear documentation of why default logic is skipped - Subscriber debugging (breakpoints, snapshots) → load `skill-debug.md` - Subscriber performance overhead analysis → load `skill-performance.md`