# How Plane's Custom Views and Filtering System Works Under the Hood: A Technical Deep Dive

> Explore how Plane's custom Views and filtering system work. Learn about persistent JSON filter trees and the reactive UI powered by a MobX tree.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: deep-dive
- Published: 2026-06-23

---

**Plane's custom views persist filter trees as compact JSON in `rich_filters`, using a bidirectional `WorkItemFiltersAdapter` to convert between the external storage format and an internal MobX tree that powers the reactive UI.**

Plane's custom views and filtering system in the `makeplane/plane` repository provides a sophisticated way to save, share, and dynamically update filtered perspectives of work items. The implementation centers on three tightly-coupled layers: a domain model defining the data shape, a state management layer handling format conversion, and a service layer managing persistence. This architecture enables complex, nested filtering logic while maintaining a clean separation between the stored JSON representation and the reactive UI state.

## The Three-Layer Architecture

The system is organized into distinct layers that handle specific responsibilities:

- **Domain Model**: Defines the shape of views and filter expressions in [`packages/types/src/views.ts`](https://github.com/makeplane/plane/blob/main/packages/types/src/views.ts) and [`packages/types/src/view-props.ts`](https://github.com/makeplane/plane/blob/main/packages/types/src/view-props.ts). Key types include `IProjectView`, `TWorkItemFilterExpression`, and `IIssueDisplayFilterOptions`.
- **State & Conversion**: Manages the current filter tree in the UI and handles conversion between formats. Implemented in [`packages/shared-state/src/store/work-item-filters/adapter.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/store/work-item-filters/adapter.ts) and [`filter.store.ts`](https://github.com/makeplane/plane/blob/main/filter.store.ts).
- **Service & UI**: Persists views via REST API and provides React hooks for interaction. Found in [`packages/services/src/workspace/view.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/workspace/view.service.ts) and various hooks under `apps/web/core/hooks/store/`.

## Data Model and Filter Schema

At the core of Plane's custom views is the `IProjectView` interface, which defines the structure of a saved view including its filter expression and display preferences.

In [`packages/types/src/views.ts`](https://github.com/makeplane/plane/blob/main/packages/types/src/views.ts), the view model is defined as:

```typescript
export interface IProjectView {
  id: string;
  name: string;
  description: string;
  rich_filters: TWorkItemFilterExpression;   // saved filter tree
  display_filters: IIssueDisplayFilterOptions;
  display_properties: IIssueDisplayProperties;
  query: IIssueFilterOptions;                // legacy flat filter params
  // …other meta fields
}

```

The **`rich_filters`** field stores the **compact external representation**—a JSON object where each key encodes `property__operator` and the value contains the filter criteria. This format is optimized for database storage and API transmission, differing from the tree structure used by the UI components.

## Converting Between External and Internal Filter Trees

The `WorkItemFiltersAdapter` class in [`packages/shared-state/src/store/work-item-filters/adapter.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/store/work-item-filters/adapter.ts) serves as the translation layer between the compact JSON stored in the database and the hierarchical tree required by the UI.

```typescript
class WorkItemFiltersAdapter extends FilterAdapter<TWorkItemFilterProperty, TWorkItemFilterExpression> {
  // external → internal
  toInternal(external: TWorkItemFilterExpression) {
    if (!external || isEmpty(external)) return null;
    return this._convertExpressionToInternal(external);
  }

  // internal → external
  toExternal(internal: TFilterExpression<TWorkItemFilterProperty>) {
    if (!internal) return {};
    return this._convertExpressionToExternal(internal);
  }
}

```

The **`toInternal`** method parses the external JSON and constructs MobX-ready nodes using `createConditionNode` and `createAndGroupNode`, recognizing whether a node represents a simple condition or an `AND` group. Conversely, **`toExternal`** serializes the UI tree back to the compact JSON format for persistence.

## Building Filter Expressions from UI Conditions

When users interact with the filter UI, their selections are captured as an array of `TWorkItemFilterCondition` objects. The system converts these into the storable JSON format using `buildWorkItemFilterExpressionFromConditions` in [`packages/shared-state/src/utils/work-item-filters.helper.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/utils/work-item-filters.helper.ts):

```typescript
export const buildWorkItemFilterExpressionFromConditions = (
  params: Omit<TBuildFilterExpressionParams<...>, "adapter">
): TWorkItemFilterExpression | undefined => {
  const workItemFilterExpression = buildTempFilterExpressionFromConditions({
    ...params,
    adapter: workItemFiltersAdapter,
  });
  if (!workItemFilterExpression) console.error("Failed to build work item filter expression");
  return workItemFilterExpression;
};

```

This utility delegates to the generic helper in [`packages/shared-state/src/utils/rich-filter.helper.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/utils/rich-filter.helper.ts), which is shared between rich issue filters and work-item filters, ensuring consistency across the application.

## Persisting Views via the API Service

The `WorkspaceViewService` class in [`packages/services/src/workspace/view.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/workspace/view.service.ts) provides a thin wrapper around the REST API endpoints for view management:

```typescript
class WorkspaceViewService extends APIService {
  async create(workspaceSlug: string, data: Partial<IWorkspaceView>) {
    return this.post(`/api/workspaces/${workspaceSlug}/views/`, data)
      .then(r => r?.data)
      .catch(e => { throw e?.response?.data; });
  }
  // update, list, retrieve, destroy …
}

```

When creating or updating a view, the service receives the `rich_filters` object that was built by the helper utilities. The server stores this JSON verbatim without schema validation on the frontend, enabling flexible filter definitions that can evolve with the product.

## React Integration and State Management

Plane leverages MobX for reactive state management, exposing stores through custom React hooks. The `useProjectView` hook in [`apps/web/core/hooks/store/use-project-view.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/hooks/store/use-project-view.ts) provides access to the project-view store, while `useWorkItemFilters` in [`apps/web/core/hooks/store/work-item-filters/use-work-item-filters.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/hooks/store/work-item-filters/use-work-item-filters.ts) exposes the filter store.

The modal component in [`apps/web/core/components/views/modal.tsx`](https://github.com/makeplane/plane/blob/main/apps/web/core/components/views/modal.tsx) demonstrates the integration pattern:

```tsx
const { createView, updateView } = useProjectView();
const { resetExpression } = useWorkItemFilters();

// On submit:
if (!data) await createView(workspaceSlug, projectId, payload);
else {
  const viewDetails = await updateView(workspaceSlug, projectId, data.id, payload);
  resetExpression(EIssuesStoreType.PROJECT_VIEW, viewDetails.id, viewDetails.rich_filters);
}

```

Both stores are **MobX observable** objects; components reading `workItemFilters.expression` automatically re-render when the view updates, eliminating the need for manual subscription management.

## End-to-End Workflow Example

The complete lifecycle of a custom view follows this sequence:

1. **Retrieval**: `WorkspaceViewService.retrieve` fetches the view JSON including `rich_filters`.
2. **Conversion**: The adapter's `toInternal` method transforms the saved JSON into an internal filter tree.
3. **Rendering**: UI components read from the MobX store to display current filter states.
4. **Mutation**: User changes trigger store actions that mutate the tree reactively.
5. **Serialization**: On save, `buildWorkItemFilterExpressionFromConditions` converts the tree back to external JSON.
6. **Persistence**: `WorkspaceViewService.update` transmits the JSON to the backend.

## Summary

- **Plane's custom views** combine filter trees (`rich_filters`) with display settings to create persistent, shareable work item perspectives.
- **Bidirectional conversion** between compact JSON and internal trees is handled by `WorkItemFiltersAdapter` in [`packages/shared-state/src/store/work-item-filters/adapter.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/store/work-item-filters/adapter.ts).
- **State management** uses MobX stores exposed via React hooks like `useWorkItemFilters` and `useProjectView`.
- **Persistence layer** relies on `WorkspaceViewService` to communicate with REST endpoints at `/api/workspaces/${slug}/views/`.
- **Filter building** utilities in [`work-item-filters.helper.ts`](https://github.com/makeplane/plane/blob/main/work-item-filters.helper.ts) bridge the gap between UI condition arrays and storable JSON expressions.

## Frequently Asked Questions

### What is the difference between `rich_filters` and `query` in Plane's view model?

The `rich_filters` field stores modern, hierarchical filter trees as `TWorkItemFilterExpression` objects, supporting nested logical groups and complex operators. The `query` field contains legacy flat filter parameters (`IIssueFilterOptions`) used for backward compatibility with older view implementations. New code should exclusively use `rich_filters` for filter storage and retrieval.

### How does Plane handle complex nested filters like "(Priority = High AND State = Todo) OR Assignee = Me"?

Plane represents nested logic using the internal `TFilterExpression` tree structure, where `createAndGroupNode` and `createConditionNode` build hierarchical representations. The `WorkItemFiltersAdapter` serializes these trees to flat JSON keys using the `__` delimiter (e.g., `priority__eq`, `state__eq`) combined with grouping logic, then deserializes them back to trees when loading views.

### Can developers create custom views programmatically outside the standard UI?

Yes, developers can instantiate `WorkspaceViewService` directly and pass manually constructed `rich_filters` objects. Using `buildWorkItemFilterExpressionFromConditions` with the `workItemFiltersAdapter` ensures proper format conversion, though the service accepts any valid `TWorkItemFilterExpression` JSON that matches the expected schema defined in [`packages/types/src/view-props.ts`](https://github.com/makeplane/plane/blob/main/packages/types/src/view-props.ts).

### How does the filter store synchronize across multiple components?

The `workItemFilters` store in [`packages/shared-state/src/store/work-item-filters/filter.store.ts`](https://github.com/makeplane/plane/blob/main/packages/shared-state/src/store/work-item-filters/filter.store.ts) is a MobX observable that maintains a single source of truth for the current filter expression. When `resetExpression` is called (as seen in [`apps/web/core/components/views/modal.tsx`](https://github.com/makeplane/plane/blob/main/apps/web/core/components/views/modal.tsx)), all subscribed components receive the update through MobX's reactive system, ensuring UI consistency without prop drilling or manual event emitters.