Embedding in applications
A persisted View can be opened with no application code, embedded as a React component, or executed directly through the client. Applications can also contribute Views in code so they install alongside the app.
The generic route
The generic Studio route renders a persisted View without application-specific page code:
/view/sales-orders
Use it for internal tools, quick links, and anywhere you do not need custom page chrome.
The ViewExperience component
Most applications embed ViewExperience, which loads the authoritative configuration, executes the View, renders
navigation and results, manages pagination, and synchronizes state with the URL. Inside a Vertesia session it
self-fetches — pass only a viewId:
Sales Order Console
Search orders and browse them by geography or collection.
SalesOrdersPage.tsx
import type { ViewHit } from '@vertesia/common';
import { ViewExperience } from '@vertesia/ui/features';
import { useNavigate } from '@vertesia/ui/router';
export function SalesOrdersPage() {
const navigate = useNavigate();
const openSalesOrder = (hit: ViewHit) => {
navigate(`/store/objects/${encodeURIComponent(hit.id)}`, { isBasePathNested: false });
};
return <ViewExperience viewId="sales-orders" onOpenHit={openSalesOrder} />;
}
ViewExperience uses client.views.execute from the active Vertesia session. Outside a session provider, supply
an execute function:
EmbeddedSalesOrders.tsx
<ViewExperience
viewId="sales-orders"
execute={(request) => client.views.execute('sales-orders', request)}
showHeader={false}
/>
Built-in system Views
Two built-in Views are available without creating a project resource:
| ID | Purpose |
|---|---|
sys:ContentLibrary | Deterministic content browsing with folder, collection, type, and status navigation; list, table, and card displays; selection, export/delete, and upload-on-drop. |
sys:AgenticDocumentExplorer | Demonstrates natural-language Elasticsearch query, adaptive presentation planning, bounded result reranking, and why-match guidance over the always-available sys:GenericDocument type, while retaining folder and collection navigation. |
Open them through the generic route (the colon may be URL-encoded) or embed them exactly like a persisted View:
<ViewExperience viewId="sys:AgenticDocumentExplorer" onOpenHit={openContent} />
The agentic system View inherits the project model configured for the Content Search system interaction and reuses it
for reranking. It does not pin a model or environment. It scopes results to sys:GenericDocument; try queries such as
compare contracts about termination clauses, show documents by Alice as cards, or
group presentations updated last month. System Views are
read-only in-code definitions; create a project View when you need to customize their configuration.
Execute directly
The underlying content API is POST /api/v1/view-executions/:id/execute. Clients send only user-controlled state, never a
complete Elasticsearch request:
Execute directly
const result = await client.views.execute('sales-orders', {
query: 'orders delayed by weather',
key_terms: { customer: ['Acme'] },
navigation: { sold_to_state: ['CA', 'NV'], status: ['open'] },
navigation_queries: { collections: 'renewals' },
display: 'table',
sort: 'recent',
offset: 0,
limit: 25,
});
The response contains the authoritative View definition, normalized content hits, total count, navigation nodes and counts, the applied display and sort, search mode and warnings, and execution time.
Shareable deep links
With URL synchronization enabled, ViewExperience serializes the user-visible state:
| Parameter | Meaning |
|---|---|
q | Natural-language or deterministic text query |
t.<id> | Structured key-term value |
n.<id> | Navigation selection |
nq.<id> | Server-side navigation-node filter, currently supported for collections |
display | Configured display ID |
sort | Configured sort ID |
offset | Result offset |
Multiple values use repeated parameters. For example:
/view/sales-orders?q=orders+delayed+by+weather&t.customer=Acme&n.sold_to_state=CA&nq.collections=renewals&display=table&sort=recent
The component preserves unrelated host query parameters and responds to browser back/forward navigation. After execution it removes stale key-term or navigation IDs and uses the display and sort accepted by the authoritative runtime. Generated Elasticsearch DSL and security filters are never placed in the URL.
Set syncUrl={false} for a preview, modal, or embedded surface that should not modify the host route.
Custom renderers
The built-in renderers cover common search, navigation, and result layouts. An embedded application can replace any of them with a typed React component:
- Set a stable
renderername onsearch, a navigation item, or a result display. - Pass components under matching names in the
renderersregistry. - Keep data access in the View runtime; custom renderers receive normalized state and callbacks.
First, declare the renderer names in the View configuration:
sales-order-view.ts
import type { ViewExperienceConfiguration } from '@vertesia/common';
export const salesOrderView = {
name: 'Sales orders',
description: 'Find and manage sales orders.',
search: {
mode: 'deterministic',
renderer: 'sales-order-search',
},
navigation: [
{
id: 'status',
label: 'Status',
source: 'terms',
field: 'properties.status',
renderer: 'status-pipeline',
},
],
results: {
default_display: 'cards',
displays: [
{
id: 'cards',
label: 'Cards',
type: 'cards',
renderer: 'sales-order-cards',
title: { field: 'name' },
},
],
},
} satisfies ViewExperienceConfiguration;
Then register components under those exact names in the application:
Renderer registry
import type {
ViewNavigationRendererProps,
ViewResultsRendererProps,
ViewSearchRendererProps,
} from '@vertesia/ui/features';
import { ViewExperience } from '@vertesia/ui/features';
function SalesOrderSearch(props: ViewSearchRendererProps) {
// Render inputs, then call props.onQueryChange and props.onSubmit.
return <MySalesOrderSearch {...props} />;
}
function StatusPipeline(props: ViewNavigationRendererProps) {
// Render props.result.nodes, then call props.onChange with selected node IDs.
return <MyStatusPipeline {...props} />;
}
function SalesOrderCards(props: ViewResultsRendererProps) {
// Render props.result.hits and use props.onOpenHit for host navigation.
return <MySalesOrderCards {...props} />;
}
const renderers = {
search: { 'sales-order-search': SalesOrderSearch },
navigation: { 'status-pipeline': StatusPipeline },
results: { 'sales-order-cards': SalesOrderCards },
};
export function SalesOrders() {
return <ViewExperience viewId="sales-orders" renderers={renderers} />;
}
Persisted configuration stores renderer names, not executable code. When a renderer name is missing from the
application registry — including on the generic /view/<id> route — the component safely falls back to the built-in
renderer for that section. Treat renderer names as an application compatibility contract: deploy the new renderer
before updating a persisted View to reference it, and keep the old name registered while older View revisions or deep
links may still use it.
Actions and drop contributions
The built-in result renderer supports single or multiple selection. Persisted View JSON may enable the standard
export and delete actions, and may declare app action metadata by a stable handler name. The application supplies the
executable handler through contributions.actions:
Contributed View actions
import { ViewExperience } from '@vertesia/ui/features';
export function SalesOrders() {
return (
<ViewExperience
viewId="sales-orders"
contributions={{
actions: {
'approve-orders': {
isAvailable: ({ result }) => result.total > 0,
run: async ({ action, hits, refresh, clearSelection }) => {
await approveOrders({
ids: hits.map((hit) => hit.id),
status: action.params?.status,
});
clearSelection();
await refresh();
},
},
},
}}
/>
);
}
The contribution receives normalized hits, the authoritative View definition and result, the current request, and
refresh/clearSelection callbacks. No executable code is stored in the View. Custom result renderers receive the
selection controller in props.selection and can use the exported useViewActions hook for app-specific action UI.
Persisted JSON supports one drop handler: the standard upload flow. For any other behavior, contribute drop code
directly from the application:
Code-only custom drop
<ViewExperience
viewId="case-inbox"
contributions={{
drop: {
run: async ({ files, definition, request, refresh }) => {
await importCaseBundle({ files, view: definition.name, query: request.query });
await refresh();
},
},
}}
/>
A code contribution works even when the View has no results.drop entry and overrides the built-in upload target
when both are present. Because it is application code, the app is responsible for checking any domain-specific
permissions. The generic /view/<id> route has no app contribution registry, so it only runs the persisted built-in
upload behavior.
In-code (app-contributed) Views
An application or plugin can ship a View in its own code so it installs with the app, instead of being persisted
through the Studio API. Declare InCodeViewDefinition entries in the tool server's views configuration:
plugin: an in-code View
import type { InCodeViewDefinition } from '@vertesia/common';
export const documentLibraryView = {
id: 'document-library',
name: 'document_library',
title: 'Document Library',
description: 'Browse and search indexed documents by type with full-text search.',
definition: {
name: 'Document Library',
navigation: [{ id: 'type', label: 'Type', source: 'terms', field: 'type.name' }],
search: { mode: 'deterministic' },
results: {
default_display: 'list',
displays: [{ id: 'list', label: 'List', type: 'list', title: { field: 'name' } }],
},
},
} satisfies InCodeViewDefinition;
When the app is installed, Studio resolves the View as app:<app-name>:<view-id> — for example
app:my-plugin:document-library. It renders through the same /view/<id> route and ViewExperience component as a
persisted View; the app-scoped ID is the only difference:
<ViewExperience viewId="app:my-plugin:document-library" />
App-contributed Views are read-only at runtime: edit them in the app's source, not through the Studio CRUD API.
