#SharePointProblems | Koskila.net

Solutions are worthless unless shared! Antti K. Koskela's Personal Professional Blog

Grounding Copilot agents: files or connectors?

Reading Time 9 min
Word Count 1534 words
Comments 0 comments
View

This is part 2 of Baby Steps to Copilot Mastery, my multi-part series on useful declarative Microsoft 365 Copilot agents. In part 1, we looked at why you might choose to build one instead of using Copilot Studio or going all-in with a "real" custom code solution.

This time, let's give the agent something useful to talk about 😆

We'll look at SharePoint and files, when to use a Copilot Connector, and what our support-ticket demo actually puts into the index. Including the permission setting you absolutely should not copy into a customer project without thinking about it first!

Background

The first demo in our HELish Summit session with Gautam covered both sides of grounding (even if VERY superficially!): Building a custom Copilot Connector to ingest support tickets, and declaring an agent that could find and explain them. No email sending, no closing issues, no write operations (yet!)

We could have used a gallery connector instead. The custom connector was there to make the demo a little spicier and let us show the ingestion code. You don't actually need it to build a declarative agent 😅

Start with the information you already have

For policies, handbooks, project documents, and other material already living within your Microsoft 365 tenant, adding the relevant SharePoint sites or files may be enough. Overcomplicating the solution is rarely worth the extra effort, and giving Copilot too many options on what to do will just serve to confuse it.

What I mean to say is that you need to scope the source to the task.

An agent that explains one product's support procedures does not need every document your company has ever accumulated. Especially not the seven slightly different copies of the same procedure, even if one of them is called "_final.docx" and the other one is "_final_v2.docx".

SharePoint content is not static, of course, but retrieval and indexing still affect when changes become visible.

And keep the permissions straight: pointing an agent at a source does not grant its users access to that source.

When the information lives elsewhere

For an external ticketing system, knowledge base, or other repository, check the connector gallery first. If a supported connector handles your content, synchronization, and access model, configure it. Building (and especially maintaining) a crawler is not something you should do "just because".

Anyway - this specific article focuses on synced connectors: they ingest external content into the Microsoft 365 Substrate through Microsoft Graph, populating the data in Microsoft Search's index. That gives Microsoft Search - and hence, Copilot - something to retrieve. It is not a live call back to your ticketing system for every answer, and neither does it give the agent write access to that system.

I want to stress: Only build a custom connector when the gallery doesn't cover the source or the required ingestion behavior. You then own three fairly concrete things:

  1. Connection and schema: identify the source and describe the properties the index will store.
  2. Content: transform and ingest records, then keep them current.
  3. Permissions: represent who may see each record, including changes to that access over time.

The agent stays declarative. No backend, and (technically) no code to maintain! But the connector does not magically write itself, unfortunately.

What our sample actually does

Because we chose the custom route for the demo, the connector project contains an Azure Functions-based TypeScript ingestion process and a separate declarative agent definition. With a suitable gallery connector, you would configure that connection instead of maintaining this ingestion code yourself.

The reader loads the ticket records and, for an incremental crawl, yields only tickets whose dates.updatedDateTime is newer than the supplied crawl timestamp. Replacing that fixture with a real ticketing API is a customization point, not something the sample has already implemented for you.

The mapper then produces a Graph externalItem for each ticket:

  • The ticket ID becomes the external item ID.
  • Title, description, customer, status, priority, and ownership become properties.
  • The nested source dates become flat date properties.
  • A text body combines the useful ticket details for retrieval.
  • The ticket's link becomes the source URL, through the schema's url label.
  • An ACL determines who can retrieve the item.

Notice that this is an indexed representation of the ticket. The external item ID and the number at the end of a GitHub issue URL are different identifiers, even when the demo data makes them look suspiciously related. We'll need that distinction when we start changing issues (a topic for another day, I suppose!)

The schema is part of the behavior

The sample labels title as the title, link as the URL, and updatedDateTime as the last-modified date. These labels help Microsoft 365 interpret the content.

Other property settings answer different questions:

Setting What it enables
isSearchable Full-text matching against the property's contents
isQueryable Queries that address that property specifically
isRetrievable Returning the property in search results
isRefinable Using the property for refiners or aggregations

Those settings are not interchangeable, and not every combination is supported for every property type. Use the current schema guidance when designing a new connection rather than copying every flag from a demo.

Think about the questions the agent must answer. "Which tickets are due soon?" needs a usable due date. "Who should own this escalation?" needs more than an engineer's display name. Since our agent is declarative, we can't just code our way out of the problem - we need to give Copilot enough, so it can figure the right workflow out itself!

The fixture includes engineer and account-owner email fields for a reason.

An API or source data containing a field does not, by itself, make it useful to Copilot. It needs to survive mapping, indexing, retrieval, and the agent's interpretation.

Tell the agent which connection to use

This is the relevant capability block from the sample's agent definition:

{
  "capabilities": [
    {
      "name": "GraphConnectors",
      "connections": [
        {
          "connection_id": "InternalSupportTickets"
        }
      ]
    }
  ]
}

This scopes the declared connector knowledge to that connection - but the connection has to already exist and work properly!

(If you need a refresher, my earlier article explains how to properly set one up: introduction to Copilot Connectors in Microsoft Search)

The agent's instructions tell the agent to ground ticket-specific claims in connector results, preserve source links, and distinguish facts from recommendations. They also define active statuses: Open, In Progress, Pending Customer, and Pending Vendor. Resolved and Closed are treated as inactive.

Without that definition, "show active tickets" leaves the model to decide what your business means by active. And trust me - it would be unwise to delegate your status taxonomy to Copilot's imagination... 😅

The instructions have further guardrails, althought it's useful to remember that the agent will keep disobeying them. Instructions can never function as security directives - they only guide the agent to the right outcome.

Anyway - they prohibit claiming to update, assign, or close tickets to limit the hallucinations. This agent has read-only knowledge access.

About that demo permission setting...

The sample's ACL function returns this grant for every ticket:

{
  "accessType": "grant",
  "type": "everyone",
  "value": "everyone"
}

That is tenant-wide access, not anonymous access to the public internet. It is still much too broad if your tickets contain customer-confidential information or material restricted to particular teams. But a demo is a demo, and for a demo this shortcut was enough... 🙈

Before ingesting real data, replace this with ACLs that reflect the source system. Handle changes to users, groups, permissions, and deleted records as part of synchronization. Don't assume that restricting the agent's instructions fixes an over-permissive index.

Indexed knowledge or a live lookup?

Use indexed knowledge to find relevant tickets, summarize their descriptions, and discover earlier incidents. Use an appropriate live API operation when the task depends on the current state of a specific record.

For example, a connector result might say a ticket was resolved during the last crawl. Before closing its backing GitHub issue, the action agent should read that issue from GitHub. The index is a snapshot, and its freshness depends on the connector.

Try one ticket before trying everything

Pick a ticket whose contents and access rules you know. Verify that it reaches the connection, that search can retrieve it for an authorized user, and that someone without access cannot retrieve it. Then ask the agent about it and inspect the citation.

After that, change a field, advance its update timestamp, ingest it again, and check when the new value becomes searchable. That gives you a much more useful baseline than asking Copilot to "summarize latest support activity" and hoping for the best.

For the actual Graph requests, my Copilot Connectors in Microsoft Search article is a useful next read. The sample repository provides the local setup; don't point it at production tickets until you've replaced the demo ACL.

Next in this series, on October 13 (ish): giving the agent Microsoft 365 tools and a small GitHub API plugin.

See, finding the ticket was the easy part. Next we get to be careful about changing things. 👻

References

Comments

Interactive comments not implemented yet. Showing legacy comments migrated from WordPress.

No comments yet.

Whitewater Magpie Ltd.
•
© 2026
Static Site Generation timestamp: 2026-10-06T06:56:59Z