> ## Documentation Index
> Fetch the complete documentation index at: https://docs.encord.com/llms.txt
> Use this file to discover all available pages before exploring further.

# How To

Editor logs serve as a comprehensive record of actions performed within the Encord platform's Label Editor.

## View Editor Logs

The following method is available on the Project class to retrieve Editor Logs. `start_time` and `end_time` are required parameters, while the rest are optional filters.

[/sdk-documentation/projects-sdk/sdk-activity-logs#object-actions](/sdk-documentation/projects-sdk/sdk-activity-logs#object-actions)

```python theme={"dark"}
def get_editor_logs(
        start_time: datetime.datetime,
        end_time: datetime.datetime,
        action: Optional[str] = None,
        actor_user_email: Optional[str] = None,
        workflow_stage_id: Optional[UUID] = None,
        data_unit_id: Optional[UUID] = None) -> Iterator[EditorLog]
```

Use the following script to view Editor Logs for a specific project and data unit.

```python theme={"dark"}
import datetime
from encord import EncordUserClient

SSH_PATH = "<your_private_key>"
PROJECT_ID = "<project_id>"
DATA_UNIT_ID = "<data_unit_id>"

# Authenticate
user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    domain="https://api.encord.com",
)

# Get the project
project = user_client.get_project(PROJECT_ID)

# Define time range for logs
start_time = datetime.datetime.now() - datetime.timedelta(days=29)
end_time = datetime.datetime.now()

# Logs can be filtered by "action", "actor_user_email", "workflow_stage_id", "data_unit_id"
logs = project.get_editor_logs(
    start_time=start_time,
    end_time=end_time,
)

# Specific logs can be retrieved by passing additional filters
for log in logs:
    print(log.branch_name)
```

## View Task Actions

Task actions can be retrieved using the following method on the Project class. `after` is a required parameter, while the rest are optional filters.

<Info>
  See the full list of task actions [here](/sdk-documentation/projects-sdk/sdk-activity-logs#task-actions).
</Info>

```python theme={"dark"}
def get_task_actions(
    *,
    after: datetime.datetime,
    before: Optional[datetime.datetime] = None,
    actor_email: Union[str, List[str], None] = None,
    action_type: Union[TaskActionType, List[TaskActionType], None] = None,
    workflow_stage_uuid: Union[UUID, List[UUID], str, List[str], None] = None,
    data_unit_uuid: Union[UUID, List[UUID], str, List[str], None] = None
) -> Iterable[TaskAction]
```

The following script retrieves task actions for a specific Project and data unit, filtering for actions that occurred in the last 7 days.

```python theme={"dark"}
import datetime
from encord import EncordUserClient

SSH_PATH = "<your_private_key>"
PROJECT_ID = "<project_id>"
DATA_UNIT_ID = "<data_unit_id>"

# Authenticate
user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    domain="https://api.encord.com",
)

# Get the project
project = user_client.get_project(PROJECT_ID)

# Get task action type and the user that made the action
seven_days_ago = datetime.now() - timedelta(days=7)
for action in project.get_task_actions(after=seven_days_ago):
    print(f"{action.action_type} by {action.actor_email}")
```

## Use Cases

### Get History of a Label

```python theme={"dark"}
import datetime
from encord import EncordUserClient

SSH_PATH = "<your_private_key>"
PROJECT_ID = "<project_id>"
DATA_UNIT_ID = "<data_unit_id>"

# Authenticate
user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
    ssh_private_key_path=SSH_PATH,
    domain="https://api.encord.com",
)

# Get the project
project = user_client.get_project(PROJECT_ID)

# Define time range for logs
start_time = datetime.datetime.now() - datetime.timedelta(days=29)
end_time = datetime.datetime.now()

# Logs can be filtered by "action", "actor_user_email", "workflow_stage_id", "data_unit_id"
logs = project.get_editor_logs(
    start_time=start_time,
    end_time=end_time,
)

# Filter log by the name of the label
log_in_branch_a = [
    log for log in logs
    if log.labelName == "Porpoise"
]
```

### Approval and Rejection Rates

The following script prints the approval and rejection rates for users in a Project.

<CodeGroup>
  ```python Reviewers theme={"dark"}
  import datetime
  from collections import defaultdict
  from encord import EncordUserClient
  from encord.analytics import TaskActionType

  SSH_PATH = "<your_private_key>"
  PROJECT_ID = "<project_id>"

  user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      domain="https://api.encord.com",
  )

  # Get the project
  project = user_client.get_project(PROJECT_ID)

  # Define time range
  start_time = datetime.datetime.now() - datetime.timedelta(days=29)

  # Fetch only approve and reject actions
  actions = list(project.get_task_actions(
      after=start_time,
      action_type=[TaskActionType.APPROVE, TaskActionType.REJECT],
  ))

  # --- Overall rates ---
  total = len(actions)
  approvals = [a for a in actions if a.action_type == TaskActionType.APPROVE]
  rejections = [a for a in actions if a.action_type == TaskActionType.REJECT]

  print("=== Overall Review Rates ===")
  if not total:
      print("No actions found.")
  else:
      print(f"Total review actions : {total}")
      print(f"Approvals            : {len(approvals)} ({len(approvals) / total * 100:.1f}%)")
      print(f"Rejections           : {len(rejections)} ({len(rejections) / total * 100:.1f}%)")

  # --- Per-reviewer breakdown ---
  reviewer_stats = defaultdict(lambda: {"approved": 0, "rejected": 0})

  for action in actions:
      email = action.actor_email
      if action.action_type == TaskActionType.APPROVE:
          reviewer_stats[email]["approved"] += 1
      elif action.action_type == TaskActionType.REJECT:
          reviewer_stats[email]["rejected"] += 1

  print("\n=== Per-Reviewer Breakdown ===")
  print(f"{'Reviewer':<35} {'Approved':>10} {'Rejected':>10} {'Approval Rate':>15}")
  print("-" * 72)
  for email, stats in sorted(reviewer_stats.items()):
      total_by_reviewer = stats["approved"] + stats["rejected"]
      rate = stats["approved"] / total_by_reviewer * 100 if total_by_reviewer else 0
      print(f"{email:<35} {stats['approved']:>10} {stats['rejected']:>10} {rate:>14.1f}%")
  ```

  ```python Annotators theme={"dark"}
  import datetime
  from collections import defaultdict
  from encord import EncordUserClient
  from encord.analytics import TaskActionType

  SSH_PATH = "<your_private_key>"
  PROJECT_ID = "<project_id>"

  # Authenticate
  user_client: EncordUserClient = EncordUserClient.create_with_ssh_private_key(
      ssh_private_key_path=SSH_PATH,
      domain="https://api.encord.com",
  )

  # Get the project
  project = user_client.get_project(PROJECT_ID)

  # Define time range
  start_time = datetime.datetime.now() - datetime.timedelta(days=29)

  # Get task actions
  actions = list(project.get_task_actions(
      after=start_time,
      action_type=[TaskActionType.SUBMIT, TaskActionType.APPROVE, TaskActionType.REJECT],
  ))

  # Group by task, sorted by time
  by_task = defaultdict(list)
  for action in actions:
      by_task[action.task_uuid].append(action)
  for events in by_task.values():
      events.sort(key=lambda a: a.timestamp)

  # For each approve/reject, attribute it to the annotator who last submitted
  annotator_stats = defaultdict(lambda: {"approved": 0, "rejected": 0})

  for events in by_task.values():
      current_annotator = None
      for event in events:
          if event.action_type == TaskActionType.SUBMIT:
              current_annotator = event.actor_email
          elif event.action_type == TaskActionType.APPROVE and current_annotator:
              annotator_stats[current_annotator]["approved"] += 1
          elif event.action_type == TaskActionType.REJECT and current_annotator:
              annotator_stats[current_annotator]["rejected"] += 1

  print("=== Approval & Rejection Rates per Annotator ===")
  if not annotator_stats:
      print("No data found.")
  else:
      print(f"{'Annotator':<35} {'Approved':>10} {'Rejected':>10} {'Total':>8} {'Approval Rate':>15}")
      print("-" * 80)
      for email, stats in sorted(annotator_stats.items()):
          total = stats["approved"] + stats["rejected"]
          rate = stats["approved"] / total * 100 if total else 0
          print(f"{email:<35} {stats['approved']:>10} {stats['rejected']:>10} {total:>8} {rate:>14.1f}%")
  ```
</CodeGroup>

## Migrating from Activity Logs to New Editor Logs

### Overview

Encord introduced a Editor Logs to replace the deprecated Activity Logs. The new logs are more structured, scalable, and aligned with how annotation actions are actually performed in the editor.

Migrating to Editor Logs is not just a rename. Using Editor logs is a shift to a more structured, scalable logging system that better reflects real annotation workflows.

After migration you will benefit from:

* Faster queries
* Cleaner filtering
* More reliable audit trails

If you're currently using the deprecated logs, this content will help you transition smoothly.

**Key Differences**

| Deprecated Activity Logs       | New Editor Logs                       |
| ------------------------------ | ------------------------------------- |
| Flat log structure             | Structured, event-based logs          |
| Limited filtering              | Advanced filtering and querying       |
| Loosely tied to editor actions | Directly reflects editor interactions |
| Less scalable                  | Designed for high-volume workflows    |
| Deprecated endpoint            | Actively maintained API               |

Conceptual Shift

The biggest change is how logs are modeled:

Before: Logs were general activity records (who did what, loosely defined)
Now: Logs are editor events, tightly scoped to annotation actions

Think of the new logs as a timeline of precise annotation events, not just audit entries.

### SDK Migration

**1. Replace Deprecated Log Calls**

Deprecated:

```python theme={"dark"}
project.get_activity_logs()
```

Editor Logs:

```python theme={"dark"}
project.get_editor_logs()
```

**2. Update Filtering Logic**

Deprecated logs often required manual filtering after retrieval.

Deprecated:

```python theme={"dark"}
logs = project.get_activity_logs()

filtered = [
    log for log in logs
    if log["action"] == "LABEL_CREATED"
]
```

Editor Logs:

Benefit: Reduced payload size and faster queries

```python theme={"dark"}
logs = project.get_editor_logs(
    event_types=["LABEL_CREATED"]
)
```

**3. Adjust to New Schema**

The structure of each log entry has changed.

Deprecated:

```json theme={"dark"}
{
  "user": "user@email.com",
  "action": "LABEL_CREATED",
  "timestamp": "...",
  "details": {...}
}
```

Editor Logs:

```json theme={"dark"}
{
  "event_type": "LABEL_CREATED",
  "actor": {
    "email": "user@email.com"
  },
  "created_at": "...",
  "payload": {...}
}
```

**Key Field Changes**

| Deprecated  | New           |
| ----------- | ------------- |
| `action`    | `event_type`  |
| `user`      | `actor.email` |
| `timestamp` | `created_at`  |
| `details`   | `payload`     |

### Common Migration Patterns

#### Pattern 1: Tracking Label Creation

Before:

```python theme={"dark"}
logs = project.get_activity_logs()

label_creations = [
    log for log in logs
    if log["action"] == "LABEL_CREATED"
]
```

After:

```python theme={"dark"}
label_creations = project.get_editor_logs(
    event_types=["LABEL_CREATED"]
)
```

#### Pattern 2: Filtering by User

Before:

```python theme={"dark"}
[user_log for user_log in logs if user_log["user"] == email]
```

After:

```python theme={"dark"}
project.get_editor_logs(
    actor_emails=[email]
)
```

#### Pattern 3: Time-Based Queries

Before:

```python theme={"dark"}
[log for log in logs if log["timestamp"] > start_time]
```

After:

```python theme={"dark"}
project.get_editor_logs(
    created_after=start_time
)
```

### Migration Checklist

* Replace all get\_activity\_logs() calls
* Update filtering to use server-side parameters
* Refactor log parsing to new schema (event\_type, actor, payload)
* Validate downstream systems (analytics, dashboards, exports)
* Remove deprecated logic and fallbacks

### Tips and Tricks

1. Event Granularity is Higher

You may see more logs than before because actions are more finely tracked.

Plan for:

Increased volume
More precise filtering

2. Payload Structure Varies by Event

Different event\_types have different payload schemas.

Recommendation:

```python theme={"dark"}
if log["event_type"] == "LABEL_CREATED":
    handle_label_created(log["payload"])
```

3. Pagination Matters

The new logs are designed for scale.

Always handle pagination:

```python theme={"dark"}
logs = project.get_editor_logs(limit=100, cursor=cursor)
```

4. Backward Compatibility

Deprecated logs will be removed.

Do not rely on:

* Old endpoints
* Old schema fields

### Example Migration

Before:

```python theme={"dark"}
logs = project.get_activity_logs()

for log in logs:
    if log["action"] == "LABEL_CREATED":
        print(log["user"], log["details"])
```

After:

```python theme={"dark"}
logs = project.get_editor_logs(event_types=["LABEL_CREATED"])

for log in logs:
    print(log["actor"]["email"], log["payload"])
```
