Workspace-Level Labels and Estimates Architecture in Plane: Backend and Frontend Deep Dive

Plane implements workspace-level labels and estimates through a layered architecture where Django models with workspace ForeignKeys persist data, REST APIs expose CRUD endpoints, and React/MobX stores cache and compute hierarchical views for the UI.

Plane (makeplane/plane) treats labels and estimates as first-class workspace resources that can be shared across projects or scoped individually. This article examines the complete architecture for workspace-level labels and estimates, tracing the data flow from PostgreSQL tables through Django ORM models to the TypeScript service layer and MobX stores that power the React frontend.

Backend Architecture for Workspace Resources

The backend implements workspace-level labels and estimates using Django models that inherit from WorkspaceBaseModel, enforcing data integrity through foreign key relationships and unique constraints at the database level.

Label Models and Hierarchy

Labels in Plane support both workspace-wide and project-specific scopes, with optional parent-child relationships for categorization.

The Label model is defined in apps/api/plane/db/models/label.py. It inherits from WorkspaceBaseModel, which automatically links every label to a workspace via workspace_id. Labels can optionally belong to a specific project through the project_id field, or exist at the workspace level when project_id is null.

Key model features include:

  • Hierarchy support: The parent field is a self-referencing foreign key that enables nested label categories
  • Custom ordering: The sort_order field is auto-incremented on creation via the save() method to support drag-and-drop reordering
  • Uniqueness constraints: The database enforces unique name values per workspace or per project when records are not soft-deleted

# Conceptual model structure from apps/api/plane/db/models/label.py

class Label(WorkspaceBaseModel):
    name = models.CharField(max_length=255)
    color = models.CharField(max_length=255, blank=True)
    parent = models.ForeignKey("self", on_delete=models.CASCADE, null=True, blank=True)
    sort_order = models.FloatField(default=0)
    project = models.ForeignKey("db.Project", on_delete=models.CASCADE, null=True, blank=True)
    # workspace inherited from WorkspaceBaseModel

Estimate Models and Point Systems

Estimates in Plane are technically owned by projects but exposed at the workspace level through aggregation endpoints. The architecture separates the estimate definition from its individual point values.

In apps/api/plane/db/models/estimate.py, the Estimate model links to a project via project_id and includes metadata fields:

  • type: Either categories or points depending on the estimation method
  • last_used: Boolean flag identifying the currently active estimate for the project

Individual estimation points are stored in the EstimatePoint model, which links to an Estimate via foreign key and stores the display value, numeric key, and optional description.

The Project model (apps/api/plane/db/models/project.py) maintains an optional foreign key estimate pointing to the default estimate for that project, enabling quick lookup of the active estimation system.


# From apps/api/plane/db/models/estimate.py

class EstimatePoint(models.Model):
    estimate = models.ForeignKey("db.Estimate", on_delete=models.CASCADE, related_name="points")
    key = models.IntegerField(default=0, validators=[MinValueValidator(0)])
    value = models.CharField(max_length=255)
    description = models.TextField(blank=True)

REST API Endpoints

Plane exposes workspace-level labels and estimates through dedicated view modules that handle serialization and permission checks.

Label Endpoints:

Estimate Endpoints:

Frontend State Management

The frontend architecture uses MobX stores to cache workspace-level labels and estimates, providing computed getters that transform flat API responses into hierarchical structures suitable for UI components.

LabelStore and Hierarchical Data

The LabelStore class in apps/web/core/store/label.store.ts serves as the single source of truth for label data across the application.

The store maintains a labelMap (observable dictionary keyed by label ID) and provides computed getters that derive specific views:

  • workspaceLabels: Returns all labels where project_id is null
  • projectLabels(projectId): Filters labels by specific project
  • projectLabelsTree: Uses utils/buildTree.ts from @plane/utils to convert the flat label list into a hierarchical tree structure based on parent relationships

Data fetching flows through the service layer:

  1. Components call labelStore.fetchWorkspaceLabels(workspaceSlug)
  2. The store invokes IssueLabelService.getWorkspaceIssueLabels(slug) from apps/web/core/services/issue/issue_label.service.ts
  3. The service performs the HTTP GET to /api/workspaces/:slug/labels/
  4. Response data populates the labelMap, triggering reactive UI updates

For drag-and-drop reordering, the store provides updateLabelPosition(), which calculates new sort_order values based on neighboring labels and persists changes via IssueLabelService.patchIssueLabel().

ProjectEstimateStore for Active Estimates

Estimates are managed by ProjectEstimateStore in apps/web/core/store/estimates/project-estimate.store.ts, which handles the complexity of tracking active versus archived estimates.

The store caches estimates in an estimates map and exposes computed properties:

  • currentActiveEstimate: Returns the estimate where last_used === true for the current project
  • currentActiveEstimateId: Returns the ID of the active estimate
  • archivedEstimateIds: Lists estimates that are no longer active
  • areEstimateEnabledByProjectId: Boolean check for estimate availability

The store wraps EstimateService (located at apps/web/core/services/estimate.service.ts) to fetch data from /api/workspaces/:slug/estimates/ and /api/workspaces/:slug/projects/:id/estimates/.

Practical Implementation Examples

Fetching Workspace-Level Labels

Components access workspace labels through the root store context, triggering fetches only when data is not cached:

import { useRootStore } from "@/store/context";

const LabelDropdown = ({ workspaceSlug }: { workspaceSlug: string }) => {
  const { labelStore } = useRootStore();

  useEffect(() => {
    if (!labelStore.workspaceLabels) {
      labelStore.fetchWorkspaceLabels(workspaceSlug);
    }
  }, [workspaceSlug, labelStore]);

  const labels = labelStore.workspaceLabels ?? [];

  return (
    <Select>
      {labels.map((label) => (
        <Option key={label.id} value={label.id}>
          {label.name}
        </Option>
      ))}
    </Select>
  );
};

Updating Label Hierarchy

Drag-and-drop reordering updates both parent relationships and sort order:

await labelStore.updateLabelPosition(
  workspaceSlug,
  projectId,
  draggedLabelId,
  newParentId,   // null for top-level placement
  targetLabelId, // label to insert after
  false          // do not force drop at end
);

Accessing Active Estimates

UI components determine which estimate system to display using computed store properties:

const activeEstimate = projectEstimateStore.currentActiveEstimate;
if (activeEstimate) {
  console.log("Active estimate:", activeEstimate.name, activeEstimate.type);
  // Access points via activeEstimate.points
}

Summary

  • Workspace-level labels are stored in the Label model (apps/api/plane/db/models/label.py) with optional project scoping and hierarchical parent relationships, exposed via apps/api/plane/app/views/workspace/label.py.
  • Estimates are project-owned but workspace-accessible through aggregation endpoints in apps/api/plane/app/views/workspace/estimate.py, with points stored separately in the EstimatePoint model.
  • Frontend caching uses MobX stores (LabelStore and ProjectEstimateStore) to maintain local state, compute hierarchical trees, and track active estimates via the last_used flag.
  • Service layer abstraction in IssueLabelService and EstimateService decouples React components from direct API calls, enabling optimistic updates and error rollback.
  • Ordering and hierarchy are handled through database-level sort_order fields and runtime tree-building utilities that transform flat API responses into nested UI structures.

Frequently Asked Questions

What is the difference between workspace-level and project-level labels in Plane?

Workspace-level labels have a null project_id and are available across all projects within that workspace, while project-level labels have a specific project_id foreign key and are only visible within that project. Both types store their workspace association via WorkspaceBaseModel inheritance, but the LabelStore frontend computes separate workspaceLabels and projectLabels getters to filter the cached labelMap accordingly.

How does Plane handle label hierarchy and ordering?

Labels support arbitrary nesting through a self-referencing parent foreign key in the database model. The frontend uses utils/buildTree.ts to convert the flat list returned by the API into a hierarchical tree structure for display. Ordering is maintained via a sort_order float field; when users drag and drop labels, the updateLabelPosition method in LabelStore recalculates sort values based on neighboring labels and persists the change via PATCH requests to the label API.

Can estimates be shared across multiple projects in a workspace?

No, estimates are owned by individual projects through a project_id foreign key in the Estimate model. However, the workspace-level API endpoint (GET /api/workspaces/:slug/estimates/) aggregates all estimates from all projects within the workspace, allowing users to browse and select from existing estimates when configuring new projects. The Project model tracks its currently selected default estimate via an estimate foreign key.

How does the frontend determine which estimate is currently active?

The ProjectEstimateStore exposes a computed currentActiveEstimate getter that filters the cached estimates map to return the estimate where last_used === true. When a user selects a different estimate for a project, the backend updates the last_used flag on the Estimate model, and the store refreshes its local cache via getProjectEstimates(), triggering reactive updates in components like the estimate dropdown selector.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →