How Plane Page Hierarchies Support Nested Documents and AI Capabilities
Plane implements page hierarchies through a self-referencing Django model with cascade deletion, exposes nested document operations via REST endpoints filtering by parent UUID, and integrates AI capabilities through an optional aiHandler prop in the PageRenderer component that renders context-aware menus.
Plane (makeplane/plane) is an open-source project management platform that treats pages as first-class hierarchical documents. The architecture supports arbitrary-depth nesting through database-level parent-child relationships while enabling AI-powered editing features through a flexible handler interface in the frontend editor.
Database Architecture for Page Hierarchies
The hierarchical structure in Plane begins at the data layer with a self-referencing foreign key that establishes tree-like relationships between documents.
The Self-Referencing Page Model
In apps/api/plane/db/models/page.py, the Page class defines the hierarchy through a parent field pointing to itself:
class Page(BaseModel):
…
parent = models.ForeignKey(
"self",
on_delete=models.CASCADE,
null=True,
blank=True,
related_name="child_page",
)
…
- Self-reference: The
parentfield creates a recursive relationship wherenullvalues represent root-level documents. - Cascade deletion: Setting
on_delete=models.CASCADEensures that deleting a parent page automatically removes all descendants. - Reverse query: The
related_name="child_page"allows efficient fetching of immediate children viapage.child_page.all().
The model also includes fields like archived_at, is_locked, and view_props that persist alongside the hierarchy metadata.
API Endpoints for Managing Nested Documents
The backend exposes hierarchical operations through the PageViewSet in apps/api/plane/app/views/page/base.py, which handles CRUD operations while respecting parent-child relationships.
Creating and Moving Pages
The PageSerializer in apps/api/plane/app/serializers/page.py exposes the parent field for write operations:
class PageSerializer(BaseSerializer):
parent = serializers.PrimaryKeyRelatedField(
queryset=Page.objects.all(),
required=False,
allow_null=True,
)
…
To create a nested document, POST to /api/pages/ with a parent reference:
curl -X POST https://plane.example.com/api/pages/ \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"name": "Sub-section", "parent": "e3b0c442-98fc-1c14-9af4-7f5c1b2d6d0a"}'
To move a page within the hierarchy, PATCH the same endpoint with a new parent UUID.
Fetching Subtrees
The list method in PageViewSet supports filtering by parent to retrieve specific tree branches:
def list(self, request, *args, **kwargs):
parent_id = request.query_params.get("parent")
queryset = self.get_queryset()
if parent_id:
queryset = queryset.filter(parent_id=parent_id)
…
Requesting GET /api/pages/?parent=<uuid> returns only direct children of the specified page, allowing client-side recursion to build complete tree structures. All endpoints enforce authorization through the permission classes defined in apps/api/plane/app/permissions/page.py.
Frontend Rendering of Hierarchical Pages
The frontend renders individual pages through the PageRenderer component, which treats hierarchy as a data-layer concern while focusing on document editing capabilities.
The PageRenderer Component
Located in packages/editor/src/core/components/editors/document/page-renderer.tsx, this component receives an optional aiHandler prop alongside the editor instance:
export function PageRenderer(props: Props) {
const { editor, titleEditor, aiHandler, … } = props;
…
return (
<div className={cn("frame-renderer w-full flex-grow", { "wide-layout": displayConfig.wideLayout })}>
{titleEditor && <EditorContainer …>{/* title editor */}</EditorContainer>}
<EditorContainer …>
<EditorContentWrapper editor={editor} … />
{editor.isEditable && !isTouchDevice && (
<>
{bubbleMenuEnabled && <EditorBubbleMenu … />}
<BlockMenu … />
</>
)}
</EditorContainer>
</div>
);
}
The component does not directly render nested children; instead, sidebar tree components handle hierarchy visualization by fetching data via the parent-filtered API endpoints described above.
AI Capabilities Integration in Plane
Plane integrates artificial intelligence through a modular handler system that connects document content with AI services while maintaining awareness of page hierarchies.
The TAIHandler Interface
The contract for AI functionality is defined in packages/editor/src/core/types/ai.ts:
export type TAIHandler = {
/** Optional render function for the AI dropdown */
menu?: (props: TAIMenuProps) => React.ReactNode;
};
Rendering AI Menus
The AIFeaturesMenu component in packages/editor/src/core/components/menus/ai-menu.tsx renders when users interact with the AI handle element:
export function AIFeaturesMenu({ menu }: Props) {
const [isPopupVisible, setIsPopupVisible] = useState(false);
const menuRef = useRef<HTMLDivElement>(null);
…
return (
<div className={cn("pointer-events-none fixed inset-0 …", { "pointer-events-auto opacity-100": isPopupVisible })}>
<div ref={menuRef} className="z-10">
{menu?.({ isOpen: isPopupVisible, onClose: hidePopup })}
</div>
</div>
);
}
When supplied to PageRenderer, the aiHandler enables the AI menu to access the current page ID, allowing backend services to traverse the parent chain for context-aware suggestions:
const aiHandler: TAIHandler = {
menu: ({ isOpen, onClose }) => (
<MyAIDropdown
open={isOpen}
onClose={onClose}
pageId={currentPage.id}
/>
),
};
<PageRenderer
editor={editor}
titleEditor={titleEditor}
aiHandler={aiHandler}
…
/>
Practical Implementation Example
Combining these layers enables a complete workflow for hierarchical documents with AI assistance:
- Create root:
POST /api/pages/ {"name":"Project Specs"}returns IDR1 - Add child:
POST /api/pages/ {"name":"API Details","parent":"R1"}returns IDC1 - Fetch branch:
GET /api/pages/?parent=R1returns the child array - Render with AI: Supply
aiHandlertoPageRendererso that when users click the AI handle, the dropdown receivespageId: C1and can fetch parent context for intelligent suggestions
Summary
- Database layer:
apps/api/plane/db/models/page.pyimplements hierarchy via a self-referencingparentForeignKey withchild_pagereverse relation and cascade deletion. - API layer:
apps/api/plane/app/views/page/base.pysupports creating, moving, and filtering pages by parent UUID, whileapps/api/plane/app/serializers/page.pyhandles validation. - Frontend layer:
packages/editor/src/core/components/editors/document/page-renderer.tsxrenders document content and accepts an optionalaiHandlerprop. - AI integration:
packages/editor/src/core/components/menus/ai-menu.tsxrenders AI interfaces that can access page identifiers and traverse hierarchies for contextual assistance.
Frequently Asked Questions
How does Plane handle recursive deletion of nested pages?
When a parent page is deleted, Django's on_delete=models.CASCADE configured in apps/api/plane/db/models/page.py automatically removes all descendant pages. This ensures data consistency without requiring manual cleanup of nested documents.
Can AI features access parent page context for generating suggestions?
Yes. The aiHandler prop receives the current page identifier, allowing implementations to fetch the page's parent chain via the API. AI services can traverse the hierarchy using the parent field to build rich context for content generation or summarization.
How do you move a page to a different parent in the hierarchy?
Send a PATCH request to /api/pages/<id>/ with a new parent UUID in the payload. The PageSerializer validates the reference and updates the relationship, effectively moving the page and its descendants within the tree structure.
Is there a limit to nesting depth in Plane?
The data model itself imposes no artificial depth limit; pages can nest arbitrarily deep through the self-referencing parent field. Practical limits depend on API performance when recursively fetching deep trees and frontend rendering capabilities for heavily nested sidebar structures.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →