Hi @bjarnef
I can’t see anything in the docs about customising the collection view to add your own filters. It looks like you have to create your own workspace view if you want to achieve that.
I asked AI to come up with a basic example:
Here’s a basic but complete workspace view scoped to your collection’s doc type, with filter controls, a custom management API endpoint querying Examine, and a table.
I’ll show the four pieces: the manifest, the Lit view element, the C# controller, and a note on wiring. Let me keep the client code TypeScript-flavoured since that’s the backoffice norm.
1. The manifest
Register a workspaceView against the document workspace, then narrow it to your specific collection doc type with WorkspaceContentTypeAlias. Without that second condition it shows on every document.
typescript
// umbraco-package.json (or registered via your entry point)
import { UMB_WORKSPACE_CONDITION_ALIAS } from "@umbraco-cms/backoffice/workspace";
const manifest: UmbExtensionManifest = {
type: "workspaceView",
alias: "My.WorkspaceView.FilteredCollection",
name: "Filtered Collection Workspace View",
element: () => import("./filtered-collection-view.element.js"),
weight: 250, // higher weight = further left in the tab strip
meta: {
label: "Items",
pathname: "items",
icon: "icon-list",
},
conditions: [
{ alias: UMB_WORKSPACE_CONDITION_ALIAS, match: "Umb.Workspace.Document" },
{ alias: "Umb.Condition.WorkspaceContentTypeAlias", match: "myCollectionDocType" },
],
};
export const manifests = [manifest];
The WorkspaceContentTypeAlias condition is confirmed in the 17 docs — it requires the current workspace to be based on a Content Type whose Alias matches the one specified. Combined with the Umb.Workspace.Document workspace condition, that pins the view to exactly your collection parent. Umbraco
2. The Lit view element
This is the core of it. It grabs the current node’s key from the document workspace context (so it can pass the parent id to your endpoint), renders your filter controls, calls your custom endpoint, and renders a table. I’m using the UUI components since they match the backoffice look and give you sorting/inputs for free.
import { css, customElement, html, state } from "@umbraco-cms/backoffice/external/lit";
import { UmbLitElement } from "@umbraco-cms/backoffice/lit-element";
import { UMB_DOCUMENT_WORKSPACE_CONTEXT } from "@umbraco-cms/backoffice/document";
import { tryExecute } from "@umbraco-cms/backoffice/resources";
interface FilteredItem {
key: string;
name: string;
status: string;
updateDate: string;
}
@customElement("filtered-collection-view")
export class FilteredCollectionViewElement extends UmbLitElement {
@state() private _parentKey?: string;
@state() private _items: FilteredItem[] = [];
@state() private _loading = false;
// filter state
@state() private _status = "";
@state() private _query = "";
constructor() {
super();
this.consumeContext(UMB_DOCUMENT_WORKSPACE_CONTEXT, (ctx) => {
// observe the unique (the node's GUID key) so we know which parent to query
this.observe(ctx?.unique, (unique) => {
this._parentKey = unique ?? undefined;
if (this._parentKey) this.#load();
});
});
}
async #load() {
if (!this._parentKey) return;
this._loading = true;
const params = new URLSearchParams({
parentKey: this._parentKey,
status: this._status,
query: this._query,
});
// tryExecute wraps fetch with the backoffice auth + error handling
const { data } = await tryExecute(
this,
fetch(`/umbraco/management/api/v1/filtered-collection?${params}`, {
headers: { "Content-Type": "application/json" },
}).then((r) => r.json())
);
this._items = data?.items ?? [];
this._loading = false;
}
#onStatusChange(e: Event) {
this._status = (e.target as HTMLSelectElement).value;
this.#load();
}
#onQueryInput(e: InputEvent) {
this._query = (e.target as HTMLInputElement).value;
// debounce in real code; immediate here for brevity
this.#load();
}
override render() {
return html`
<umb-body-layout>
<uui-box>
<div class="filters">
<uui-input
label="Search"
placeholder="Search…"
.value=${this._query}
@input=${this.#onQueryInput}>
</uui-input>
<uui-select
label="Status"
.value=${this._status}
@change=${this.#onStatusChange}>
<option value="">All statuses</option>
<option value="active">Active</option>
<option value="archived">Archived</option>
</uui-select>
</div>
${this._loading
? html`<uui-loader></uui-loader>`
: this.#renderTable()}
</uui-box>
</umb-body-layout>
`;
}
#renderTable() {
if (!this._items.length) return html`<p>No items match.</p>`;
return html`
<uui-table>
<uui-table-head>
<uui-table-head-cell>Name</uui-table-head-cell>
<uui-table-head-cell>Status</uui-table-head-cell>
<uui-table-head-cell>Updated</uui-table-head-cell>
</uui-table-head>
${this._items.map(
(item) => html`
<uui-table-row>
<uui-table-cell>
<a href=${`/umbraco/section/content/workspace/document/edit/${item.key}`}>
${item.name}
</a>
</uui-table-cell>
<uui-table-cell>${item.status}</uui-table-cell>
<uui-table-cell>${item.updateDate}</uui-table-cell>
</uui-table-row>
`
)}
</uui-table>
`;
}
static override styles = css`
.filters {
display: flex;
gap: var(--uui-size-space-4);
margin-bottom: var(--uui-size-space-5);
}
`;
}
export default FilteredCollectionViewElement;
A couple of notes on the context bits, because these are the parts that change between versions and are easy to get wrong. The 24 Days In Umbraco 2025 article confirms that if you only need to consume the document workspace context, the token is UMB_DOCUMENT_WORKSPACE_CONTEXT (there’s also UMB_CONTENT_WORKSPACE_CONTEXT if you need the broader content abstraction). I’m reading unique off it for the node key — confirm the exact observable name against your installed version’s typings, as the content/document context split has shifted recently. 24days
3. The management API controller
This is where your Examine query lives. Inherit from the management API base so it sits under /umbraco/management/api, picks up backoffice auth, and shows in the Swagger UI.
using Asp.Versioning;
using Examine;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Umbraco.Cms.Api.Management.Controllers;
using Umbraco.Cms.Api.Management.Routing;
using Umbraco.Cms.Core.Security.Authorization;
[ApiController]
[VersionedApiBackOfficeRoute("filtered-collection")]
[ApiExplorerSettings(GroupName = "Filtered Collection")]
[Authorize(Policy = AuthorizationPolicies.SectionAccessContent)]
public class FilteredCollectionController : ManagementApiControllerBase
{
private readonly IExamineManager _examineManager;
public FilteredCollectionController(IExamineManager examineManager)
=> _examineManager = examineManager;
[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(typeof(FilteredCollectionResponse), StatusCodes.Status200OK)]
public IActionResult Get(
[FromQuery] Guid parentKey,
[FromQuery] string? status = null,
[FromQuery] string? query = null,
[FromQuery] int skip = 0,
[FromQuery] int take = 100)
{
if (!_examineManager.TryGetIndex("InternalIndex", out var index))
return NotFound("InternalIndex not available");
var searcher = index.Searcher;
var criteria = searcher.CreateQuery("content");
// children of this parent — parentID is indexed in InternalIndex
var q = criteria.Field("parentID", parentKey.ToString());
if (!string.IsNullOrWhiteSpace(status))
q = q.And().Field("status", status); // your child property alias
if (!string.IsNullOrWhiteSpace(query))
q = q.And().GroupedOr(new[] { "nodeName", "status" }, query.Trim());
var results = q.Execute(new QueryOptions(skip, take));
var items = results.Select(r => new FilteredItem(
Key: Guid.TryParse(r.Id, out var g) ? g : Guid.Empty,
Name: r.Values.GetValueOrDefault("nodeName") ?? "",
Status: r.Values.GetValueOrDefault("status") ?? "",
UpdateDate: r.Values.GetValueOrDefault("updateDate") ?? ""
)).ToArray();
return Ok(new FilteredCollectionResponse(items, (int)results.TotalItemCount));
}
}
public record FilteredItem(Guid Key, string Name, string Status, string UpdateDate);
public record FilteredCollectionResponse(FilteredItem[] Items, int Total);
I’ve not tried this but it should get you most of the way there.
Justin