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:
| Layer | Use it for | Do not treat it as |
|---|---|---|
| SharePoint webhook | Tell my service that a subscribed list may have changed. | A complete record of which fields changed. |
| SharePoint change log | Retrieve changes since my last saved change token. | My application's approval rules or current user permissions. |
| Application state | Store the request status and the durable approval queue. | A transient notification or a Copilot chat transcript. |
| Azure Event Grid | Optionally route normalized application events to other subscribers. | The request database or a guarantee of exactly-once processing. |
| MCP status tool | Read the current state when the user asks or opens the experience. | A background trigger that starts or updates an arbitrary chat. |
- 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.
- 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.
- 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.
- 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.
- 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.
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:
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:
- Create a request, then change its status to Ready for review in SharePoint.
- Confirm the webhook is mapped to the expected tenant and list, the worker reads the change, and the approval queue shows the new state.
- Replay the same notification and confirm there is still one logical state transition and one queue entry.
- Stop the worker or downstream publisher briefly, then confirm the saved token and outbox let processing resume.
- Test a malformed or unknown subscription, a mismatched
clientState, an expired subscription, and a notification that is never successfully delivered. - 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.
- 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.