---
title: "Hierarchy — Tree Structure for Collections"
aliases: [payload-hierarchy, nested-documents, breadcrumb-paths]
tags: [payloadcms, hierarchy, tree, breadcrumbs, parent-child, polymorphic]
sources: [raw/hierarchy__overview.md]
created: 2026-05-15
updated: 2026-05-15
---

## Overview

The Hierarchy feature adds automatic tree structure management to any Payload collection. Enable it with a single `hierarchy` property — Payload handles parent-child relationships, circular reference protection, and on-demand breadcrumb path generation.

**Use for:** Nested pages, category trees, org charts, folder systems, blog posts under pages.

## Quick Start

```ts
export const Pages: CollectionConfig = {
  slug: 'pages',
  admin: { useAsTitle: 'title' },
  fields: [{ name: 'title', type: 'text', required: true }],
  hierarchy: {
    parentFieldName: 'parent', // auto-created if field doesn't exist
  },
}
```

Auto-generated by enabling hierarchy:
- `parent` relationship field (self-referential)
- Virtual `_h_slugPath` — slugified URL path, computed on-demand
- Virtual `_h_titlePath` — human-readable breadcrumb path, computed on-demand
- Hooks: circular reference validation, orphan cleanup on deletion

## Auto-Generated Virtual Fields

Both fields are **not stored in DB** — computed by walking the parent chain on read.

| Field | Type | Example value |
|-------|------|---------------|
| `_h_slugPath` | `string` or localized object | `'store/products/widgets'` |
| `_h_titlePath` | `string` or localized object | `'Store/Products/Widgets'` |

If `admin.useAsTitle` field is `localized: true`, both path fields are automatically localized too.

## Requesting Path Computation

Paths are opt-in (prevents unnecessary DB queries on relationship loads).

**Method 1 — Context flag (Local API):**
```ts
const page = await payload.findByID({
  collection: 'pages',
  id: 'page-id',
  context: { computeHierarchyPaths: true },
})
// page._h_slugPath → 'store/products/widgets'
```

**Method 2 — Query param (REST API):**
```
GET /api/pages/abc123?computeHierarchyPaths=true
```

**Method 3 — Explicit select:**
```ts
const page = await payload.findByID({
  collection: 'pages',
  id: 'page-id',
  select: { title: true, _h_slugPath: true, _h_titlePath: true },
})
```

### Performance

- Each document needing paths = 1 extra query for ancestors
- **Request-scoped caching:** shared ancestors fetched only once
- 50 sibling docs → only 2 queries total (1 for docs, 1 for shared parent)
- Avoid `computeHierarchyPaths` when populating relationships where paths aren't needed

## Config Options

| Option | Type | Required | Description |
|--------|------|----------|-------------|
| `parentFieldName` | `string` | Yes | Field name for parent relationship (auto-created if missing) |
| `relationTo` | `string \| string[]` | No | Collection(s) allowed as parents. Default: self-referential |
| `slugify` | `function` | No | Custom slug function for path generation |
| `slugPathFieldName` | `string` | No | Override virtual field name (default: `_h_slugPath`) |
| `titlePathFieldName` | `string` | No | Override virtual field name (default: `_h_titlePath`) |

### Custom Parent Field

If you define the field manually, it **must** be:
- `type: 'relationship'`
- `relationTo: 'same-collection-slug'` (self-referential)
- `hasMany: false`

The `relationTo` in the manual field and in `hierarchy` config must match exactly.

## Polymorphic Hierarchies

Allow documents to have parents from **different collections**:

```ts
// Posts can nest under pages OR other posts
export const Posts: CollectionConfig = {
  slug: 'posts',
  hierarchy: {
    parentFieldName: 'parent',
    relationTo: ['pages', 'posts'],
  },
}
```

Creating cross-collection parent:
```ts
const post = await payload.create({
  collection: 'posts',
  data: {
    title: 'First Post',
    parent: { relationTo: 'pages', value: blogPage.id },
  },
})
```

Common polymorphic patterns:
- Blog posts under pages, threaded replies under posts
- Products under categories, variants under products
- Folders + documents nested under pages or other folders

## Important Behaviors

### No Cascade Updates
Moving or renaming a document does **not** update descendants — only the `parent` field is changed. Paths always reflect current hierarchy because they're computed fresh on read.

```ts
// Move folder with 50 descendants — still fast, no cascade
await payload.update({ collection: 'pages', id: 'folder-id', data: { parent: 'new-parent' } })
// Descendants: unchanged. Paths: accurate on next read.
```

### Circular Reference Protection
Automatic detection across collections. Setting a descendant as a parent throws before any DB change.

### Draft Support
Path computation respects the `draft` context:
- Reading draft → ancestor titles from draft versions
- Reading published → ancestor titles from published versions

### Locale Limitations
- `locale: 'all'` skips hierarchy processing — update each locale individually
- `parent` field is **not localized**: tree structure is the same across all locales
- `publishSpecificLocale` changes parent for **all locales** (since parent is not localized)

## Use Cases

```ts
// Nested pages
hierarchy: { parentFieldName: 'parent' }

// Nested categories
hierarchy: { parentFieldName: 'parentCategory' }

// Org chart with custom path field names
hierarchy: {
  parentFieldName: 'parentDept',
  slugPathFieldName: '_orgPath',
  titlePathFieldName: '_orgBreadcrumb',
}
```

## TypeScript

Virtual fields are included in generated types but are `undefined` unless paths are requested:

```ts
const page = await payload.findByID({
  collection: 'pages',
  id: 'page-id',
  context: { computeHierarchyPaths: true },
})
const url: string = `/${page._h_slugPath}`      // 'store/products/widget'
const crumbs = page._h_titlePath.split('/')      // ['Store', 'Products', 'Widget']
```

Regenerate types after adding hierarchy:
```bash
payload generate:types
```

## Key Takeaways

- Enable hierarchy with one config property — no custom code needed
- Virtual path fields (`_h_slugPath`, `_h_titlePath`) are computed on-demand, not stored
- Always opt-in with `computeHierarchyPaths: true` — avoids unnecessary queries on relationship loads
- No cascade updates: moving/renaming is O(1) regardless of descendants
- Polymorphic: `relationTo: ['pages', 'posts']` lets documents parent across collections
- Circular references caught automatically before any DB change
- `parent` field is never localized — tree structure is global, only path labels vary per locale
- Deep trees (10+ levels) slow down path computation — keep trees shallow when possible

## Related

- [[wiki/payloadcms/folders|Folders — Content Organization]] — folder grouping (beta), different from hierarchy
- [[wiki/payloadcms/fields-relationship|Relationship Field]] — underlying field type used for `parent`
- [[wiki/payloadcms/fields-join|Join Field]] — virtual reverse lookups (complements hierarchy)
- [[wiki/payloadcms/collection-config|Collection Config]] — full collection-level config reference
- [[wiki/payloadcms/localization|Localization]] — how localized titles feed into path computation

## Sources

- `raw/hierarchy__overview.md` — https://payloadcms.com/docs/hierarchy/overview
