Skip to main content
All Docs
FeaturesagentOS Block ManagerUpdated April 12, 2026

Five-Level Entity Hierarchy

Five-Level Entity Hierarchy

The platform organises all data within a strict five-level hierarchy:

Managing Agent → Owner → Block → Unit/Leaseholder → Management Accounts

This structure governs data organisation, reporting scopes, and permission boundaries across the entire application. Every entity must exist within this hierarchy — no entity can be created outside it.


Hierarchy Levels

Level 1 — Managing Agent

The top-level organisational container. Represents the property management agency and maps 1:1 with the shell organisations table. There is exactly one managing agent per organisation.

Level 2 — Owner

An individual or company portfolio holder. An owner holds one or more blocks under the managing agent. At least one owner must exist before blocks can be assigned.

Level 3 — Block

A single residential building. A block belongs to exactly one owner and cannot be shared across owners. Each block contains units and has management accounts attached to it.

Level 4 — Unit / Leaseholder

An individual flat or apartment within a block. Each unit has one current leaseholder with their own financial ledger and document storage.

Level 5 — Management Accounts

Financial data sources for a block — bank feeds (TrueLayer), Calmony client money accounts, manual ledgers, or accounting connectors. A block requires at least one management account before financial reporting is enabled.


Hierarchy Rules

  • No orphaned entities. Every entity must be positioned correctly within the hierarchy. Blocks without owners, or units referencing non-existent blocks, are flagged as hierarchy violations.
  • Reporting flows upward. Financial and compliance data aggregates from Units → Block → Owner → Managing Agent.
  • Permissions cascade downward. A user's access scope is defined at a level in the hierarchy, and they see all entities below that level (subject to their role).
  • Multi-tenant isolation. All hierarchy queries are scoped by orgId. No data crosses organisational boundaries.

Dashboard Widget

The Entity Hierarchy widget on the main dashboard provides a live overview of the hierarchy for your organisation:

  • Count of entities at each level.
  • Completion indicator (✓ or ⚠) per level showing whether that level meets its minimum requirement.
  • Completeness score (0–100%) shown as a progress bar, calculated as 25 points per completed requirement.
  • Navigation links to the management page for each level.
  • Guidance panel listing outstanding setup steps when setup is incomplete.

The widget is displayed alongside the Recent Activity feed in a two-column layout on the dashboard.

Completeness Scoring

RequirementPoints
Managing agent profile configured25
At least one owner exists25
All blocks assigned to an owner25
At least one management account connected to a block25

A score of 100% means the hierarchy is fully configured and all financial reporting features are available.


Permission Cascade

The hierarchy defines how block-level access is scoped per user role:

Platform RoleAccessible Blocks
principalAll blocks in the organisation
read_onlyAll blocks in the organisation
property_managerExplicitly assigned blocks, or all blocks if no restriction is set
freeholderAll blocks belonging to their scoped owner
leaseholderTheir single scoped block only

This is enforced at the data layer via getAccessibleBlockIds, which is called before any block-scoped query.


tRPC API Reference

All hierarchy endpoints are under the hierarchy namespace and require the blocks.read RBAC permission.

hierarchy.summary

Returns a full overview of the hierarchy for the current organisation.

Response

{
  orgId: string;
  agent: { id: string; name: string | null; tradingName: string | null } | null;
  counts: {
    owners: number;
    blocks: number;
    unassignedBlocks: number;
    units: number;
    managementAccountSources: number;
    clientMoneyAccounts: number;
  };
  completeness: {
    hasAgentProfile: boolean;
    hasOwners: boolean;
    allBlocksAssigned: boolean;
    hasManagementAccounts: boolean;
    score: number; // 0–100
  };
  hierarchyLevels: Record<HierarchyLevel, HierarchyLevelInfo>;
}

hierarchy.children

Returns the children of a given entity at the next hierarchy level.

Input

{
  level: HierarchyLevel; // "managing_agent" | "owner" | "block" | "unit" | "management_account"
  entityId: string;
}

Response

{
  parentLevel: HierarchyLevel;
  parentId: string;
  children: HierarchyNode[];
}

For management_account (the leaf level), children is always an empty array.


hierarchy.ancestors

Resolves the full ancestor chain for an entity, ordered from root (Managing Agent) down to the entity's immediate parent. Used for breadcrumb navigation.

Input

{ level: HierarchyLevel; entityId: string; }

Response

{
  entityId: string;
  entityLevel: HierarchyLevel;
  ancestors: Array<{ level: HierarchyLevel; id: string; name: string }>;
}

Example — resolving ancestors for a unit returns:

[ Managing Agent ] → [ Owner: "Smith Properties" ] → [ Block: "Maple Court" ]

hierarchy.validate

Validates that an entity is correctly positioned in the hierarchy. Returns a result object rather than throwing, making it suitable for diagnostic and admin views.

Input

{ level: HierarchyLevel; entityId: string; }

Response

{
  valid: boolean;
  level: HierarchyLevel;
  entityId: string;
  parentId: string | null;
  parentLevel: HierarchyLevel | null;
  issues: string[]; // human-readable descriptions of any hierarchy violations
}

Validation failures are written to the audit log with action hierarchy.validation_failed.


hierarchy.levels

Returns static metadata about all five hierarchy levels. No input required.

Response

{
  levels: Array<{
    key: HierarchyLevel;
    label: string;
    description: string;
    parentLevel: HierarchyLevel | null;
    childLevel: HierarchyLevel | null;
    position: number; // 1–5
  }>;
  totalLevels: number; // 5
  structure: string;   // "Managing Agent → Owner → Block → Unit/Leaseholder → Management Accounts"
}

hierarchy.tree

Returns a two-level nested snapshot of owners and their blocks. Optimised for navigation UIs and portfolio selectors.

Response

{
  tree: Array<{
    owner: HierarchyOwnerNode;
    blocks: HierarchyBlockNode[];
  }>;
}

Hierarchy Node Types

type HierarchyLevel =
  | "managing_agent"
  | "owner"
  | "block"
  | "unit"
  | "management_account";

interface HierarchyOwnerNode {
  level: "owner";
  id: string;
  orgId: string;
  name: string;
  ownerType: string;
  email: string | null;
  childCount: number; // number of blocks
}

interface HierarchyBlockNode {
  level: "block";
  id: string;
  orgId: string;
  ownerId: string | null;
  name: string;
  addressLine1: string | null;
  city: string | null;
  postcode: string | null;
  totalUnits: number | null;
  childCount: number; // number of units
  hasManagementAccount: boolean;
}

interface HierarchyUnitNode {
  level: "unit";
  id: string;
  orgId: string;
  blockId: string;
  unitNumber: string;
  floor: string | null;
  status: string | null;
  leaseholderName: string | null;
  leaseholderEmail: string | null;
}

interface HierarchyAccountNode {
  level: "management_account";
  id: string;
  orgId: string;
  blockId: string;
  sourceType: string; // e.g. "truelayer", "calmony", "manual"
  accountType?: string;
  label: string;
  isActive: boolean;
}