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.py that 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 Issue objects through both issue_module__module_id and cycle_id fields
  • Dependency visibility spans module boundaries through the IssueLink table and serializer-side inclusion of dependencies arrays
  • Frontend components like ModulesListView leverage the groupBy utility 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:

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 →