How Cycles and Modules Differ in Plane's Project Management Model
Cycles are time-boxed sprint containers with fixed start and end dates, whereas Modules are logical feature groupings without temporal constraints; critically, an issue can belong to only one Cycle but can be simultaneously linked to multiple Modules.
Plane (an open-source project management platform) provides two distinct organizational primitives for managing work within a project. While both Cycles and Modules serve to group issues, they operate on fundamentally different principles—Cycles govern when work happens through calendar-bound iterations, while Modules define what work belongs together through categorical associations. Understanding these architectural distinctions is essential for effective data modeling and API usage.
Core Conceptual Differences
Plane treats Cycles and Modules as complementary organizational layers rather than interchangeable tags. Their designs reflect distinct project management needs.
Cycles: Time-Boxed Iterations
Cycles represent calendar-bound execution windows analogous to agile sprints. Each Cycle enforces a strict temporal boundary through mandatory start_date and end_date fields, creating a burndown-friendly container where progress is measured against time remaining. The model tracks progress_snapshot to preserve historical velocity data even after archiving.
Modules: Logical Work Groupings
Modules function as taxonomic buckets for organizing issues by feature area, component, or product domain. Unlike Cycles, Modules carry no temporal constraints; instead, they utilize a color attribute for visual categorization and serve as long-term classifications that persist across multiple sprints. An issue can exist in multiple Modules simultaneously, enabling cross-cutting concerns like "Authentication" and "Technical Debt" to span disparate feature sets.
Data Model and Database Architecture
The underlying database schemas reflect these philosophical differences in storage location and field composition.
Cycle Schema
In apps/api/plane/db/models/cycle.py, the Cycle model inherits from BaseModel and stores temporal execution data:
- Required fields:
name,start_date,end_date - Status tracking:
status(active, upcoming, completed),progress_snapshot(JSON for analytics) - Soft deletion:
archived_attimestamp
Module Schema
In apps/api/plane/db/models/module.py, the Module model emphasizes categorical identity over temporal bounds:
- Identification fields:
name,description,color(hex code for UI theming) - Status tracking:
status(backlog, in-progress, completed) - Soft deletion:
archived_attimestamp (retains color and status for historical views)
Both models maintain a foreign key relationship to their parent Project, ensuring scoped access control and isolation.
Issue Relationships and Cardinality
The most significant architectural distinction lies in how issues associate with these containers.
One-to-One Cycle Assignment
Issues maintain a foreign key relationship to Cycles through the CycleIssue model (implicit in apps/api/plane/db/models/cycle_issue.py). An issue can belong to zero or one Cycle at any time. When reassigned, the previous Cycle link is destroyed and replaced, ensuring strict sprint ownership.
Many-to-Many Module Linking
Issues relate to Modules through an explicit junction table defined in apps/api/plane/db/models/module_issue.py. This many-to-many relationship allows an issue to exist in multiple Modules simultaneously—such as belonging to both the "Payments" feature Module and the "Q4 Tech Debt" Module—without temporal restriction.
API and Service Layer Implementation
Business logic for both entities follows similar patterns but handles distinct validation rules.
Cycle Services
The Cycle service layer in packages/services/src/cycle/cycle.service.ts enforces date validation and progress calculation:
createCycle(): Validates thatend_datefollowsstart_dateand initializesprogress_snapshotupdateCycle(): Prevents modification of dates if issues are already assigned (optional business rule)- API endpoints:
POST /api/v1/cycles/,PATCH /api/v1/cycles/{id}/
Module Services
The Module service in packages/services/src/module/module.service.ts focuses on categorization and color management:
createModule(): Validates hex color codes and enforces unique naming per projectupdateModule(): Handles status transitions without temporal constraints- API endpoints:
POST /api/v1/modules/,PATCH /api/v1/modules/{id}/
Frontend State Management
The React frontend utilizes MobX stores to manage local state and API synchronization.
Cycle Stores
apps/web/core/store/cycle.store.ts manages the selected Cycle, filter states, and CRUD operations. Filter definitions reside in packages/types/src/cycle/cycle_filters.ts, supporting date-range queries and progress-based filtering.
Module Stores
apps/web/core/store/module.store.ts handles Module selection and categorical filtering. The corresponding type definitions in packages/types/src/module/module_filters.ts enable color-based and status-based UI filtering.
Practical Implementation Examples
Creating a Sprint Cycle
import { useCycle } from '@/core/hooks/store/use-cycle';
function NewSprintForm() {
const { createCycle } = useCycle();
const handleSubmit = async (e) => {
e.preventDefault();
await createCycle({
name: 'Sprint 23',
start_date: '2026-07-01',
end_date: '2026-07-14',
project: currentProjectId,
});
};
return (
<form onSubmit={handleSubmit}>
{/* form fields for name, start/end dates */}
</form>
);
}
Implementation path: packages/services/src/cycle/cycle.service.ts → API call to POST /api/v1/cycles/.
Creating a Feature Module
import { useModule } from '@/core/hooks/store/use-module';
function NewFeatureGroupForm() {
const { createModule } = useModule();
const handleSubmit = async (e) => {
e.preventDefault();
await createModule({
name: 'Payments',
color: '#ff6600',
project: currentProjectId,
});
};
return (
<form onSubmit={handleSubmit}>
{/* fields for name, color picker */}
</form>
);
}
Implementation path: packages/services/src/module/module.service.ts → API call to POST /api/v1/modules/.
Assigning Issues to Both Containers
The IssueSerializer in apps/api/plane/space/serializer/issue.py handles dual assignment:
class IssueSerializer(serializers.ModelSerializer):
cycle = serializers.PrimaryKeyRelatedField(
queryset=Cycle.objects.all(),
required=False,
allow_null=True,
)
modules = serializers.PrimaryKeyRelatedField(
many=True,
queryset=Module.objects.all(),
required=False,
)
This configuration allows a single API payload to assign an issue to one Cycle (via foreign key) and multiple Modules (via many-to-many relation) simultaneously.
Summary
- Cycles enforce time-boxed execution with
start_dateandend_dateconstraints, tracking sprint progress through theCycleIssueforeign key relationship. - Modules provide flexible categorization without temporal bounds, using many-to-many
ModuleIssuerelationships allowing cross-cutting feature organization. - Data models reside in
apps/api/plane/db/models/cycle.pyandmodule.py, with associated services inpackages/services/src/cycle/cycle.service.tsandmodule/module.service.ts. - Frontend state is managed through dedicated MobX stores (
cycle.store.ts,module.store.ts) with type-safe filter definitions. - Archival preserves historical data for both entities, but Cycles retain progress snapshots while Modules retain color categorization for long-term product mapping.
Frequently Asked Questions
Can an issue belong to multiple Cycles simultaneously?
No. According to the Plane source code in apps/api/plane/db/models/cycle_issue.py, issues maintain a foreign key relationship to Cycles, restricting an issue to zero or one Cycle at any time. If you need to move work between sprints, you must reassign the issue to the new Cycle, effectively transferring it from the previous iteration.
What happens when you archive a Cycle compared to a Module?
Archived Cycles remain accessible in a dedicated "Archived" tab and retain their progress_snapshot data for historical velocity analysis, as implemented in apps/api/plane/db/models/cycle.py. Archived Modules also move to an archived view but preserve their color and status attributes for categorical reference, allowing issues to remain associated with these logical groupings for long-term product mapping even after the Module is no longer active.
Do Modules support burndown charts like Cycles?
No. Burndown and velocity tracking are Cycle-specific features tied to the start_date and end_date fields. Modules lack temporal constraints and therefore do not generate progress-over-time statistics. For tracking completion rates within a Module, Plane aggregates issue statuses statically rather than calculating time-based burndown, as Modules are designed for categorical organization rather than sprint execution monitoring.
Can Modules have start and end dates like Cycles?
While the base Module model in apps/api/plane/db/models/module.py does not include date fields by design, the platform's architecture supports custom fields or workflow adaptations. However, the standard implementation treats Modules as timeless categories. If you need date-bound groupings, you should use Cycles specifically, as they are optimized for temporal queries and progress snapshots that Modules intentionally omit.
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 →