Trigger Copilot Experiences from Business Events

Trigger Copilot Experiences from Business Events

An approval can become ready in SharePoint while nobody is talking to Copilot. I don’t want the process to depend on someone asking, “Is it ready yet?” The backend should notice the change and update the business state.

That does not mean an event should start a Copilot conversation or push content into an open chat. I treat the event as a way to keep the application current. Copilot can read that current state when the user returns to the experience or asks for an update.

The Microsoft documentation linked below was checked on November 1, 2026. Check current API limits, tenant policy, host support, and licensing before deploying.

Start with one business event

I’ll use a SharePoint list that tracks access requests. When a request moves to Ready for review, an approval queue should reflect that change. A reviewer can then open the queue or ask Copilot for the request’s current status.

SharePoint list webhooks are notifications that list items changed. The notification does not contain the individual changes. My app must use the list’s change log and a saved change token to find what happened. That makes the webhook a signal to reconcile, not the business event itself.

I keep the responsibilities separate:

Keep the notification, change record, and user experience in their own lanes.
LayerUse it forDo not treat it as
SharePoint webhookTell my service that a subscribed list may have changed.A complete record of which fields changed.
SharePoint change logRetrieve changes since my last saved change token.My application's approval rules or current user permissions.
Application stateStore the request status and the durable approval queue.A transient notification or a Copilot chat transcript.
Azure Event GridOptionally route normalized application events to other subscribers.The request database or a guarantee of exactly-once processing.
MCP status toolRead the current state when the user asks or opens the experience.A background trigger that starts or updates an arbitrary chat.
SharePoint webhook
Use it for
Tell my service that a subscribed list may have changed.
Do not treat it as
A complete record of which fields changed.
SharePoint change log
Use it for
Retrieve changes since my last saved change token.
Do not treat it as
My application's approval rules or current user permissions.
Application state
Use it for
Store the request status and the durable approval queue.
Do not treat it as
A transient notification or a Copilot chat transcript.
Azure Event Grid
Use it for
Optionally route normalized application events to other subscribers.
Do not treat it as
The request database or a guarantee of exactly-once processing.
MCP status tool
Use it for
Read the current state when the user asks or opens the experience.
Do not treat it as
A background trigger that starts or updates an arbitrary chat.

Trace the event from SharePoint to Copilot

I start with a small pipeline. The webhook endpoint accepts the signal quickly, then a worker does the slower reconciliation. Event Grid is optional: I add it when multiple independent consumers need the same application event, not just because an event happened.

Change notification

Validate and enqueue

GetChanges

Commit state, token, and outbox

On user request

Optional fan-out

SharePoint list and change log

Azure Function
webhook endpoint

Durable work queue

Change worker
reads from saved token

Application database

MCP status tool

Copilot experience

Outbox publisher

Azure Event Grid

Other consumers

The database owns the current request state. The webhook and Event Grid help move work between components; neither replaces that durable source of truth.

1. Register and renew the SharePoint subscription

I create a subscription for the specific list and keep its identity with the app’s tenant, site, list, notification URL, clientState, and expiration. SharePoint sends a validation request during subscription creation. My endpoint must return the supplied validationtoken as a plain-text response with HTTP 200; this is different from Event Grid’s endpoint-validation handshake.

Subscriptions expire. SharePoint’s documentation says the requested expiration must be less than 180 days, so I renew ahead of the stored expiration and alert if renewal fails. If a subscription expires before I can renew it, I create a new one and reconcile from my saved change token rather than assuming I received every notification.

2. Accept notifications without doing the whole job inline

For each notification, I look up the expected subscription and verify its clientState against the value stored for that subscription. I also verify that the subscription maps to the expected tenant and list. clientState is useful for checking that a notification matches a subscription I created; it is not a replacement for authentication or authorization.

After validation, I enqueue a durable “sync this list” job and return success. I don’t fetch every changed item or publish downstream events while SharePoint is waiting for the HTTP response. Repeated notifications can safely request the same list sync.

The callback can contain several notifications, and SharePoint retries failed deliveries. Its documentation says it retries up to five times, with five minutes between attempts, before dropping an unsuccessful notification. A later webhook can let my worker catch up from its change token, but I also schedule reconciliation so that a dropped final notification does not leave the list stale forever.

3. Read the actual changes and save progress

The worker loads the saved change token for that list and calls SharePoint’s GetChanges API. It processes the returned changes, follows additional pages when needed, and stores the token for the last change it has safely handled.

For a status transition, I use the change record to identify the affected item, then retrieve the current item from SharePoint when I need its values. I apply the same validation and business rules as any other update path. A webhook notification is not proof that a request should be approved.

I make retries safe in the application database. For each processed change, I use a stable deduplication key based on the list and source change identity. I update the projection, write an outbox event if needed, and advance the saved token in one database transaction. If the worker fails before that commit, it can read the change again; if it fails after the commit, the outbox publisher can resume without repeating the state transition.

Here is an example of the small application event I might publish. It is an app-owned data contract, not the SharePoint notification payload or a complete Azure Event Grid envelope:

type RequestStatusChanged = {
  eventId: string;
  tenantId: string;
  siteId: string;
  listId: string;
  itemId: string;
  sourceChangeToken: string;
  status: "Submitted" | "ReadyForReview" | "Approved" | "Rejected";
};

I keep this event small. I don’t include a justification, access token, or other user-submitted content if a consumer only needs the request identifier and new status.

4. Fan out only when another consumer needs it

An outbox publisher can send the normalized event to Event Grid. Event Grid retries delivery and uses at-least-once delivery, so subscribers must tolerate duplicate events. I configure dead-lettering and monitor it when an event that cannot be delivered needs investigation or replay.

I keep the publisher separate from the change worker. If Event Grid is unavailable, the outbox remains pending and can be retried; SharePoint does not need to send the same change again just because a downstream consumer was unavailable.

Make the updated state visible without promising a chat push

The application projection now says that the request is ready for review. An MCP tool can read that projection when the user asks for the queue or requests the item’s status. The backend still checks who is asking and whether they can see that request.

I don’t assume the event can update a Copilot pane that is already open, start a new conversation, or notify a user through every Copilot client. Those are host-specific behaviors, separate from updating the backend. If people need an immediate alert, I add a supported notification channel for the target host and test it separately.

This distinction also helps with the user experience:

  • The reviewer sees the durable status, not a transient webhook message.
  • A retry does not create a second approval or duplicate queue entry.
  • A failed notification does not change who is allowed to approve.
  • Copilot reads current state when the user interacts; it does not become the event store.

Recover when something goes wrong

I design for repeated notifications, unavailable services, missed callbacks, and expired subscriptions from the start:

No

Yes

No

Yes

No

Yes

Scheduled reconciliation

Enqueue list sync

SharePoint notification

Known subscription and matching clientState?

Reject and log minimal diagnostic data

Was the durable job accepted?

Return failure so SharePoint can retry

Return success

Worker reads from saved change token

Changes and checkpoint committed?

Retry from the previous token

Publish pending outbox events

Current request state is available to authorized readers

The scheduled reconciliation is important. SharePoint can retry a notification only a limited number of times; querying from the last committed change token gives the application a way to recover when a callback was ultimately missed. If a token is no longer usable or a subscription has expired, I handle that as an explicit recovery path and verify the resulting state against SharePoint.

Verify the business outcome

For a demo, I would exercise the whole path with a test request and a user who has the intended permissions:

  1. Create a request, then change its status to Ready for review in SharePoint.
  2. Confirm the webhook is mapped to the expected tenant and list, the worker reads the change, and the approval queue shows the new state.
  3. Replay the same notification and confirm there is still one logical state transition and one queue entry.
  4. Stop the worker or downstream publisher briefly, then confirm the saved token and outbox let processing resume.
  5. Test a malformed or unknown subscription, a mismatched clientState, an expired subscription, and a notification that is never successfully delivered.
  6. Ask the MCP status tool for the request as both an authorized and unauthorized user; confirm it returns current state only to the authorized user.
  7. If Event Grid is enabled, test duplicate delivery, retry behavior, dead-letter handling, and replay independently of the SharePoint webhook.

An MCP Inspector check or a successful webhook callback proves only part of this design. It does not prove that the real Copilot host renders the experience, that the user has permission, or that a durable state change completed. I verify those boundaries separately.

Takeaway

Business events should keep the application current, not pretend that every Copilot surface supports an unsolicited UI update. For SharePoint, I treat the webhook as a prompt to read the change log, save progress durably, and make the resulting process state available to authorized tools.

I add Event Grid when I need event routing to independent consumers. I keep retries idempotent, subscription renewal visible, and the user-facing experience tied to the current business record.

References