How Plane Module Management Groups Related Issues Across Cycles and Tracks Dependencies
Plane uses a hierarchical data model where Modules serve as first-class grouping containers above individual issues, leveraging a many-to-many link table (ModuleIssue) to connect issues to modules while preserving cycle assignments through a nullable cycle_id field, allowing cross-dimensional filtering and dependency surfacing via the IssueLink table.
Plane's module management system enables teams to organize work thematically across time-boxed iterations. In the makeplane/plane repository, modules function as persistent containers that transcend individual cycles, creating a matrix view where you can track related issues by both thematic grouping and sprint timing. This architecture supports complex dependency mapping while maintaining clean separation between temporal and logical organization.
The Data Model: Modules, Issues, and Cycles
The relationship structure follows a chain where cycles and modules intersect at the issue level:
Cycle ──► Issue ◄──► ModuleIssue ◄──► Module
Modules are defined in apps/api/plane/db/models/module.py. The Module model stores metadata including name, dates, status, and ordering fields. The ModuleIssue class acts as a junction table:
# apps/api/plane/db/models/module.py
class ModuleIssue(ProjectBaseModel):
module = models.ForeignKey("db.Module", on_delete=models.CASCADE, related_name="issue_module")
issue = models.ForeignKey("db.Issue", on_delete=models.CASCADE, related_name="issue_module")
Issues maintain a nullable cycle_id column in their model definition, allowing them to exist independently or within a specific cycle. This dual-linking capability enables the system to surface issues that belong to both a specific module and a specific cycle simultaneously.
Grouping Issues Across Cycles
Backend Query Logic
The API retrieves module-grouped issues through the endpoint GET /api/v1/modules/{module_id}/issues/, decorated with OpenAPI specifications in apps/api/plane/utils/openapi/decorators.py. The query construction leverages the filter_module helper in apps/api/plane/utils/issue_filters.py:
Issue.objects.filter(
issue_module__module_id=module_id, # join via ModuleIssue
cycle_id=cycle_id, # optional filter by cycle
issue_module__deleted_at__isnull=True, # exclude soft-deleted links
)
This query pattern allows the system to fetch all issues associated with a module while optionally constraining the results to a specific cycle, enabling the "Module X in Cycle Y" view.
Frontend Grouping
The ModulesListView component in apps/web/core/components/modules/modules-list-view.tsx handles the presentation layer. When users toggle "group by cycle," the component aggregates issues using the cycle field:
// apps/web/core/components/modules/modules-list-view.tsx
const grouped = groupBy(issues, (issue) => issue.cycle?.name ?? 'No Cycle');
return (
<Accordion>
{Object.entries(grouped).map(([cycleName, issues]) => (
<AccordionItem key={cycleName} title={cycleName}>
<IssueList items={issues} />
</AccordionItem>
))}
</Accordion>
);
The filter configuration resides in packages/utils/src/work-item-filters/configs/filters/module.ts, which constructs filter objects that the module_filter.store.ts store processes to include cycle_id parameters in API requests.
Dependency Tracking
While dependencies are stored separately in the IssueLink table, module-centric views surface these relationships to maintain visibility across thematic boundaries. When serializer logic in apps/api/plane/utils/serializers/issue.py prepares module data, it includes a dependencies field listing linked issue IDs.
The IssueDetail component in apps/web/core/components/issue-detail/issue-detail.tsx renders these dependencies regardless of module membership:
// apps/web/core/components/issue-detail/issue-detail.tsx
{issue.dependencies?.length && (
<Section title="Dependencies">
{issue.dependencies.map((dep) => (
<Link key={dep.id} to={`/issues/${dep.id}`}>{dep.title}</Link>
))}
</Section>
)}
This implementation allows users to identify cross-module blockers while maintaining their primary module and cycle groupings.
Implementation Examples
Linking Issues to Modules
Use the service layer method in packages/services/src/module/module.service.ts:
import { ModuleService } from '@plane/services/module';
async function addIssueToModule(moduleId: string, issueId: string) {
await ModuleService.addIssue({ module_id: moduleId, issue_id: issueId });
}
Filtering by Module and Cycle
API requests support combined filtering via query parameters:
curl "https://api.plane.so/api/v1/modules/550e8400-e29b-41d4-a716-446655440001/issues?cycle=3f1c2d4e"
The orderModules utility in packages/utils/src/module.ts handles client-side sorting of these results.
Summary
- Modules are first-class containers defined in
apps/api/plane/db/models/module.pythat persist across cycles - ModuleIssue serves as a many-to-many junction table linking issues to modules while preserving independent cycle assignments
- Cross-cycle grouping is achieved by filtering
Issueobjects through bothissue_module__module_idandcycle_idfields - Dependency visibility spans module boundaries through the
IssueLinktable and serializer-side inclusion ofdependenciesarrays - Frontend components like
ModulesListViewleverage thegroupByutility to organize issues by cycle within module views
Frequently Asked Questions
How does Plane handle issues that belong to multiple modules?
Plane supports many-to-many relationships between issues and modules through the ModuleIssue junction table. An issue can link to multiple modules simultaneously while maintaining its cycle assignment, allowing it to appear in different module views while retaining its sprint context. The ModuleIssue model in apps/api/plane/db/models/module.py manages these associations with soft-delete support via the deleted_at field.
Can I filter module issues by specific cycles in the API?
Yes. The API endpoint GET /api/v1/modules/{module_id}/issues/ accepts a cycle query parameter that filters results to issues belonging to both the specified module and cycle. The filter_module function in apps/api/plane/utils/issue_filters.py constructs this query by joining the ModuleIssue and Issue tables and applying the cycle_id filter.
Where does Plane store dependency information between issues?
Dependencies are tracked in the IssueLink table (referenced in the serializers), though the exact table definition lives outside the module-specific code. The IssueDetail component surfaces these relationships by rendering the dependencies array populated by apps/api/plane/utils/serializers/issue.py, showing linked issues regardless of their module or cycle assignments.
How do I programmatically add an issue to a module?
Use the ModuleService class in packages/services/src/module/module.service.ts. The addIssue method accepts an object containing module_id and issue_id, creating the association in the ModuleIssue table. This service layer handles the API communication to POST /api/v1/modules/{module_id}/issues/.
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 →