Add P0 integration and control add-in runtime guidance (#100)

* Add P0 integration and control add-in guidance

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 02baffe8-0600-430d-81fa-a9993685e7cb

* Correct API part multiplicity guidance

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 02baffe8-0600-430d-81fa-a9993685e7cb

* Refine API part multiplicity guidance

Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>

Copilot-Session: 1c37924e-9749-4e63-9d58-bd73d659f736

---------

Co-authored-by: Jesper Schulz-Wedde <jesper.schulzwedde@microsoft.com>
This commit is contained in:
Jesper Schulz-Wedde 2026-07-14 12:53:26 +02:00 committed by GitHub
parent e0ebdd35c7
commit 0bb1065bc3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
16 changed files with 526 additions and 12 deletions

View file

@ -0,0 +1,3 @@
function loadPackagedTemplate(url) {
return $.get(url).done(renderTemplate);
}

View file

@ -0,0 +1,8 @@
function loadPackagedTemplate(url) {
return $.ajax({
url: url,
xhrFields: {
withCredentials: true
}
}).done(renderTemplate);
}

View file

@ -0,0 +1,30 @@
---
bc-version: [all]
domain: ui
keywords: [control-add-in, packaged-resource, ajax, withcredentials, xhrfields, jquery]
technologies: [javascript]
countries: [w1]
application-area: [all]
---
# Load packaged control add-in resources with credentialed AJAX
## Description
JavaScript in a Business Central control add-in can load a static resource from its extension package with AJAX, but the request needs the Business Central context and cookies. Set `xhrFields.withCredentials = true`; shorthand calls such as `$.get` omit that setting and can work during development yet fail in production.
## Best Practice
Use an AJAX form that explicitly enables `withCredentials` whenever a control add-in requests a packaged static resource. Keep this rule scoped to resources served from the add-in package; it is not generic advice to attach credentials to arbitrary external requests.
See sample: `control-addin-package-resource-ajax-needs-withcredentials.good.js`.
## Anti Pattern
Using `$.get(url)` or an `XMLHttpRequest` without `withCredentials = true` to retrieve package content. The request can lack the context and cookies required by the Business Central service.
See sample: `control-addin-package-resource-ajax-needs-withcredentials.bad.js`.
## Source
[Control add-in object: Loading static resources using AJAX requests](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-control-addin-object#loading-static-resources-using-ajax-requests).

View file

@ -0,0 +1,8 @@
function startSendingRows(rows) {
window.setInterval(() => {
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod(
"StoreRows",
[JSON.stringify(rows)],
false);
}, 100);
}

View file

@ -0,0 +1,74 @@
const pendingChunks = [];
let callInProgress = false;
let transferHalted = false;
function sendRows(rows, maxArgumentsBytes) {
if (transferHalted)
throw new Error("Retry or discard the failed chunk before sending more rows.");
const encoder = new TextEncoder();
const chunks = [];
let chunk = [];
const argumentBytes = (payload) =>
encoder.encode(JSON.stringify([payload])).length;
for (const row of rows) {
if (argumentBytes(JSON.stringify([row])) > maxArgumentsBytes)
throw new Error("A row exceeds the configured payload limit.");
const candidate = JSON.stringify([...chunk, row]);
if (argumentBytes(candidate) <= maxArgumentsBytes) {
chunk.push(row);
continue;
}
chunks.push(JSON.stringify(chunk));
chunk = [row];
}
if (chunk.length > 0)
chunks.push(JSON.stringify(chunk));
pendingChunks.push(...chunks);
sendNextChunk();
}
function sendNextChunk() {
if (callInProgress || pendingChunks.length === 0)
return;
callInProgress = true;
const payload = pendingChunks[0];
Microsoft.Dynamics.NAV.InvokeExtensibilityMethod(
"StoreRows",
[payload],
false,
() => {
pendingChunks.shift();
callInProgress = false;
sendNextChunk();
},
() => {
callInProgress = false;
transferHalted = true;
showTransferError();
});
}
function retryFailedChunk() {
if (!transferHalted)
return;
transferHalted = false;
sendNextChunk();
}
function discardFailedChunk() {
if (!transferHalted)
return;
pendingChunks.shift();
transferHalted = false;
sendNextChunk();
}

View file

@ -0,0 +1,30 @@
---
bc-version: [20..]
domain: ui
keywords: [control-add-in, invokeextensibilitymethod, success-callback, throttling, payload, reduced-functionality]
technologies: [javascript]
countries: [w1]
application-area: [all]
---
# Serialize control add-in AL calls and keep payloads small
## Description
`InvokeExtensibilityMethod` crosses from a control add-in into the Business Central service. Repeated calls that outpace AL execution fill the communication channel, trigger reduced-functionality warnings, and can be queued, throttled, or rejected; an oversized single payload can also be rejected immediately. The success and error callbacks exist so the add-in can bound this traffic.
## Best Practice
Send byte-bounded chunks and invoke the next AL event only from the previous call's completion callback. Handle the error callback and stop until the caller explicitly retries or discards the failed chunk. There is no universal safe threshold, so measure the serialized argument array, reserve transport headroom below the server's `ClientServicesMaxUploadSize`, and reject an individual item that exceeds the configured budget.
See sample: `control-addin-throttle-al-calls-and-payload-size.good.js`.
## Anti Pattern
Calling `InvokeExtensibilityMethod` on an interval without tracking completion, recursively creating intervals, or serializing an entire unbounded dataset into one call. These patterns can overwhelm the client-service channel or exceed the upload limit.
See sample: `control-addin-throttle-al-calls-and-payload-size.bad.js`.
## Source
[Control add-in performance best practices](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-control-addin-bestpractices), [InvokeExtensibilityMethod](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/methods/devenv-invokeextensibility-method), and [control add-in resiliency](https://learn.microsoft.com/dynamics365/business-central/across-controladdin-resiliency).

View file

@ -0,0 +1,80 @@
page 50353 "WS Order API Bad"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'order';
EntitySetName = 'orders';
ODataKeyFields = SystemId;
SourceTable = "Sales Header";
layout
{
area(content)
{
repeater(records)
{
part(lines; "WS Order Line API Bad")
{
EntityName = 'orderLine';
EntitySetName = 'orderLines';
Multiplicity = ZeroOrOne;
SubPageLink = "Order No." = Field("No.");
}
}
}
}
}
table 50353 "WS Order Line Bad"
{
fields
{
field(1; "Entry No."; Integer)
{
AutoIncrement = true;
}
field(2; "Order No."; Code[20])
{
TableRelation = "Sales Header"."No.";
}
}
keys
{
key(PK; "Entry No.")
{
Clustered = true;
}
}
}
page 50354 "WS Order Line API Bad"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'orderLine';
EntitySetName = 'orderLines';
ODataKeyFields = SystemId;
SourceTable = "WS Order Line Bad";
layout
{
area(content)
{
repeater(records)
{
field(id; Rec.SystemId)
{
Editable = false;
}
field(orderNumber; Rec."Order No.")
{
}
}
}
}
}

View file

@ -0,0 +1,151 @@
page 50350 "WS Order API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'order';
EntitySetName = 'orders';
ODataKeyFields = SystemId;
SourceTable = "Sales Header";
layout
{
area(content)
{
repeater(records)
{
field(id; Rec.SystemId)
{
Editable = false;
}
part(lines; "WS Order Line API")
{
EntityName = 'orderLine';
EntitySetName = 'orderLines';
SubPageLink = "Order Id" = Field(SystemId);
}
part(summary; "WS Order Summary API")
{
EntityName = 'orderSummary';
Multiplicity = ZeroOrOne;
SubPageLink = "Order Id" = Field(SystemId);
}
}
}
}
}
table 50350 "WS Order Line"
{
fields
{
field(1; "Entry No."; Integer)
{
AutoIncrement = true;
}
field(2; "Order Id"; Guid)
{
TableRelation = "Sales Header".SystemId;
}
field(3; Description; Text[100])
{
}
}
keys
{
key(PK; "Entry No.")
{
Clustered = true;
}
}
}
table 50351 "WS Order Summary"
{
fields
{
field(1; "Order Id"; Guid)
{
TableRelation = "Sales Header".SystemId;
}
field(2; Summary; Text[100])
{
}
}
keys
{
key(PK; "Order Id")
{
Clustered = true;
}
}
}
page 50351 "WS Order Line API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'orderLine';
EntitySetName = 'orderLines';
ODataKeyFields = SystemId;
SourceTable = "WS Order Line";
DelayedInsert = true;
layout
{
area(content)
{
repeater(records)
{
field(id; Rec.SystemId)
{
Editable = false;
}
field(orderId; Rec."Order Id")
{
}
field(description; Rec.Description)
{
}
}
}
}
}
page 50352 "WS Order Summary API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'orderSummary';
EntitySetName = 'orderSummaries';
ODataKeyFields = SystemId;
SourceTable = "WS Order Summary";
DelayedInsert = true;
layout
{
area(content)
{
repeater(records)
{
field(id; Rec.SystemId)
{
Editable = false;
}
field(orderId; Rec."Order Id")
{
}
field(summary; Rec.Summary)
{
}
}
}
}
}

View file

@ -0,0 +1,30 @@
---
bc-version: [17..]
domain: web-services
keywords: [api-page, page-part, subpagelink, systemid, multiplicity, deep-insert, navigation-property]
technologies: [al]
countries: [w1]
application-area: [all]
---
# Link API parts on SystemId and choose the correct multiplicity
## Description
`Multiplicity` is available from runtime 6.3 (Business Central 17.3) and defaults an API page part to a 1:N collection. The multiplicity-specific guidance therefore does not apply to BC 17.0 through 17.2. An API page part creates an OData navigation property and, for collection multiplicity, enables deep insert of child entities. When a custom parent API is keyed by its immutable `SystemId`, its child should carry a related GUID foreign key so the navigation constraint uses that same stable external identity. `Multiplicity` controls whether metadata exposes an object (`ZeroOrOne`) or a collection (`Many`).
## Best Practice
Define the child foreign key as `Guid` with a `TableRelation` to the parent table's `SystemId`, then use `SubPageLink = "<Parent Id>" = Field(SystemId)` on the parent API page. A child collection may omit `Multiplicity` and rely on the default 1:N relationship, or declare `Multiplicity = Many` explicitly. Set `Multiplicity = ZeroOrOne` when the intended navigation metadata is a singleton.
See sample: `link-api-parts-on-systemid-and-set-multiplicity.good.al`.
## Anti Pattern
On a parent API with `ODataKeyFields = SystemId`, linking a child business field such as `"Order No."` to the parent's `"No."` creates a second identity scheme for navigation instead of using the contract's stable GUID. A separate defect is an explicit `Multiplicity` that conflicts with the intended shape, such as `ZeroOrOne` on an order-lines collection or `Many` on a singleton. Do not treat omission alone as a defect: it is valid for a collection because the default is 1:N, while an intended singleton must explicitly use `Multiplicity = ZeroOrOne`.
See sample: `link-api-parts-on-systemid-and-set-multiplicity.bad.al`.
## Source
[Developing a custom API](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/devenv-develop-custom-api) and [Multiplicity property](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/developer/properties/devenv-multiplicity-property).

View file

@ -0,0 +1,22 @@
query 50355 "WS Webhook Customer Query"
{
QueryType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'webhookCustomer';
EntitySetName = 'webhookCustomers';
elements
{
dataitem(customer; Customer)
{
column(id; SystemId)
{
}
column(displayName; Name)
{
}
}
}
}

View file

@ -0,0 +1,4 @@
function receiveBusinessCentralWebhook(request, response) {
processNotifications(request.body.value);
response.sendStatus(200);
}

View file

@ -0,0 +1,28 @@
page 50354 "WS Webhook Customer API"
{
PageType = API;
APIPublisher = 'contoso';
APIGroup = 'sales';
APIVersion = 'v1.0';
EntityName = 'webhookCustomer';
EntitySetName = 'webhookCustomers';
ODataKeyFields = SystemId;
SourceTable = Customer;
layout
{
area(content)
{
repeater(records)
{
field(id; Rec.SystemId)
{
Editable = false;
}
field(displayName; Rec.Name)
{
}
}
}
}
}

View file

@ -0,0 +1,11 @@
function receiveBusinessCentralWebhook(request, response) {
const validationToken = request.query.validationToken;
if (typeof validationToken === "string") {
response.status(200).type("text/plain").send(validationToken);
return;
}
processNotifications(request.body.value);
response.sendStatus(200);
}

View file

@ -0,0 +1,30 @@
---
bc-version: [all]
domain: web-services
keywords: [webhook, subscription, validationtoken, expirationdatetime, webhook-supported-resources, api-page, sourcetabletemporary, querytype]
technologies: [al, javascript]
countries: [w1]
application-area: [all]
---
# Verify webhook eligibility and complete every validationToken handshake
## Description
Business Central can subscribe only to eligible API pages, not every endpoint that can be read through an API. Webhooks exclude API queries, temporary API pages, pages with composite OData keys, pages over system tables, and pages over Job Queue Entry (table 472); the environment's `webhookSupportedResources` endpoint is authoritative. Creating and renewing a subscription both call the `notificationUrl` with `validationToken`, and both fail unless the subscriber returns that token in the response body with `200 OK`.
## Best Practice
Before creating a subscription, confirm the resource appears in `webhookSupportedResources` and that a custom endpoint is an API page with a single stable key over an eligible persistent table. Use one validation path that echoes `validationToken` for both create (`POST`) and renew (`PATCH`) handshakes. Track `expirationDateTime` and renew before expiry: online subscriptions expire after three days, while on-premises lifetime defaults to three days and can be changed with `ApiSubscriptionExpiration`.
See samples: `webhook-eligibility-and-validationtoken-renewal.good.al` and `webhook-eligibility-and-validationtoken-renewal.good.js`.
## Anti Pattern
Attempting to subscribe to an API query, temporary/composite/system-table/Job Queue Entry API page, or assuming a successful create handshake makes renewal automatic. Composite includes an explicit multi-field `ODataKeyFields` and a missing `ODataKeyFields` when the source table's primary key has multiple fields. A renewal issues the same validation challenge; a notification handler that ignores the query-string token cannot create or renew the subscription.
See samples: `webhook-eligibility-and-validationtoken-renewal.bad.al` and `webhook-eligibility-and-validationtoken-renewal.bad.js`.
## Source
[Working with webhooks](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/api-reference/v2.0/dynamics-subscriptions) and [Update subscriptions](https://learn.microsoft.com/dynamics365/business-central/dev-itpro/api-reference/v2.0/api/dynamics_subscriptions_update).

View file

@ -16,7 +16,7 @@ application-area: [all]
Reviews AL page source and control add-in UI files against the `ui` knowledge domain in BCQuality and emits a findings report. This is a leaf action skill: it invokes no sub-skills. It is one of the skills composed by `al-code-review`.
UI findings apply to page files — files that declare `PageType = ...`, including `*.Page.al` under the standard file-naming convention — and to JavaScript/CSS/HTML files that render Business Central control add-ins. The skill returns `not-applicable` when the diff contains no page or control add-in UI changes.
UI findings apply to page files — files that declare `PageType = ...`, including `*.Page.al` under the standard file-naming convention — and to JavaScript/CSS/HTML files that implement Business Central control add-ins, including their client-service communication. The skill returns `not-applicable` when the diff contains no page or control add-in changes.
An orchestrator invokes this skill with either a `pr-diff` or a `file-path`. The skill produces a single JSON document conforming to the DO output contract.
@ -39,9 +39,9 @@ Discard files that are not applicable. Retain conditionally applicable files onl
Narrow the relevant files to the subset that applies to the changes under review.
- **UI-file filter.** UI review applies to files declaring `page`, `pageextension`, or `pagecustomization`, and to control add-in JavaScript/CSS/HTML that changes rendered UI. When the diff contains no such files, return `outcome: "not-applicable"` without evaluating knowledge files.
- For each relevant knowledge file, compute overlap against changed page declarations and control add-in UI files, weighted toward `Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `OptionCaption`, `ShowCaption`, `InstructionalText`, `GridLayout`, `Style`, `StyleExpr`, action definitions, field-level properties, DOM creation, ARIA attributes, and keyboard/focus handlers.
- Tokens extracted from the diff (`Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `PageType`, `ShowCaption`, `InstructionalText`, `grid`, `fixed`, `GridLayout`, `Style`, `StyleExpr`, `Favorable`, `Unfavorable`, `Ambiguous`, `cuegroup`, `controladdin`, `usercontrol`, `aria-`, `tabindex`, `keydown`, `focus`, `innerHTML`, `createElement`, `&`, `Specifies`, `Message(`, `Confirm(`, `Error(` in a page context, `Disabled`, `Invalid`, `Whitelist`, `Blacklist`, trailing punctuation patterns on captions).
- **UI-file filter.** UI review applies to files declaring `page`, `pageextension`, or `pagecustomization`, and to JavaScript/CSS/HTML that implements a control add-in's rendering or Business Central communication. When the diff contains no such files, return `outcome: "not-applicable"` without evaluating knowledge files.
- For each relevant knowledge file, compute overlap against changed page declarations and control add-in files, weighted toward `Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `OptionCaption`, `ShowCaption`, `InstructionalText`, `GridLayout`, `Style`, `StyleExpr`, action definitions, field-level properties, DOM creation, ARIA attributes, keyboard/focus handlers, packaged-resource AJAX, and calls from JavaScript into AL.
- Tokens extracted from the diff (`Caption`, `ToolTip`, `AboutTitle`, `AboutText`, `PageType`, `ShowCaption`, `InstructionalText`, `grid`, `fixed`, `GridLayout`, `Style`, `StyleExpr`, `Favorable`, `Unfavorable`, `Ambiguous`, `cuegroup`, `controladdin`, `control-add-in`, `usercontrol`, `aria-`, `tabindex`, `keydown`, `focus`, `innerHTML`, `createElement`, `packaged-resource`, `ajax`, `$.get`, `$.ajax`, `XMLHttpRequest`, `xhrFields`, `withCredentials`, `withcredentials`, `InvokeExtensibilityMethod`, `invokeextensibilitymethod`, `skipIfBusy`, `successCallback`, `success-callback`, `errorCallback`, `setInterval`, `JSON.stringify`, `payload`, `throttling`, `reduced-functionality`, `ClientServicesMaxUploadSize`, `&`, `Specifies`, `Message(`, `Confirm(`, `Error(` in a page context, `Disabled`, `Invalid`, `Whitelist`, `Blacklist`, trailing punctuation patterns on captions).
A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed page element. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone.
@ -53,6 +53,8 @@ When the post-conflict worklist is empty because no applicable UI knowledge exis
For each worklist entry, evaluate the diff against the file's `## Best Practice` and `## Anti Pattern` sections. UI text findings are generally `minor` — they affect localization and polish rather than correctness. Accessibility findings for missing labels, broken grid semantics, semantic color without text meaning, or UI-rendering control add-in changes can be `major`; use `minor` for low-risk manual-review reminders and polish issues.
For packaged-resource requests, flag `$.get` or AJAX/XHR that omits `withCredentials` only when the URL is identifiable as a resource in the control add-in package; do not generalize the rule to external endpoints. For `InvokeExtensibilityMethod`, flag repeated or timer-driven calls that can overlap because they do not wait for the success/error callbacks, and unbounded serialized payloads sent in one call. Prefer bounded chunks serialized through completion callbacks. Do not emit generic browser or JavaScript performance advice.
Set `confidence` to:
- `high` when the detection is based on an unambiguous pattern match (banned term literal, missing "Specifies" opener on a field tooltip, caption exceeding documented limit).
@ -69,7 +71,7 @@ Outcome selection:
- `completed` — the skill evaluated every worklist item.
- `no-knowledge` — no applicable UI knowledge survived filtering.
- `not-applicable` — the diff contains no page, pageextension, pagecustomization, or control add-in UI files.
- `not-applicable` — the diff contains no page, pageextension, pagecustomization, or control add-in implementation files.
- `partial` — a budget was hit before the worklist was exhausted.
- `failed` — an unrecoverable error occurred.

View file

@ -3,11 +3,11 @@ kind: action-skill
id: al-web-services-review
version: 1
title: AL web services review
description: Reviews AL source changes against web-services (API page) guidance from BCQuality.
description: Reviews AL API surfaces and webhook integration handlers against web-services guidance from BCQuality.
inputs: [pr-diff, file-path]
outputs: [findings-report]
bc-version: [all]
technologies: [al]
technologies: [al, javascript]
countries: [w1]
application-area: [all]
---
@ -27,7 +27,7 @@ Read the BCQuality knowledge index once — the `knowledge-index.json` BCQuality
Apply the frontmatter matching rules defined in READ (*Frontmatter matching semantics*) against the task context:
- `bc-version` — the target BC version from the PR branch's `app.json` or the orchestrator-supplied version. If unavailable, the dimension is `unknown`.
- `technologies``[al]`.
- `technologies``[al]` or `[javascript]`.
- `countries` — the countries declared in the consuming app's `app.json`. Default to the orchestrator's configured context; if absent, `unknown`.
- `application-area` — the union of application areas declared by the changed objects. Pass the actual set; do not substitute `[all]`. If the area cannot be determined from the changes, the dimension is `unknown`.
@ -37,9 +37,10 @@ Discard files that are not applicable. Retain conditionally applicable files (an
Narrow the relevant files to the subset that applies to the changes under review. For each relevant file, compute overlap against:
- The changed AL object names and types — especially page objects declared with `PageType = API`, and any procedure on such a page that exposes a bound action.
- The changed properties and triggers, weighted toward API page metadata (`APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `ODataKeyFields`, `SourceTable`), CRUD guards (`InsertAllowed`, `ModifyAllowed`, `DeleteAllowed`, `Editable`), the `OnOpenPage` trigger, and `OnValidate` triggers on exposed fields.
- Tokens extracted from the diff that relate to API surface and behaviour (`PageType`, `API`, `APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `ODataKeyFields`, `SystemId`, `ServiceEnabled`, `WebServiceActionContext`, `SetActionResponse`, `ReadIsolation`, `IsolationLevel`, `ReadCommitted`, `InsertAllowed`, `ModifyAllowed`, `DeleteAllowed`, `Editable`, `SourceTable`).
- The changed AL object names and types — especially pages declared with `PageType = API`, API page `part` controls, queries declared with `QueryType = API`, and procedures that expose bound actions.
- The changed properties and triggers, weighted toward API page metadata (`APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `ODataKeyFields`, `SourceTable`, `SourceTableTemporary`), navigation metadata (`SubPageLink`, `Multiplicity`, and visible singleton or collection semantics), CRUD guards (`InsertAllowed`, `ModifyAllowed`, `DeleteAllowed`, `Editable`), the `OnOpenPage` trigger, and `OnValidate` triggers on exposed fields.
- Webhook subscriber handlers and subscription lifecycle code, especially code that creates or renews subscriptions, handles `validationToken`, schedules from `expirationDateTime`, or targets resources whose eligibility is visible in the diff.
- Tokens extracted from the diff that relate to API surface and behaviour (`PageType`, `QueryType`, `API`, `api-page`, `page-part`, `APIPublisher`, `APIGroup`, `APIVersion`, `EntityName`, `EntitySetName`, `ODataKeyFields`, `SystemId`, `SubPageLink`, `subpagelink`, `Multiplicity`, `multiplicity`, `Many`, `ZeroOrOne`, `SourceTableTemporary`, `Job Queue Entry`, `webhook`, `webhookSupportedResources`, `webhook-supported-resources`, `subscriptions`, `notificationUrl`, `validationToken`, `validationtoken`, `expirationDateTime`, `expirationdatetime`, `ServiceEnabled`, `WebServiceActionContext`, `SetActionResponse`, `ReadIsolation`, `IsolationLevel`, `ReadCommitted`, `InsertAllowed`, `ModifyAllowed`, `DeleteAllowed`, `Editable`, `SourceTable`).
A file enters the candidate worklist when its `keywords` intersect the extracted tokens or its topic (derived from the index entry's `path`, `title`, and `description`) matches a changed object type. Read an article's full file — its `## Best Practice` / `## Anti Pattern` bodies — only after it makes the worklist; candidate selection uses the index alone.
@ -55,6 +56,8 @@ For each worklist entry, evaluate the diff against the file's `## Best Practice`
- When the diff contains code that contradicts a Best Practice without being a full anti-pattern, emit `minor` with the same reference shape.
- When the skill cannot detect a violation but the file is clearly applicable to the change, emit `info` citing the file. Repository-wide observations MAY omit `location`.
For API parts whose parent declares `ODataKeyFields = SystemId`, detect a child foreign key linked to a parent business field instead of `Field(SystemId)`. Do not apply the SystemId-link rule to APIs intentionally keyed by another field. Omitted `Multiplicity` is valid and means the documented default 1:N collection; never report omission alone. Report an explicit `ZeroOrOne` only when the visible contract clearly intends a collection or deep insert, and report an explicit `Many` only when it clearly intends a singleton. Singleton metadata requires an explicit `ZeroOrOne`; do not infer singleton intent from naming alone. For webhook eligibility, detect `QueryType = API`, `SourceTableTemporary = true`, composite `ODataKeyFields` (including an omitted property when a visible source primary key is composite), Job Queue Entry, and visible system-table sources; do not infer an unknown table number. For lifecycle code, require both create and renew paths to use a handler that returns the query-string `validationToken` verbatim with `200 OK`, and flag renewal scheduling that assumes subscriptions are permanent instead of using `expirationDateTime`. Do not emit generic HTTP or REST advice.
Set `confidence` to:
- `high` when the detection is based on an unambiguous pattern match (identifier, syntax, object type).
@ -71,7 +74,7 @@ Outcome selection:
- `completed` — the skill evaluated every worklist item; default when the skill finishes normally, including when the resulting `findings` array is empty.
- `no-knowledge` — no applicable web-services knowledge survived Source, Relevance, configuration filtering, and conflict resolution. `findings` is empty.
- `not-applicable` — the task context lacks an AL dimension (no AL changes in the diff, or `technologies` filter rejected the task).
- `not-applicable` — the task context contains no AL API surface, JavaScript webhook subscription lifecycle code, or JavaScript notification handler, or the `technologies` filter rejected the task.
- `partial` — a time or token budget was hit before the worklist was exhausted. `summary.coverage` reflects the evaluated subset; `outcome-reason` explains the cause.
- `failed` — an unrecoverable error occurred. `outcome-reason` is required.