# How Plane Manages Cycles for Sprint Planning: A Deep Dive into the Architecture

> Discover how Plane's three-layer architecture efficiently manages sprint cycles for planning. Learn about Django, TypeScript, and MobX for seamless synchronization and display.

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

---

**Plane uses a three-layer architecture—Django backend, TypeScript service layer, and MobX state management—to persist, synchronize, and reactively display sprint cycles across the entire application.**

In open-source project management tools, **cycle management** forms the backbone of agile sprint planning. Plane, the self-hosted project management platform from makeplane/plane, implements this through a sophisticated system that spans Python's Django ORM, RESTful APIs, and a reactive TypeScript frontend. This article examines how Plane handles the complete lifecycle of a sprint cycle—from creation and storage to filtering, analytics, and archival—based on the actual source code implementation.

## The Three-Layer Cycle Architecture

Plane's sprint planning system is organized into distinct layers, each with clear responsibilities and well-defined interfaces between them.

### Backend Layer: Django Models and REST Endpoints

The foundation sits in Django, where cycles are defined as persistent database entities with rich relationships to projects, issues, and users.

**Core Models in [`apps/api/plane/db/models/cycle.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/cycle.py)**

The `Cycle` model defines the essential sprint fields: `name`, `start_date`, `end_date`, `status`, and `progress_snapshot`. Related models handle specific concerns:

- `CycleIssue` — links individual issues to specific cycles
- `CycleUserProperties` — stores per-user preferences and favorite status

**Serialization and API Surface**

The `CycleBaseSerializer` in [`apps/api/plane/space/serializer/cycle.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/space/serializer/cycle.py) validates incoming data and formats responses. API views in the `views` folder—particularly `CycleViewSet`—expose endpoints at `/api/workspaces/{workspace}/projects/{project}/cycles/`, handling standard CRUD operations plus specialized actions for archiving and analytics.

### Service Layer: TypeScript HTTP Abstraction

The frontend communicates through a typed service layer that constructs URLs, manages HTTP verbs, and propagates errors consistently.

**`CycleService` — Core CRUD and Analytics**

Located at [`packages/services/src/cycle/cycle.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/cycle/cycle.service.ts), this class wraps all standard cycle operations:

```typescript
// Fetching cycles with query parameters
async getWithParams(
  workspaceSlug: string,
  projectId: string,
  params?: Partial<CycleParams>
): Promise<ICycle[]> { ... }

// Creating a new sprint
async create(
  workspaceSlug: string,
  projectId: string,
  data: Partial<ICycle>
): Promise<ICycle> { ... }

// Real-time progress snapshots
async workspaceActiveCyclesProgress(
  workspaceSlug: string,
  cycleId: string
): Promise<IProgressSnapshot> { ... }

```

**`CycleOperationsService` — Cross-Cycle Operations**

Found in [`packages/services/src/cycle/cycle-operations.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/cycle/cycle-operations.service.ts), this handles operations that span multiple cycles or involve user preferences:

- `addToFavorites` / `removeFromFavorites` — user-specific cycle starring
- `transferIssues` — moving issues between sprints without losing history

### State Management: MobX Store with Computed Properties

The reactive heart of Plane's cycle management lives in [`apps/web/core/store/cycle.store.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/cycle.store.ts). This MobX store caches cycle data, derives filtered collections, and triggers UI updates automatically.

**Key Store Properties**

| Property | Purpose |
|----------|---------|
| `cycleMap` | Keyed record of all loaded `ICycle` objects |
| `activeCycleIdMap` | Tracks which cycle ID is currently active per project |
| `archivedCycleIds` | Set of IDs for completed/archived sprints |

**Computed Getters for Sprint Views**

The store automatically derives useful collections:

```typescript
// Current active sprint for the selected project
get currentProjectActiveCycleId(): string | null;

// All completed cycles for retrospective analysis
get currentProjectCompletedCycleIds(): string[];

// Filtered view based on user search and filter selections
getFilteredCycleIds(projectId: string): string[];

```

## How Cycles Flow Through the System

### Creating a New Sprint

When a user initiates a new sprint, the data flows through all three layers:

1. **UI Component** calls `CycleStore.createCycle(workspaceSlug, projectId, data)`
2. **Store** delegates to `CycleService.create`, which POSTs to the Django endpoint
3. **Backend** validates via `CycleBaseSerializer` and persists a new `Cycle` row
4. **Store** normalizes the response into `cycleMap`, triggering reactive updates

```tsx
import { useCycle } from "@/hooks/store/use-cycle";

function NewSprintForm({ workspaceSlug, projectId }: Props) {
  const { createCycle } = useCycle();

  const handleSubmit = async (e: React.FormEvent) => {
    e.preventDefault();
    const data = {
      name: "Sprint 42",
      start_date: "2026-09-01T00:00:00Z",
      end_date: "2026-09-14T23:59:59Z",
    };
    await createCycle(workspaceSlug, projectId, data);
    // UI automatically updates because CycleStore caches the new cycle
  };

  return <form onSubmit={handleSubmit}>/* …inputs… */</form>;
}

```

### Loading and Displaying the Active Sprint

The active cycle determination relies on the `status` field. A cycle with `status === "current"` (case-insensitive) is treated as the active sprint:

```tsx
import { useEffect } from "react";
import { useCycle } from "@/hooks/store/use-cycle";

function ActiveSprint({ workspaceSlug, projectId }: Props) {
  const {
    fetchActiveCycle,
    fetchActiveCycleProgress,
    currentProjectActiveCycle,
    getIsPointsDataAvailable,
  } = useCycle();

  useEffect(() => {
    fetchActiveCycle(workspaceSlug, projectId);
    if (currentProjectActiveCycle?.id) {
      fetchActiveCycleProgress(workspaceSlug, projectId, currentProjectActiveCycle.id);
    }
  }, [workspaceSlug, projectId, currentProjectActiveCycle?.id]);

  if (!currentProjectActiveCycle) return <p>No active sprint.</p>;

  const hasPoints = getIsPointsDataAvailable(currentProjectActiveCycle.id);
  return (
    <div>
      <h2>{currentProjectActiveCycle.name}</h2>
      <p>{hasPoints ? "Points‑based burndown available" : "No point data yet"}</p>
    </div>
  );
}

```

### Filtering and Search

User filtering combines store-side computation with utility helpers. The `shouldFilterCycle` function in [`packages/utils/src/work-item-filters/configs/filters/cycle.ts`](https://github.com/makeplane/plane/blob/main/packages/utils/src/work-item-filters/configs/filters/cycle.ts) implements predicate logic for various filter dimensions—priority, state, assignees, date ranges. The store's `getFilteredCycleIds` method applies these predicates plus any text search query to produce the final display set.

### Archives and Issue Transfer

Completed sprints move to archival through `CycleArchiveService`, which wraps `/archive/` endpoints. The store updates `archived_at` timestamps and removes archived cycles from active collections. For replanning, `CycleOperationsService.transferIssues` migrates issues between cycles while preserving full history.

## Analytics and Progress Tracking

Plane embeds real-time progress data directly into cycle records. The `progress_snapshot` JSON field stores burndown metrics, and `estimate_distribution` or `distribution` fields capture point-based breakdowns. The store's `fetchActiveCycleAnalytics` method populates these fields:

```typescript
// From CycleStore
async fetchActiveCycleAnalytics(
  workspaceSlug: string,
  projectId: string,
  cycleId: string
): Promise<void> {
  const analytics = await this.cycleService.getCycleDetailsAnalytics(
    workspaceSlug,
    projectId,
    cycleId
  );
  // Merged into cycleMap[cycleId] for reactive access
}

```

## Key Implementation Files for Reference

| File | Role |
|------|------|
| [`apps/api/plane/db/models/cycle.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/cycle.py) | Django ORM: `Cycle`, `CycleIssue`, `CycleUserProperties` models |
| [`apps/api/plane/space/serializer/cycle.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/space/serializer/cycle.py) | `CycleBaseSerializer` for API validation |
| [`packages/services/src/cycle/cycle.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/cycle/cycle.service.ts) | HTTP client: CRUD, list, analytics, validation |
| [`packages/services/src/cycle/cycle-operations.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/cycle/cycle-operations.service.ts) | Favorites and issue-transfer operations |
| [`apps/web/core/store/cycle.store.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/store/cycle.store.ts) | MobX store: caching, computed properties, actions |
| [`packages/utils/src/work-item-filters/configs/filters/cycle.ts`](https://github.com/makeplane/plane/blob/main/packages/utils/src/work-item-filters/configs/filters/cycle.ts) | Filter predicate utilities |
| `apps/web/core/components/cycles/` | UI components: list views, selection, active sprint display |

## Summary

- **Plane's cycle management** spans three architectural layers: Django models for persistence, TypeScript services for API abstraction, and MobX for reactive state.
- **Active sprint detection** relies on the `status` field matching `"current"`, with the store maintaining `activeCycleIdMap` for O(1) lookups.
- **Filtering and search** combine store-level computation with reusable utilities in `shouldFilterCycle`, supporting priority, state, assignee, and date filters.
- **Progress analytics** are embedded in cycle records via `progress_snapshot` and distribution fields, fetched on demand through dedicated service methods.
- **Archival and cross-cycle operations** are handled by specialized services that maintain data integrity while updating reactive store state.

## Frequently Asked Questions

### How does Plane determine which sprint is currently active?

Plane checks the `status` field of each cycle for the value `"current"` (case-insensitive). The `CycleStore` computes `currentProjectActiveCycleId` by scanning the project's cycles, and maintains this in `activeCycleIdMap` for efficient access. When the active cycle progresses through its date range or is manually updated, reactive observers trigger UI re-renders automatically.

### Can issues be moved between sprints without losing history?

Yes. The `CycleOperationsService.transferIssues` method in [`packages/services/src/cycle/cycle-operations.service.ts`](https://github.com/makeplane/plane/blob/main/packages/services/src/cycle/cycle-operations.service.ts) handles this migration. It updates the issue-to-cycle associations while preserving all activity history, comments, and state transitions. The store's `cycleMap` and issue caches update reactively to reflect the transfer.

### How does Plane handle sprint progress analytics?

Progress data flows through `CycleService.workspaceActiveCyclesProgress` and `CycleStore.fetchActiveCycleAnalytics`, which populate the `progress_snapshot` JSON field and distribution metrics on cycle records. These snapshots include burndown data and point distributions, enabling real-time sprint dashboards without repeated heavy queries.

### What happens when a sprint is archived?

Archival invokes `CycleArchiveService` to POST to the `/archive/` endpoint. The Django backend sets `archived_at`, and the MobX store removes the cycle ID from active collections while adding it to `archivedCycleIds`. If the cycle was favorited, `is_favorite` is cleared and the store's favorite map updates accordingly.