Handle intents in the Structure Tool
How the Structure Tool matches intents to panes, and how to declare intent handling on lists that don't match on their own.
Intents are Studio's internal routing mechanism. When a user clicks a search result, follows an Open in Studio link from Visual Editing, or uses a Create new button, Studio fires an intent (like edit or create) with parameters such as the document ID and type. The Structure Tool resolves that intent by finding the right pane in your structure.
This guide explains how Studio decides which pane handles an intent, and shows how to declare intent handling on lists that don't match on their own.
Prerequisites
- A studio with the Structure Tool installed. New projects include it; for existing projects, install it by updating your project's configuration file.
- Familiarity with panes, lists, and child resolvers, as covered in Structure tool and Structure builder.
How Studio matches an intent to a pane
Studio matches an intent to a pane in two independent ways. Either one is enough.
First, a pane can declare what it handles with canHandleIntent. Lists built with S.documentTypeList() and S.documentTypeListItem() get a default implementation that reads the schema types named in the list's filter and matches when the intent's type parameter is one of them. Any filter that names a type counts, such as _type == "post" && defined(publishedAt) — not only the default filter.
Second, the intent resolver checks the pane directly, whatever its canHandleIntent says. A documentList matches when its schema type equals the intent's type parameter and its filter is still exactly _type == $type.
Panes that match neither way need canHandleIntent to declare which intents they handle: a custom S.list(), or a documentList whose filter doesn't name a schema type.
Adding a custom .child() resolver to a documentTypeList clears the default canHandleIntent, because Studio can't guarantee the new child handles the intent. The list keeps matching through the second route anyway, since .child() changes neither the schema type nor the filter. Change the filter as well and both routes drop out, so you have to declare canHandleIntent yourself.
What .child() does change is what opens. The intent routes to a pane for the target document ID, and your resolver decides what that pane shows. If it returns a list instead of a document node, the document editor doesn't open, and canHandleIntent won't change that.
Common symptoms of missing intent handling
If a list in your structure matches neither way, and you notice any of these, missing canHandleIntent could be the cause:
- Documents open in the wrong pane. The document opens as a bare editor instead of navigating to the correct location in your structure.
- Open in Studio links from Visual Editing don't route correctly.
- Search results land in the wrong place. Global search can bypass your custom structure entirely.
- Create new buttons may not work as expected. Custom structures that replace
documentTypeListcan lose the built-in create intent handling. AdocumentTypeListthat keeps its default filter and adds a.child()resolver has a different cause: the intent routes correctly, but the child resolver decides what opens.
When no pane in your structure matches, Studio opens the document in a fallback editor outside your structure. You can recognize it by the pane ID in the URL, which starts with __edit__.
Add intent handling
Add canHandleIntent to any list that matches neither way. The function receives the intent name and parameters, and returns true if the pane should handle the intent:
// structure.ts
import type {StructureResolver} from 'sanity/structure'
export const structure: StructureResolver = (S) =>
S.list()
.title('Content')
.items([
// Built-in documentTypeList: intent handling works automatically
S.documentTypeListItem('author').title('Authors'),
// Custom child resolver: needs canHandleIntent
S.listItem()
.title('Blog Posts')
.schemaType('post')
.child(
S.documentTypeList('post')
.title('Blog Posts')
.child((documentId) =>
S.document()
.documentId(documentId)
.schemaType('post')
)
.canHandleIntent((intentName, params) =>
['create', 'edit'].includes(intentName) && params.type === 'post'
)
),
])Custom lists need intent handling
A custom S.list(), or a documentList whose filter doesn't name a schema type, matches no intents by default. Without canHandleIntent, search results, Visual Editing links, and Create new buttons open the fallback editor instead of your structure.
For the full canHandleIntent signature and parameters, see the Structure Builder API Reference.
Next steps
- Create Studio edit intent links: build the URLs that fire an
editintent, including the ones the Vision Tool renders next to_idand_refvalues. - Get started with Structure Builder API: build the lists, panes, and child resolvers that intent handling applies to.