---
title: "Embedding in applications"
source: "https://docs.vertesiahq.com/content/view-experiences/embedding"
markdown: "https://docs.vertesiahq.com/llms/content/view-experiences/embedding.md"
---

# 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:

```text
/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`:


```tsx {{ title: '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:

```tsx {{ title: '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:

```tsx
<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:

```typescript {{ title: '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.` | Structured key-term value |
| `n.` | Navigation selection |
| `nq.` | 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:

```text
/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:

1. Set a stable `renderer` name on `search`, a navigation item, or a result display.
2. Pass components under matching names in the `renderers` registry.
3. Keep data access in the View runtime; custom renderers receive normalized state and callbacks.

First, declare the renderer names in the View configuration:

```typescript {{ title: '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:

```tsx {{ title: '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/` 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`:

```tsx {{ title: '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:

```tsx {{ title: '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/` 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:

```typescript {{ title: '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/` route and `ViewExperience` component as a
persisted View; the app-scoped ID is the only difference:

```tsx
<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.