Skip to content

Experimental Copilot Credit Connection

Sam Betts edited this page Nov 5, 2026 · 10 revisions

Copilot credit history (preview) collects each person's month-to-date Copilot credit consumption from Microsoft Graph, through its own delegated connection. The installer, the portal and the sidebar all use that name. For how it relates to the other cost and value figures, see Copilot → Good to know about cost and value figures.

Preview, off by default, since Stable 1856. Implemented in PR #770, addressing issue #769. A separate delegated Microsoft Graph connection supports daily SQL observations and resumable background collection. It is separate from the Agent costs importer. Since Stable 1857 its readings appear on Copilot spend and Copilot investment, beside billed Copilot Studio credits but never added to them.

Role simplification, since Stable 1856 (PR #784): connection management uses the existing Portal.Administration app role. A separate cost-connection operator role is no longer needed. If someone has only the old CopilotCostConnection.Operator assignment, assign Administration and have them sign out and back in before removing the obsolete assignment/role from Entra. Existing saved reader connections and history are retained. Administration does not imply See PII or grant Microsoft Graph permissions.

Reporting and connection warnings, since Stable 1857 (PR #787): the Copilot spend and Copilot investment reports use this history alongside, but never added to, dated Copilot Studio consumption. The reporting and failure-warning behaviour described below needs Stable 1857 or later; Stable 1856 had only the original connection preview.

Token cache encrypted, since Stable 1859 (PR #825): reconnect after you rotate the runtime app's client secret or certificate. See Connection and probe operations.

Microsoft Graph Cost Management uses beta APIs whose contract may change. A successful connection or completed sweep is not proof of effective tenant-wide access, source freshness or uninterrupted refresh.

What the figures mean

Microsoft's service-balance reference defines consumedQuantity as month-to-date consumption in UTC for the selected service context. Each row belongs to a user and service and identifies the selected spendingPolicyId. These are credits, not daily consumption, invoice charges or currency. Service IDs identify case-sensitive governed categories, not individual agents. A zero, missing quantity, unlimited quantity, unknown quantity type, unevaluated service and denied request are different outcomes; the probe does not replace them with zero.

The diagnostic probe keeps private evidence in Azure Tables; the background collector retains daily observations and separate policy/tenant context in SQL. Reporting-period trust, category usefulness, effective tenant-wide access, refresh longevity and reset behaviour still require live evidence. They are not assumed merely because the preview can run.

Are these today's costs or month-to-date consumption?

The documented measure is month-to-date credits per user/service, not today's spend. "Daily history" means that the importer saves readings on their observation dates. It does not mean Graph returns a separate daily billing ledger.

Do not add successive readings together to calculate a period's consumption. Each can include usage already represented in an earlier reading. The report keeps these readings separate from the dated Copilot Studio ledger and from Azure currency charges.

Does the balance reset each month?

The documented accounting window is the current UTC month. Microsoft's reference also describes configuredPeriodLimit and configuredPerUserLimit as limits for the current UTC month. The month-to-date window therefore rolls forward at the start of the next UTC calendar month; it is not a rolling 30-day window or a month based on the portal reader's local time zone.

That contract is not proof that a freshly fetched row immediately becomes zero at midnight. Source data can lag; the response carries freshness information. Exact live reset timing and behaviour across policy changes have not been verified by this project. A policy change, correction or reset can make two readings unsuitable for subtraction.

Why does the report say the period is unverified?

The documented monthly meaning and the evidence attached to an individual saved reading are different things. The current payload has no explicit accounting-month identifier or period start/end fields. dataStatus.lastModifiedDateTime describes source freshness, not the accounting period.

The collector deliberately leaves the reading's SourceMonth unset and its PeriodStatus as unverified; it does not invent an accounting month from the response date or a freshness timestamp. The reports therefore show the latest reading and its observation date, with Period not confirmed by the source, rather than certifying an independently reconciled monthly bill. This caution does not mean Microsoft leaves the measure's documented month-to-date semantics undefined.

What happens after a failed connection, and can costs be backfilled?

Previously saved figures remain available; a failed read is not zero consumption. The collector retries, but a saved or refreshed token alone is not proof of recovery. In PR #787, a connected source is considered recovered only after a successful relevant cost API read, not after reconnect, directory inventory, policy/context reads or an independent Studio capacity read. Intentionally disabling collection or disconnecting suppresses that source's warnings without deleting its SQL history.

Source Recovery and historical limits
Graph credit-balance readings A later successful read records the balance observed then. Within the same month it may reflect consumption that happened during an outage, but it does not recover the missing daily observations or allocate that consumption to particular days. The documented user-balance method provides no historical-date parameter. Neither missing days nor a missed previous month's closing reading can be recreated by this collector.
Copilot Studio dated consumption The importer re-reads CopilotStudioCreditsTrailingWindowDays: 7 days by default, configurable up to 180 days. An outage older than the configured window needs a larger window before retrying. Recovery still depends on what the upstream API retains; the 180-day configuration ceiling is not a guaranteed retention commitment. See Agent Costs Import Configuration.

PR #787 also adds portal warnings and cost checks to the existing installer-managed health monitoring/action-group rule. External notifications require the updated default health checks and alerts to be deployed and enabled; a portal warning does not prove that notification delivery is configured or has been tested. See Health alerts. The wording of the portal banner, and which connection states show one, is in Copilot spend → Connection warnings.

Connection permissions and setup

  1. Leave Copilot credit history (preview) off in the installer until the prerequisites are ready. The saved import setting is ImportTaskSettings.CopilotCostManagement; it defaults to false. The installer propagates the preview connection availability setting to the web application. Enabling the switch does not connect a reader or grant Graph access.
  2. Configure the existing Azure Storage connection and Table network access. No Storage connection means unavailable: there is no runtime in-memory or Blob fallback. The feature creates the dedicated CopilotCostConnections table on first authorised access, not on ordinary portal startup.
  3. If shared-key access is disabled, grant the runtime service principal Storage Table Data Contributor and configure its existing certificate or client-secret credentials. Blob permissions and control-plane Contributor do not grant Table data access. The shared Table-client factory honours the runtime account's configured authentication; it does not substitute a managed identity or DefaultAzureCredential.
  4. Assign the existing Portal.Administration app role on the runtime enterprise application to the administrators who will manage the connection. No new app role is needed. Sign out and back in after assignment so the portal cookie receives it. Portal.SeePII is not required for these controls. Existing portal-role enforcement settings still apply; tenant, application, object-ID and browser bindings remain required even when EnforcePortalRoles=false.
  5. Add the exact Web redirect URI https://contoso.example/copilot-cost/callback, replacing only the synthetic host with the portal's HTTPS root URL. WebAppURL must be the corresponding HTTPS root without query/fragment. Leave the existing normal sign-in redirect registered.
  6. Add and consent to these delegated Microsoft Graph read permissions: CopilotCostManagement-UserData.Read.All, CopilotCostManagement-Policy.Read.All, CopilotCostManagement-Assignment.Read.All and CopilotCostManagement.Read.All. The flow also requests openid profile offline_access. It does not request delegated directory enumeration. Existing application User.Read.All supplies the independent complete current-user inventory.
  7. Select a work or school reader account with an active, tenant-wide Global Reader directory role, following Microsoft's permissions and role requirements. Activate an eligible PIM assignment before connecting; an expired assignment can interrupt later collection. Consent alone does not establish effective access. The administrator's app role authorises connection management; the reader's directory role authorises Graph reads. They are separate boundaries. The user-balance API reference documents delegated user-balance access; application permission is not supported for that read.
  8. Enable the installer preview switch, apply its database upgrade and deploy the configuration. Open Administration → Connections → Copilot credit history (preview), check status, and connect the intended reader. The portal explains these permissions before connecting. Check scheduled collection separately; the background importer must be running. Test Configuration uses app-only authentication and cannot prove delegated reader access.

The feature does not create app roles, assign roles, grant consent, change policies or bypass MFA/Conditional Access. It reuses existing secret/certificate support for the confidential Graph client. Ordinary portal/Teams sign-in scopes and their token handling are unchanged.

Connection and probe operations

Check connection reads safe metadata only, cached briefly in the web process; it does not acquire a Graph token. Connection management requires an authenticated portal administrator in the configured tenant and application, subject to the portal's role-enforcement setting. State-changing calls additionally require same-origin POSTs and anti-forgery protection.

Connect/reconnect opens a separate tenant-bound authorization-code flow with PKCE, nonce and single-use browser/operator correlation. Its form_post callback keeps the authorization code out of the URL and never replaces the ordinary portal authentication cookie. Reconnect accepts only the already selected immutable reader account. To deliberately choose a different reader, disconnect first, then establish a new connection. Expired or revoked consent/refresh access requires interactive reconnect; no automatic credential/MFA workaround is attempted.

Run or resume read-only probe requires a separate confirmation. It reads existing policies, service settings, selected-group audiences and tenant balance, then enumerates current directory IDs independently of importer metadata/activity filters. User reads start only when that inventory is complete. Until then, the displayed denominator is provisional.

The browser advances bounded server-side slices while the page is open: one context request, up to 25 inventory membership checks, or up to five named-user observations per slice. Private Table progress allows another instance/restart to resume. Stop or closing the page stops scheduling new slices; an already accepted slice can finish. Storage/network failures and throttling remain visible rather than becoming an empty successful sweep. A missing/security-trimmed 404 counts as denied/missing, not as an empty balance. Observed, measured, empty, partial and unevaluated counts can overlap and must not be added together.

Prepare a new observation sweep clears only the diagnostic probe's active progress pointer. It does not delete earlier private evidence and does not start reading until another probe is authorised. It neither starts nor stops the independent scheduled collector.

Disconnect removes the active connection/cache, fences older owners and invalidates pending authorization/probe progress. It does not delete existing evidence or SQL history, revoke tenant consent, or sign the reader out of Microsoft. Use Entra's consent/session controls separately if revocation is required.

Reconnect after rotating the runtime credential. Since Stable 1859 (PR #825), the reader's token cache is encrypted at rest with keys derived from the runtime app's client secret or Key Vault certificate. A cache stored unencrypted by an earlier build is encrypted the first time it's used, so upgrading doesn't need a reconnect. But after you rotate the runtime app's client secret or certificate, the cache can't be read any more: under Administration → Connections → Copilot credit history (preview) the page says the connected account needs to sign in again, and you select Reconnect same reader. Do the same if you roll back to an earlier build after 1859 has used the connection.

Scheduled daily history

The preview runs as a separate optional phase in the ordinary Office 365 importer. It yields between bounded slices rather than blocking the other imports for an entire tenant sweep. Turning it off does not delete data. It does not reuse the Power Platform/Azure cost connection, portal visitors' tokens or the Teams token cache.

Every current directory user is considered, independently of group filters, licence status, enabled-account filters or whether an existing SQL user row exists. A complete inventory is established before user observations start; a failed inventory cannot silently shrink coverage. Raw Entra IDs are preserved, with nullable links to in-scope existing SQL users rather than creating users outside metadata scope.

Later inventories also relink up to 1,000 previously unlinked observations per inventory page when the corresponding existing SQL users become available. Larger retained backlogs converge across later sweeps; no users are created to force a link.

The parent observation and its service/unevaluated bundle retain the actual UTC response observation date. Work crossing midnight is not backdated, and missed days are not filled with today's reading. Same-day retries are idempotent, with last-valid data preserved when a later attempt fails. User facts, policy context and tenant billing-method balances remain separate.

Source accounting period is separate from observation date. Microsoft documents UTC month-to-date consumption, but an individual reading's accounting month is not explicitly identified in the payload. Unknown or inconsistent source-period evidence remains unknown; no daily delta is inferred. A selected-policy change, recreation, correction or month reset can break comparability. Do not sum MTD readings across days, shared policy caps across people, or overlapping Graph/Power Platform/Azure figures.

Check scheduled collection reads cached operational facts without acquiring a Graph token. Inventory status, completed/failed/deferred work, backlog and last attempt/completion should be read together. A completed sweep can still contain denied/missing or unevaluated users; it is not a financial reconciliation or live-readiness approval. Source errors, throttling and reconnect requirements do not become zeros.

A missing/security-trimmed 404 ends that user's work for the current sweep and remains visible as denied/missing, without creating an empty observation. A later daily inventory considers the user again if still present. Transient failures and incomplete observations remain deferred rather than being reported as successful reads.

Scheduled coverage counters describe overlapping user outcomes, except unevaluated services, which counts service entries. They are not additive totals and must not be summed to infer successful user coverage.

SQL history is retained indefinitely until an explicit scoped DBA purge. Disconnect, a new diagnostic sweep, upgrading the preview or disabling the switch is not a purge. Plan for that storage obligation before enabling it.

Database upgrade and explicit purge

The additive migration is 202610230900001_AddCopilotCostPreview. It creates empty copilot_cost_* tables, indexes and the purge procedure; it does not rewrite existing audit or user tables, and does not change the EF entity model. Upgrade cost is empty-table DDL rather than a function of existing fact-table row counts; schema-lock acquisition can still wait behind other database activity. There is no large-table online/offline index build.

Use the installer database upgrade, or the matching 202610230900001_AddCopilotCostPreview.manual.sql supplied alongside the migration. For a manual upgrade, run prerequisite scripts in migration-ID order: its immediate predecessor is 202610221500003_UniqueOnlineMeetingIds. The script verifies schema objects and stamps __MigrationHistory; re-running it is safe. SQL migration availability is separate from the delegated connection: a missing schema reports database unavailability, not a successful empty sweep.

An authorised DBA can preview a bounded history purge using this synthetic example:

EXEC dbo.copilot_cost_purge_preview
    @BeforeDate = '20000101', @BatchSize = 1000, @DryRun = 1;

The cutoff must be before today's UTC date. @ObjectId optionally scopes it to one raw Entra user object ID; omit it for the selected date range across users. Each execution selects at most @BatchSize historical bundles and the same bound of current observation bundles. Inspect the preview, stop collection while intentionally deleting history, then explicitly supply @DryRun = 0, @Confirm = N'PURGE COPILOT COST HISTORY' with the same scope. Repeat only as deliberately required. The procedure deletes each selected observation with its dependent service/unevaluated rows in a transaction. It does not delete policy/tenant context, directory/work records or the private Table cache; those have independent retention obligations.

Private storage and operational limits

Treat the entire dedicated table, backups and exports as sensitive: it contains a serialized MSAL user-token cache (encrypted since Stable 1859, above), temporary authorization correlation/code material, reader/account IDs, directory IDs and probe evidence. Azure Storage encryption at rest does not hide these from privileged storage readers or account-key holders. No cache, raw payload or reader identity is returned by the portal API. Avoid broad HTTP-body/identity tracing; if external automatic HTTP dependency tracing is enabled, review/redact Graph request paths before using it because named-user paths contain identifiers.

Each document is one conditional Azure Table entity with eight binary properties, capped at 384 KiB of serialized data. All chunks replace atomically with an ETag and integrity check; unsupported sizes fail explicitly rather than truncating a token. A two-minute expiring owner plus conditional writes fences competing instances, callbacks and disconnects. Configured Storage failures never downgrade to ephemeral persistence.

The diagnostic probe remains an intentionally bounded tool with private per-user Table evidence. The SQL collector uses durable bounded work instead of treating that probe as the scheduled import. Both have explicit paging/response limits; a limit reached is incomplete coverage, not success. Endpoint-specific batching and real Graph throughput have not been proven; direct GETs are the supported preview path.

At a synthetic design size of 200,000 users, each sweep needs at least 200,000 balance operations plus inventory/context/retry reads. Two categories produce 400,000 service facts per complete daily observation, or 146 million across 365 observations, before parents, context and indexes. A synthetic SQL benchmark does not prove the Graph endpoint can sustain that rate. Monitor backlog and storage rather than assuming every opted-in tenant finishes daily.

Local synthetic proof used 200,000 work/parent rows and one million approximately 1-KiB history bundles. Ready-work paging used 3 logical reads and at most 1 ms at both dense and sparse eligibility; the coverage query used 1,912 reads and 110 ms. A bounded 999-ID existing-user link plus retained-observation relink used 7,213 reads and 238 ms, so complete inventory linking still has material cost. The fixture reserved approximately 1.24 GiB including its indexes, excluding service/context rows. These local warm-run results describe the additive store only: they are not a production SLA, a full-year storage estimate, a Graph throughput measurement or a claimed improvement to existing tables.

Evidence has no automatic retention deletion. Reset/disconnect leave generation-scoped private documents; expired pending authorization material is unusable but can remain stored until a later state replacement. For deliberate cleanup, stop the probe, disconnect, disable the feature and arrange a controlled removal of the dedicated table/partition and any copies/backups under your organisation's retention rules. Do not remove unrelated checkpoint tables.

Live-readiness evidence remains unproven

The preview is implemented before this evidence is available, by explicit project choice. Before relying on its readings, separately authorise a live test and establish:

  • Useful measurements for intended categories, including different users sharing an existing policy; used/unused/no-policy/limited/unlimited/partial/empty cases.
  • Complete directory coverage with denied/missing users accounted for, not a hand-picked sample.
  • Silent refresh beyond token expiry, process/instance changes and applicable Conditional Access/sign-in-frequency boundaries.
  • Direct expansion/continuation and endpoint-specific batching/fallback behaviour.
  • Trustworthy source accounting-period and month-reset evidence. Unobserved cases remain unresolved.

Keep live identifiers, names, counts, timestamps, payloads and raw evidence out of public code, issues and documentation. Record only synthetic shapes and a generic go/hold conclusion publicly.

Start here

Copilot analytics

Using the portal

Install & configure

Beyond Copilot

Operate

Upgrading from an older build

Reference

Project

Clone this wiki locally