# How Plane Implements Issue Archiving with Soft-Delete and Restore Functionality

> Discover how Plane archives issues using soft-delete and restore functionality. Learn how the archived_at field preserves data for restoration and audit trails.

- Repository: [Plane/plane](https://github.com/makeplane/plane)
- Tags: internals
- Published: 2026-06-22

---

**Plane uses a soft-delete pattern where the `archived_at` field on the Issue model acts as a deletion flag, allowing issues to be hidden from standard queries while preserving data for restoration and audit trails.**

The open-source project management platform Plane (makeplane/plane) implements issue archiving through a sophisticated three-layer architecture that combines database-level filtering, state-aware API validation, and comprehensive activity logging. This approach ensures that archived issues remain accessible for restoration while being excluded from active workflows.

## Database-Level Soft-Delete Implementation

Plane's soft-delete mechanism centers on a nullable date field and a custom model manager that automatically filters archived records from standard queries.

### The `archived_at` Field

In [`apps/api/plane/db/models/issue.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/issue.py), the **Issue** model defines an `archived_at` field as a nullable `DateField`:

```python

# From apps/api/plane/db/models/issue.py (lines 58-61)

archived_at = models.DateField(
    null=True, 
    blank=True, 
    help_text="Date when the issue was archived"
)

```

When this field contains `null`, the issue is considered active. When populated with a date, the issue enters an archived state. This preserves all relational data and issue history without physically deleting database rows.

### IssueManager Filter Logic

The **IssueManager** custom manager automatically excludes archived issues from standard querysets. Located in the same file (lines 91-99), this manager overrides the default queryset to filter out rows where `archived_at` is not null:

```python

# Conceptual implementation from IssueManager

class IssueManager(models.Manager):
    def get_queryset(self):
        return super().get_queryset().filter(archived_at__isnull=True)

```

This ensures that any ORM query using `Issue.objects` or `Issue.issue_objects` (the default manager) automatically excludes archived issues, preventing accidental data leakage into active project views.

## API Endpoints for Archive and Restore Operations

The archive functionality exposes dedicated REST endpoints in [`apps/api/plane/app/views/issue/archive.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/issue/archive.py), implementing strict state validation before allowing archival operations.

### Single Issue Archiving with State Validation

The archive endpoint (`POST /api/workspaces/{slug}/projects/{project_id}/archived-issues/`) validates that an issue is in a terminal state before archiving. As implemented in lines 56-78, the system checks the `issue.state.group` property:

```python

# Excerpt from apps/api/plane/app/views/issue/archive.py

@allow_permission([ROLE.ADMIN, ROLE.MEMBER])
def archive(self, request, slug, project_id, pk=None):
    issue = Issue.issue_objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
    
    # State validation: only completed or cancelled issues can be archived

    if issue.state.group not in ["completed", "cancelled"]:
        return Response(
            {"error": "Can only archive completed or cancelled state group issue"},
            status=status.HTTP_400_BAD_REQUEST
        )
    
    # Set archive timestamp

    issue.archived_at = timezone.now().date()
    issue.save()
    
    # Activity logging (see below)

    issue_activity.delay(...)
    
    return Response({"archived_at": str(issue.archived_at)}, status=status.HTTP_200_OK)

```

This validation prevents work-in-progress issues from being archived accidentally, enforcing project management hygiene.

### Restoring Archived Issues

Restoration uses a `DELETE` request to the same archive endpoint (lines 80-99), which clears the `archived_at` field and logs the restoration activity:

```python
def unarchive(self, request, slug, project_id, pk=None):
    # Retrieve using archived manager to access soft-deleted records

    issue = Issue.archived_objects.get(workspace__slug=slug, project_id=project_id, pk=pk)
    
    # Clear the archive flag

    issue.archived_at = None
    issue.save()
    
    # Log restoration activity

    issue_activity.delay(...)
    
    return Response(status=status.HTTP_200_OK)

```

The system uses a separate manager (`archived_objects`) to access archived records, ensuring intentional access to soft-deleted data.

### Bulk Archive Operations

For efficiency, Plane supports bulk archiving via `POST /api/workspaces/{slug}/projects/{project_id}/bulk-archive/` (lines 105-143). This endpoint:

1. Accepts a list of issue IDs in the request body
2. Validates each issue's state (must be completed or cancelled)
3. Sets `archived_at` in memory for each valid issue
4. Performs a single `bulk_update` operation to minimize database writes

```python

# Conceptual flow from bulk archive implementation

issues_to_archive = []
for issue_id in issue_ids:
    issue = Issue.issue_objects.get(pk=issue_id)
    if issue.state.group in ["completed", "cancelled"]:
        issue.archived_at = timezone.now().date()
        issues_to_archive.append(issue)
        # Log activity for each issue

        issue_activity.delay(...)

# Single update operation

Issue.objects.bulk_update(issues_to_archive, ["archived_at"])

```

This approach reduces database round-trips when archiving multiple completed sprints or backlog items simultaneously.

## Frontend Service Integration

The Plane web application abstracts these API endpoints through the **IssueArchiveService** in [`apps/web/core/services/issue/issue_archive.service.ts`](https://github.com/makeplane/plane/blob/main/apps/web/core/services/issue/issue_archive.service.ts). This TypeScript service provides methods that wrap the REST calls:

```typescript
import { IssueArchiveService } '@/services/issue/issue_archive.service';

// Archiving a single issue
const archiveIssue = async (workspace: string, project: string, issueId: string) => {
  const service = new IssueArchiveService();
  const result = await service.archiveIssue(workspace, project, issueId);
  return result.archived_at;
};

// Restoring an archived issue
const restoreIssue = async (workspace: string, project: string, issueId: string) => {
  const service = new IssueArchiveService();
  await service.restoreIssue(workspace, project, issueId);
};

```

The service handles URL construction, authentication headers, and error parsing, providing a type-safe interface for the React frontend components.

## Activity Logging and Audit Trail

Every archive and restore operation generates an audit entry via the **issue_activity** background task ([`apps/api/plane/bgtasks/issue_activities_task.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/bgtasks/issue_activities_task.py)). The system records:

- The actor performing the action
- The timestamp (as epoch)
- The previous state (serialized issue data)
- The requested changes
- Automation flags

This creates a complete history of when issues were archived and restored, accessible through the activity feed for compliance and debugging purposes.

## Summary

- **Soft-delete mechanism**: Plane uses a nullable `archived_at` DateField rather than physical deletion, preserving all issue data and relationships.
- **Automatic filtering**: The **IssueManager** custom manager automatically excludes archived issues from standard queries, ensuring archived data doesn't appear in active workflows.
- **State enforcement**: The API validates that issues are in **completed** or **cancelled** states before archiving, preventing accidental archival of active work.
- **Bulk operations**: The `bulk_update` method in the bulk archive endpoint optimizes performance for archiving multiple issues simultaneously.
- **Complete audit trail**: The `issue_activity` task logs every archive and restore operation with full context and user attribution.
- **Frontend abstraction**: The **IssueArchiveService** TypeScript class provides a clean interface for React components to interact with the archive API.

## Frequently Asked Questions

### How does Plane prevent archived issues from appearing in standard queries?

Plane implements a custom **IssueManager** in [`apps/api/plane/db/models/issue.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/db/models/issue.py) that automatically appends `archived_at__isnull=True` to every queryset. This means `Issue.objects.all()` and related queries only return active issues. To access archived issues, the codebase uses a separate manager or explicit filters that include non-null `archived_at` values.

### Can any issue be archived, or are there restrictions?

The API enforces strict state validation before archiving. As implemented in [`apps/api/plane/app/views/issue/archive.py`](https://github.com/makeplane/plane/blob/main/apps/api/plane/app/views/issue/archive.py), the `archive()` method checks that `issue.state.group` is either `"completed"` or `"cancelled"`. If an issue is in an active or backlog state, the API returns a 400 Bad Request error with the message "Can only archive completed or cancelled state group issue."

### How does the bulk archive functionality handle partial failures?

The bulk archive endpoint iterates through the provided issue IDs individually, validating each issue's state before adding it to the archive list. Successfully validated issues are archived via a single `bulk_update` operation. While the provided code excerpt shows the happy path, production implementations typically validate that all issues are archivable before performing the bulk update, or return specific error messages for invalid issue IDs in the batch.

### Is there a way to permanently delete archived issues?

The provided source code analysis focuses on soft-delete and restore functionality. While the `archived_at` field enables restoration, the system preserves the underlying database records indefinitely. For permanent deletion, Plane would likely implement separate administrative functions or data retention policies that operate outside the standard archive/restore workflow described in these files.