# DEXPRO AWF External Approvals — SharePoint Integration Contract

This document describes the **SharePoint list contract** between Business Central (the AWF extension) and any external system that consumes or responds to approval requests. It is **transport-neutral**: the reference implementation uses Power Automate + Microsoft Teams (see `SETUP-GUIDE.md`), but any system that can read and write SharePoint list items via the Microsoft Graph API — or the native SharePoint REST API — can participate.

> **Audience:** integration developers. End-user setup is covered in `SETUP-GUIDE.md`.

---

## 1. Design principles

1. **The SharePoint list is the queue.** BC writes a list item when an external approver must decide; the consumer writes back to the same item with the decision; BC's Job Queue poller picks it up.
2. **BC is the system of record.** The SP item is transient — BC deletes it after processing. Consumers must not rely on SP history.
3. **Stable internal column names.** The column `name` (not `displayName`) is the contract. Display names are localized and must not be used.
4. **Forward compatibility.** BC writes a `SchemaVersion` value on every item. Consumers should reject / log items with an unsupported version instead of silently misbehaving.
5. **Idempotency.** BC only cares about the first terminal status a consumer writes. Writing the same status twice is a no-op.

---

## 2. Polling model

- BC runs a Job Queue entry (`Codeunit 70954832 "DXP AWF Ext. Approval Poller"`) on a recurring interval (default: every 2 minutes).
- The poller fetches items that match `Processed eq false and ApprovalStatus ne 'Pending' and BCCompanyName eq '<current company>'` (server-side filter; falls back to client-side if Graph declines the filter). The company clause is always applied — see § 7a.
- For each matched item, BC updates the corresponding AWF entry, calls the workflow engine, then sets `Processed = true` on the SharePoint item and attempts to `DELETE` it.
- If `DELETE` fails, the `Processed = true` flag is enough to exclude the item from subsequent polls; orphaned items are retried every cycle by `CleanupOrphanedSPItems`.

**Implication for consumers:** you have between 0 and *N* minutes (where *N* is the polling interval) between writing the decision and BC processing it. Do not expect synchronous confirmation. If you need a confirmation back, watch for the item's `Processed` field to flip or the item to disappear.

---

## 3. Column reference

Every column below uses **the internal `name`** — that is the contract. Display names may be localized.

### 3.1 Inputs (BC → consumer)

BC populates these when the item is created. Consumers **must not modify** them.

<table id="bkmrk-nametypenotesschemav"><thead><tr><th>Name</th><th>Type</th><th>Notes</th></tr></thead><tbody><tr><td>`SchemaVersion`</td><td>Text</td><td>Currently `"1"`. Bump indicates a breaking change in this contract.</td></tr><tr><td>`BCCompanyName`</td><td>Text</td><td>The BC company that owns the entry. Always set on new rows; must not be changed by consumers. BC's per-company poller uses this to ignore items from other companies (see § 7a).</td></tr><tr><td>`Title`</td><td>Text</td><td>Short record description (e.g. purchase order no.). Part of the built-in SharePoint column.</td></tr><tr><td>`BCEntryNo`</td><td>Text</td><td>Entry No. of the AWF `Ext. Approval Entry` record **within `BCCompanyName`**. **Not globally unique** — always pair with `BCCompanyName` when correlating.</td></tr><tr><td>`BCApprovalEntryNo`</td><td>Text</td><td>Entry No. of the underlying BC standard `Approval Entry` (table 454). Identical across all peer rows in a group — useful for grouping rows by their underlying decision unit (e.g. cancelling peers when one claims).</td></tr><tr><td>`BCInstanceID`</td><td>Text</td><td>AWF workflow instance GUID — globally unique across companies.</td></tr><tr><td>`Description`</td><td>Text</td><td>Localized RecordId text of the approved record (`Format(RecId, 0, 1)`, e.g. *Purchase Line: Invoice,EKRECH1052,20000* with translated table/field captions). Shown on the card as the record line; works for any table.</td></tr><tr><td>`ApproverEmail`</td><td>Text</td><td>UPN / mail address. Route notifications to this address.</td></tr><tr><td>`ApproverDisplayName`</td><td>Text</td><td>Human-readable name.</td></tr><tr><td>`AttachmentsUrl`</td><td>Text</td><td>SharePoint sharing link to a folder containing document attachments. Empty when upload is disabled or no attachments exist.</td></tr><tr><td>`BCDocumentUrl`</td><td>Text</td><td>Direct link to the source record in Business Central. Opens for approvers whose Business Central licence grants web-client access; others see a sign-in wall. Empty if BC could not derive a card-page URL for the record.</td></tr><tr><td>`RecordDetails`</td><td>Multi-line text</td><td>CommonMark markdown rendering of the approved record's own fields (e.g. for a Purchase Line: Type, No., Description, Quantity, Direct Unit Cost, Line Amount). Renderable directly in an Adaptive Card `TextBlock` with `wrap=true`. Curated for the common Sales / Purchase header &amp; line tables; falls back to a generic field walk for other tables.</td></tr><tr><td>`IsGroupApproval`</td><td>Boolean</td><td>`true` when this item is one of several for a claim-based group approval.</td></tr><tr><td>`GroupCode`</td><td>Text</td><td>External approver group code (only set when `IsGroupApproval = true`).</td></tr><tr><td>`Amount`</td><td>Text</td><td>Display-formatted amount (locale-dependent).</td></tr><tr><td>`AmountRaw`</td><td>Number</td><td>Machine-readable amount (always point-decimal). **Prefer this over `Amount`.**</td></tr><tr><td>`DueDate`</td><td>Text</td><td>Display-formatted date.</td></tr><tr><td>`DueDateRaw`</td><td>DateTime</td><td>ISO 8601, UTC. **Prefer this over `DueDate`.**</td></tr><tr><td>`CreatedRaw`</td><td>DateTime</td><td>ISO 8601, UTC. When the item was created by BC.</td></tr><tr><td>`SenderName`</td><td>Text</td><td>Display name of the requester (`User."Full Name"`); falls back to the BC user id when no full name is recorded.</td></tr><tr><td>`StageName`</td><td>Text</td><td>Workflow stage description.</td></tr><tr><td>`LblTitle` … `LblClaimedSuccessMsg`</td><td>Text</td><td>Pre-translated UI strings and message templates for the approver's language (see 3.4). The `LblAlready…Msg` and `LblClaimedSuccessMsg` columns carry message templates with `{title}` / `{claimer}` placeholders that the consumer (e.g. Power Automate) substitutes at runtime.</td></tr></tbody></table>

### 3.2 Response fields (consumer → BC)

The consumer writes these once a decision is made. Only `ApprovalStatus` is required.

<table id="bkmrk-nametyperequirednote"><thead><tr><th>Name</th><th>Type</th><th>Required</th><th>Notes</th></tr></thead><tbody><tr><td>`ApprovalStatus`</td><td>Choice</td><td>**yes**</td><td>One of `Approved`, `Rejected`, `Claimed`. Case-insensitive on the BC side. SharePoint presents a dropdown with all valid values; the initial/default value is `Pending`. Any non-terminal value is logged by BC and the item is left alone.</td></tr><tr><td>`ResponseComment`</td><td>Multi-line Text</td><td>no</td><td>Free text, max 2048 chars on the BC side. SharePoint renders a multi-line text area for editing. Shown in BC's approval comment trail.</td></tr><tr><td>`ResponseDateTime`</td><td>DateTime</td><td>no</td><td>ISO 8601 UTC via Graph, native picker in SP. If omitted, BC uses the current time.</td></tr><tr><td>`ClaimedBy`</td><td>Text</td><td>yes when `ApprovalStatus = Claimed`</td><td>Email of the group member who claimed the entry. BC also writes this column on **peer items** that get cancelled because someone else in the group claimed first — in that case it identifies the user who took the entry over (the SP item's `ApprovalStatus` will simultaneously be `Cancelled`).</td></tr><tr><td>`TeamsMessageId`</td><td>Text</td><td>no</td><td>The Microsoft Teams message ID of the adaptive card the consumer posted for this item. A consumer that wants to **close peer cards** (replace a group member's still-open card once another member decides) writes this back right after posting the card, and reads peers' values during the claim cascade to target *Update an adaptive card*. BC also stores it (and accepts it via the `setTeamsMessageId` bound action) for diagnostics. Optional — leave empty if the consumer doesn't implement card-closing.</td></tr></tbody></table>

> **Do not write** `Processed`, `SchemaVersion`, `BCEntryNo`, `BCInstanceID`, or any `Lbl*` column. BC owns them. (`TeamsMessageId` is the one consumer-writable transport column besides the response fields above.)

### 3.3 BC-managed state

<table id="bkmrk-nametypewritten-bypu"><thead><tr><th>Name</th><th>Type</th><th>Written by</th><th>Purpose</th></tr></thead><tbody><tr><td>`Processed`</td><td>Boolean</td><td>BC only</td><td>`true` once BC has consumed the response. Poller excludes these from subsequent fetches. Consumers must treat as read-only.</td></tr></tbody></table>

### 3.4 Localized label columns

BC pre-translates the adaptive-card / UI labels for the approver's language and writes them as plain text. Consumers can use them directly without calling BC or a translation service.

<table id="bkmrk-nametypical-english-"><thead><tr><th>Name</th><th>Typical English value</th></tr></thead><tbody><tr><td>`LblTitle`</td><td>Approval Request</td></tr><tr><td>`LblApprove`</td><td>Approve</td></tr><tr><td>`LblReject`</td><td>Reject</td></tr><tr><td>`LblComment`</td><td>Comment</td></tr><tr><td>`LblClaim`</td><td>Claim</td></tr><tr><td>`LblRelease`</td><td>Release</td></tr><tr><td>`LblAmount`</td><td>Amount</td></tr><tr><td>`LblSender`</td><td>Requested by</td></tr><tr><td>`LblDueDate`</td><td>Due Date</td></tr><tr><td>`LblStage`</td><td>Stage</td></tr><tr><td>`LblDescription`</td><td>Description</td></tr><tr><td>`LblAttachments`</td><td>Approval Documents</td></tr><tr><td>`LblOpenInBC`</td><td>Open in Business Central</td></tr><tr><td>`LblDetails`</td><td>Details</td></tr><tr><td>`LblAlreadyClaimedByMsg`</td><td>This approval ({title}) has already been claimed by {claimer}. You can safely ignore this card.</td></tr><tr><td>`LblAlreadyHandledMsg`</td><td>This approval ({title}) has already been handled by another approver or cancelled in Business Central. You can safely ignore the earlier card.</td></tr><tr><td>`LblClaimedSuccessMsg` (display name *Decision Confirmation*)</td><td>The approval entry "{title}" has been successfully processed.</td></tr><tr><td>`LblApprovedResult` (display name *Approved (Result)*)</td><td>Approved</td></tr><tr><td>`LblRejectedResult` (display name *Rejected (Result)*)</td><td>Rejected</td></tr><tr><td>`LblResultTitle` (display name *Result Title*)</td><td>Approval Decision</td></tr><tr><td>`LblClosedTitle` (display name *Closed Title*)</td><td>No Longer Open</td></tr><tr><td>`LblResponseSent` (display name *Response Sent*)</td><td>Response sent.</td></tr></tbody></table>

---

## 4. ApprovalStatus state machine

```
       ┌─────────┐   consumer writes       ┌──────────┐
       │ Pending │──────────────────────▶  │ Approved │ ─┐
       └────┬────┘                         └──────────┘  │
            │                                            │  BC poller
            │ consumer writes                            │  picks up,
            ▼                                            │  sets Processed=true,
       ┌──────────┐                                      │  then DELETE
       │ Notified │──────────────▶ Approved / Rejected ──┤
       └────┬─────┘                                      │
            │                                            │
            │ group claim                                │
            ▼                                            │
       ┌─────────┐                                       │
       │ Claimed │──────────────▶ Approved / Rejected ───┘
       └─────────┘
```

- **`Pending`** — written by BC on creation.
- **`Notified`** *(optional)* — consumer can set this after the approver has been notified but before they respond. Purely informational; BC does not act on it.
- **`Claimed`** — group approvals only. Consumer must also set `ClaimedBy`.
- **`Approved` / `Rejected`** — terminal. Consumer may also set `ResponseComment` and `ResponseDateTime`.
- `Processed`, `Error`, `Cancelled` — BC-internal terminal states. Do not write them.

---

## 4a. Approving directly in SharePoint (no consumer app)

Because the list uses typed columns, a human can approve an entry by opening the list item in SharePoint and editing it — no Teams, no Power Automate, no BC access required:

1. Open the SharePoint approval list in a browser.
2. Click the pending item. SharePoint opens the edit form.
3. Pick a terminal value from the **Approval Status** dropdown: `Approved`, `Rejected`, or (for group approvals) `Claimed`.
4. Optionally type a reason in the **Response Comment** text area.
5. Optionally set **Response DateTime** (left blank, BC uses the current time).
6. Click **Save**.

BC's Job Queue poller picks up the change on the next cycle (default: every 2 minutes) and processes it through the approval workflow identically to a Power Automate write-back.

> **Permissions:** the SharePoint user needs Edit permission on the list. Because the `Processed`, `SchemaVersion`, `BCEntryNo`, and `Lbl*` columns are BC-owned, consider using a SharePoint view or column-level formatting to hide them from the edit form — users only need `ApprovalStatus`, `ResponseComment`, `ResponseDateTime`, and (for claim) `ClaimedBy`.

---

## 5. Minimal response payload

### Approve

```
PATCH /sites/{siteId}/lists/{listId}/items/{itemId}/fields HTTP/1.1
Authorization: Bearer ...
Content-Type: application/json

{
  "ApprovalStatus": "Approved",
  "ResponseComment": "Looks good to me.",
  "ResponseDateTime": "2026-04-22T10:15:00Z"
}
```

### Reject

```
{ "ApprovalStatus": "Rejected", "ResponseComment": "Duplicate PO." }
```

### Claim (group approval)

```
{ "ApprovalStatus": "Claimed", "ClaimedBy": "maria@contoso.com" }
```

---

## 6. Alternative: native BC API

The SharePoint list is the recommended integration point because the approver works entirely in Teams/SharePoint rather than the BC web client. Note that acting on BC data still requires each approver to hold an appropriate **paid Business Central user licence** — routing responses through a service account does **not** remove the per-user licence requirement (Microsoft multiplexing / indirect-access terms). DEXPRO makes no licensing representation; the customer and their Microsoft licensing partner are responsible for confirming licensing with Microsoft. If your system has a BC service-to-service principal, you can bypass SharePoint entirely and post directly to BC:

- **Publisher:** `dexpro`
- **Group:** `advancedWorkflow`
- **Version:** `v1.0`
- **Entity:** `externalApprovalEntries`
- **Bound actions:** `approve`, `reject`, `claim`, `releaseClaim`, `markNotified`

Example:

```
POST /api/dexpro/advancedWorkflow/v1.0/companies({id})/externalApprovalEntries({entryNo})/Microsoft.NAV.approve
{ "comment": "Looks good to me." }
```

Both paths — SharePoint write-back and the BC API — converge on the same `ProcessApprovalResponse` code path and are fully interchangeable.

---

## 7a. Multi-company BC installs

A single SharePoint list can serve multiple BC companies. Each company's job-queue poller runs independently in its own company context, and the poller's server-side `$filter` includes `fields/BCCompanyName eq '<current company>'` so a company only picks up its own items. There is a belt-and-braces client-side re-check on every page — rows whose `BCCompanyName` does not match the current company are discarded even if the server-side filter was bypassed.

**Implications for consumers:**

- Always treat `(BCCompanyName, BCEntryNo)` as the composite key. `BCEntryNo` alone is only unique within a single company.
- Do not write or modify `BCCompanyName`. BC sets it on item creation and never changes it.
- `BCInstanceID` (GUID) is globally unique and is a safe correlation key without `BCCompanyName`.
- If you build a Power Automate flow that writes items (e.g. a custom intake), set `BCCompanyName` to the target company or BC's poller will never see your items.

---

## 7. Versioning

- **Additive changes** (new columns, new optional response fields) will **not** bump `SchemaVersion`. Consumers must ignore unknown fields.
- **Breaking changes** (renamed columns, changed semantics, required response fields) will bump `SchemaVersion`. BC will continue to write the old schema version until a deprecation release is announced.
- Consumers should validate `SchemaVersion` on every item and refuse items whose version they do not understand.

---

## 8. Troubleshooting checklist for integrators

<table id="bkmrk-symptomlikely-causeb"><thead><tr><th>Symptom</th><th>Likely cause</th></tr></thead><tbody><tr><td>BC never processes a response</td><td>`ApprovalStatus` not one of `Approved`/`Rejected`/`Claimed`; or consumer wrote `Processed=true`; or AWF Setup has `SP Approvals Enabled = No`.</td></tr><tr><td>Item keeps reappearing in polls</td><td>`Processed` was not touched by the consumer (expected) **and** DELETE fails server-side — check Graph app permissions (`Sites.ReadWrite.All`). BC marks `Processed=true` before delete, so a single `Processed` flip in the UI is typically not needed.</td></tr><tr><td>Claim succeeds for two members</td><td>Consumer wrote `ApprovalStatus=Claimed` without checking the current status first. Prefer the BC API `claim` bound action — it enforces optimistic locking.</td></tr></tbody></table>