time-tracking

SKILL.md

Time Tracking Integration Skill

Master time tracking integrations with RescueTime and Toggl Track APIs for automated time entry, comprehensive reporting, productivity analytics, and project/task attribution. Build powerful time management automations and dashboards.

When to Use This Skill

USE Time Tracking APIs when:

  • Automating time entries - Log time from scripts, CI/CD, or other tools
  • Building productivity dashboards - Aggregate time data for analysis
  • Project billing - Track billable hours automatically
  • Focus time analysis - Understand productivity patterns
  • Team time management - Aggregate team time data
  • Integration pipelines - Connect time tracking with task managers
  • Automated reporting - Generate time reports programmatically
  • Custom analytics - Build specialized productivity insights

DON'T USE Time Tracking APIs when:

  • Simple manual tracking - Use native apps instead
  • Real-time monitoring - APIs have rate limits
  • Invasive employee surveillance - Ethical concerns
  • Complex invoicing - Use dedicated billing software

Prerequisites

Toggl Track Setup

# Get API token from:
# https://track.toggl.com/profile (scroll to API Token)

# Set environment variable
export TOGGL_API_TOKEN="your-api-token"

# Toggl uses HTTP Basic Auth with token as username, "api_token" as password
# Or you can use the token directly

# Verify authentication
curl -s -u "$TOGGL_API_TOKEN:api_token" \
    "https://api.track.toggl.com/api/v9/me" | jq '.fullname'

RescueTime Setup

# Get API Key from:
# https://www.rescuetime.com/anapi/manage

# Set environment variable
export RESCUETIME_API_KEY="your-api-key"

# Verify authentication
curl -s "https://www.rescuetime.com/anapi/data?key=$RESCUETIME_API_KEY&format=json&restrict_kind=overview" | jq

Python Setup

# Install dependencies
pip install requests python-dateutil pandas

# Using uv
uv pip install requests python-dateutil pandas

# Optional: For Toggl SDK
pip install toggl-python

# Verify
python -c "import requests; print('Ready for time tracking integration!')"

Core Capabilities

1. Toggl Track - Time Entries

REST API - Time Entries:

# Get current running time entry
curl -s -u "$TOGGL_API_TOKEN:api_token" \
    "https://api.track.toggl.com/api/v9/me/time_entries/current" | jq

# Get recent time entries (last 7 days by default)
curl -s -u "$TOGGL_API_TOKEN:api_token" \
    "https://api.track.toggl.com/api/v9/me/time_entries" | jq

# Get time entries with date range
START_DATE=$(date -d "7 days ago" +%Y-%m-%dT00:00:00Z)
END_DATE=$(date +%Y-%m-%dT23:59:59Z)

curl -s -u "$TOGGL_API_TOKEN:api_token" \
    "https://api.track.toggl.com/api/v9/me/time_entries?start_date=$START_DATE&end_date=$END_DATE" | jq

# Start a time entry (timer)
curl -s -X POST -u "$TOGGL_API_TOKEN:api_token" \
    -H "Content-Type: application/json" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/time_entries" \
    -d '{
        "created_with": "api",
        "description": "Working on project documentation",
        "tags": ["documentation", "writing"],
        "billable": false,
        "workspace_id": WORKSPACE_ID,
        "project_id": PROJECT_ID,
        "start": "'$(date -u +%Y-%m-%dT%H:%M:%S.000Z)'",
        "duration": -1
    }' | jq

# Stop running time entry
curl -s -X PATCH -u "$TOGGL_API_TOKEN:api_token" \
    -H "Content-Type: application/json" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/time_entries/TIME_ENTRY_ID/stop" | jq

# Create completed time entry
curl -s -X POST -u "$TOGGL_API_TOKEN:api_token" \
    -H "Content-Type: application/json" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/time_entries" \
    -d '{
        "created_with": "api",
        "description": "Code review session",
        "workspace_id": WORKSPACE_ID,
        "project_id": PROJECT_ID,
        "start": "2025-01-15T09:00:00.000Z",
        "duration": 3600,
        "tags": ["review", "development"]
    }' | jq

# Update time entry
curl -s -X PUT -u "$TOGGL_API_TOKEN:api_token" \
    -H "Content-Type: application/json" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/time_entries/TIME_ENTRY_ID" \
    -d '{
        "description": "Updated description",
        "tags": ["updated-tag"]
    }' | jq

# Delete time entry
curl -s -X DELETE -u "$TOGGL_API_TOKEN:api_token" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/time_entries/TIME_ENTRY_ID"

Python - Toggl Time Entries:

import requests
from datetime import datetime, timedelta
from base64 import b64encode
import os

class TogglClient:
    """Toggl Track API client."""

    BASE_URL = "https://api.track.toggl.com/api/v9"

    def __init__(self, api_token=None):
        self.api_token = api_token or os.environ.get("TOGGL_API_TOKEN")
        self.auth = (self.api_token, "api_token")

    def _request(self, method, endpoint, data=None):
        """Make API request."""
        url = f"{self.BASE_URL}{endpoint}"
        response = requests.request(
            method,
            url,
            auth=self.auth,
            json=data,
            headers={"Content-Type": "application/json"}
        )
        response.raise_for_status()
        return response.json() if response.text else None

    def get_me(self):
        """Get current user info."""
        return self._request("GET", "/me")

    def get_workspaces(self):
        """Get user's workspaces."""
        return self._request("GET", "/workspaces")

    def get_current_entry(self):
        """Get currently running time entry."""
        return self._request("GET", "/me/time_entries/current")

    def get_time_entries(self, start_date=None, end_date=None):
        """Get time entries within date range."""
        params = []
        if start_date:
            params.append(f"start_date={start_date.isoformat()}Z")
        if end_date:
            params.append(f"end_date={end_date.isoformat()}Z")

        query = f"?{'&'.join(params)}" if params else ""
        return self._request("GET", f"/me/time_entries{query}")

    def start_timer(self, workspace_id, description, project_id=None, tags=None, billable=False):
        """Start a new timer."""
        data = {
            "created_with": "python_api",
            "description": description,
            "workspace_id": workspace_id,
            "billable": billable,
            "start": datetime.utcnow().strftime("%Y-%m-%dT%H:%M:%S.000Z"),
            "duration": -1  # -1 indicates running timer
        }

        if project_id:
            data["project_id"] = project_id
        if tags:
            data["tags"] = tags

        return self._request(
            "POST",
            f"/workspaces/{workspace_id}/time_entries",
            data
        )

    def stop_timer(self, workspace_id, entry_id):
        """Stop a running timer."""
        return self._request(
            "PATCH",
            f"/workspaces/{workspace_id}/time_entries/{entry_id}/stop"
        )

    def create_time_entry(
        self,
        workspace_id,
        description,
        start_time,
        duration_seconds,
        project_id=None,
        tags=None,
        billable=False
    ):
        """Create a completed time entry."""
        data = {
            "created_with": "python_api",
            "description": description,
            "workspace_id": workspace_id,
            "start": start_time.strftime("%Y-%m-%dT%H:%M:%S.000Z"),
            "duration": duration_seconds,
            "billable": billable
        }

        if project_id:
            data["project_id"] = project_id
        if tags:
            data["tags"] = tags

        return self._request(
            "POST",
            f"/workspaces/{workspace_id}/time_entries",
            data
        )

    def update_time_entry(self, workspace_id, entry_id, **kwargs):
        """Update an existing time entry."""
        return self._request(
            "PUT",
            f"/workspaces/{workspace_id}/time_entries/{entry_id}",
            kwargs
        )

    def delete_time_entry(self, workspace_id, entry_id):
        """Delete a time entry."""
        return self._request(
            "DELETE",
            f"/workspaces/{workspace_id}/time_entries/{entry_id}"
        )


# Example usage
if __name__ == "__main__":
    client = TogglClient()

    # Get user info
    user = client.get_me()
    print(f"User: {user['fullname']}")

    # Get workspaces
    workspaces = client.get_workspaces()
    workspace_id = workspaces[0]["id"]
    print(f"Workspace: {workspaces[0]['name']}")

    # Start timer
    entry = client.start_timer(
        workspace_id=workspace_id,
        description="Working on API integration",
        tags=["development", "api"]
    )
    print(f"Started timer: {entry['id']}")

    # Get current timer
    current = client.get_current_entry()
    if current:
        print(f"Running: {current['description']}")

    # Stop timer
    stopped = client.stop_timer(workspace_id, entry["id"])
    print(f"Stopped: Duration {stopped['duration']} seconds")

2. Toggl Track - Projects and Clients

REST API - Projects:

# Get workspace projects
curl -s -u "$TOGGL_API_TOKEN:api_token" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/projects" | jq

# Create project
curl -s -X POST -u "$TOGGL_API_TOKEN:api_token" \
    -H "Content-Type: application/json" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/projects" \
    -d '{
        "name": "Client Website Redesign",
        "color": "#0b83d9",
        "is_private": false,
        "active": true,
        "client_id": CLIENT_ID,
        "billable": true,
        "estimated_hours": 100
    }' | jq

# Get clients
curl -s -u "$TOGGL_API_TOKEN:api_token" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/clients" | jq

# Create client
curl -s -X POST -u "$TOGGL_API_TOKEN:api_token" \
    -H "Content-Type: application/json" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/clients" \
    -d '{
        "name": "Acme Corporation",
        "notes": "Primary contact: John Doe"
    }' | jq

# Get tags
curl -s -u "$TOGGL_API_TOKEN:api_token" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/tags" | jq

# Create tag
curl -s -X POST -u "$TOGGL_API_TOKEN:api_token" \
    -H "Content-Type: application/json" \
    "https://api.track.toggl.com/api/v9/workspaces/WORKSPACE_ID/tags" \
    -d '{"name": "urgent"}' | jq

Python - Projects and Clients:

def get_projects(self, workspace_id):
    """Get all projects in workspace."""
    return self._request("GET", f"/workspaces/{workspace_id}/projects")

def create_project(
    self,
    workspace_id,
    name,
    client_id=None,
    color="#0b83d9",
    billable=False,
    estimated_hours=None
):
    """Create a new project."""
    data = {
        "name": name,
        "color": color,
        "billable": billable,
        "active": True
    }

    if client_id:
        data["client_id"] = client_id
    if estimated_hours:
        data["estimated_hours"] = estimated_hours

    return self._request(
        "POST",
        f"/workspaces/{workspace_id}/projects",
        data
    )

def get_clients(self, workspace_id):
    """Get all clients in workspace."""
    return self._request("GET", f"/workspaces/{workspace_id}/clients")

def create_client(self, workspace_id, name, notes=None):
    """Create a new client."""
    data = {"name": name}
    if notes:
        data["notes"] = notes

    return self._request(
        "POST",
        f"/workspaces/{workspace_id}/clients",
        data
    )

3. Toggl Track - Reports

Reports API:

# Summary report
curl -s -X POST -u "$TOGGL_API_TOKEN:api_token" \
    -H "Content-Type: application/json" \
    "https://api.track.toggl.com/reports/api/v3/workspace/WORKSPACE_ID/summary/time_entries" \
    -d '{
        "start_date": "2025-01-01",
        "end_date": "2025-01-31",
        "grouping": "projects",
        "sub_grouping": "users"
    }' | jq

# Detailed report
curl -s -X POST -u "$TOGGL_API_TOKEN:api_token" \
    -H "Content-Type: application/json" \
    "https://api.track.toggl.com/reports/api/v3/workspace/WORKSPACE_ID/search/time_entries" \
    -d '{
        "start_date": "2025-01-01",
        "end_date": "2025-01-31",
        "page_size": 50,
        "first_row_number": 0
    }' | jq

# Weekly report
curl -s -X POST -u "$TOGGL_API_TOKEN:api_token" \
    -H "Content-Type: application/json" \
    "https://api.track.toggl.com/reports/api/v3/workspace/WORKSPACE_ID/weekly/time_entries" \
    -d '{
        "start_date": "2025-01-06",
        "end_date": "2025-01-12",
        "grouping": "projects"
    }' | jq

Python - Reports:

class TogglReports:
    """Toggl Reports API client."""

    REPORTS_URL = "https://api.track.toggl.com/reports/api/v3"

    def __init__(self, api_token=None):
        self.api_token = api_token or os.environ.get("TOGGL_API_TOKEN")
        self.auth = (self.api_token, "api_token")

    def _request(self, method, endpoint, data=None):
        """Make API request."""
        url = f"{self.REPORTS_URL}{endpoint}"
        response = requests.request(
            method,
            url,
            auth=self.auth,
            json=data,
            headers={"Content-Type": "application/json"}
        )
        response.raise_for_status()
        return response.json()

    def summary_report(
        self,
        workspace_id,
        start_date,
        end_date,
        grouping="projects",
        sub_grouping=None,
        project_ids=None,
        user_ids=None
    ):
        """Generate summary report."""
        data = {
            "start_date": start_date.strftime("%Y-%m-%d"),
            "end_date": end_date.strftime("%Y-%m-%d"),
            "grouping": grouping
        }

        if sub_grouping:
            data["sub_grouping"] = sub_grouping
        if project_ids:
            data["project_ids"] = project_ids
        if user_ids:
            data["user_ids"] = user_ids

        return self._request(
            "POST",
            f"/workspace/{workspace_id}/summary/time_entries",
            data
        )

    def detailed_report(
        self,
        workspace_id,
        start_date,
        end_date,
        page_size=50,
        first_row=0
    ):
        """Generate detailed report with pagination."""
        data = {
            "start_date": start_date.strftime("%Y-%m-%d"),
            "end_date": end_date.strftime("%Y-%m-%d"),
            "page_size": page_size,
            "first_row_number": first_row
        }

        return self._request(
            "POST",
            f"/workspace/{workspace_id}/search/time_entries",
            data
        )

    def weekly_report(
        self,
        workspace_id,
        start_date,
        end_date,
        grouping="projects"
    ):
        """Generate weekly report."""
        data = {
            "start_date": start_date.strftime("%Y-%m-%d"),
            "end_date": end_date.strftime("%Y-%m-%d"),
            "grouping": grouping
        }

        return self._request(
            "POST",
            f"/workspace/{workspace_id}/weekly/time_entries",
            data
        )


# Example usage
if __name__ == "__main__":
    reports = TogglReports()
    workspace_id = 12345678

    # Get summary report
    start = datetime(2025, 1, 1)
    end = datetime(2025, 1, 31)

    summary = reports.summary_report(
        workspace_id=workspace_id,
        start_date=start,
        end_date=end,
        grouping="projects"
    )

    print("Summary Report:")
    for group in summary.get("groups", []):
        total_hours = group.get("seconds", 0) / 3600
        print(f"  {group.get('id')}: {total_hours:.1f} hours")

4. RescueTime - Data Retrieval

REST API - Analytics:

# Get summary data
curl -s "https://www.rescuetime.com/anapi/data" \
    -d "key=$RESCUETIME_API_KEY" \
    -d "format=json" \
    -d "perspective=rank" \
    -d "resolution_time=day" \
    -d "restrict_kind=overview" | jq

# Get data for specific date range
curl -s "https://www.rescuetime.com/anapi/data" \
    -d "key=$RESCUETIME_API_KEY" \
    -d "format=json" \
    -d "restrict_begin=2025-01-01" \
    -d "restrict_end=2025-01-31" \
    -d "restrict_kind=overview" | jq

# Get activity data by category
curl -s "https://www.rescuetime.com/anapi/data" \
    -d "key=$RESCUETIME_API_KEY" \
    -d "format=json" \
    -d "restrict_kind=category" \
    -d "resolution_time=week" | jq

# Get productivity data
curl -s "https://www.rescuetime.com/anapi/data" \
    -d "key=$RESCUETIME_API_KEY" \
    -d "format=json" \
    -d "restrict_kind=productivity" | jq

# Get activity details
curl -s "https://www.rescuetime.com/anapi/data" \
    -d "key=$RESCUETIME_API_KEY" \
    -d "format=json" \
    -d "restrict_kind=activity" \
    -d "resolution_time=day" | jq

# Get efficiency data (requires Premium)
curl -s "https://www.rescuetime.com/anapi/data" \
    -d "key=$RESCUETIME_API_KEY" \
    -d "format=json" \
    -d "restrict_kind=efficiency" | jq

Python - RescueTime Client:

import requests
from datetime import datetime, timedelta
import os

class RescueTimeClient:
    """RescueTime API client."""

    BASE_URL = "https://www.rescuetime.com/anapi"

    def __init__(self, api_key=None):
        self.api_key = api_key or os.environ.get("RESCUETIME_API_KEY")

    def _request(self, endpoint, params=None):
        """Make API request."""
        params = params or {}
        params["key"] = self.api_key
        params["format"] = "json"

        response = requests.get(
            f"{self.BASE_URL}/{endpoint}",
            params=params
        )
        response.raise_for_status()
        return response.json()

    def get_daily_summary(self, date=None):
        """Get daily summary data."""
        params = {
            "perspective": "rank",
            "resolution_time": "day",
            "restrict_kind": "overview"
        }

        if date:
            params["restrict_begin"] = date.strftime("%Y-%m-%d")
            params["restrict_end"] = date.strftime("%Y-%m-%d")

        return self._request("data", params)

    def get_date_range_data(
        self,
        start_date,
        end_date,
        restrict_kind="overview",
        resolution="day"
    ):
        """Get data for date range."""
        params = {
            "restrict_begin": start_date.strftime("%Y-%m-%d"),
            "restrict_end": end_date.strftime("%Y-%m-%d"),
            "restrict_kind": restrict_kind,
            "resolution_time": resolution
        }

        return self._request("data", params)

    def get_productivity_data(self, start_date=None, end_date=None):
        """Get productivity scores."""
        params = {"restrict_kind": "productivity"}

        if start_date:
            params["restrict_begin"] = start_date.strftime("%Y-%m-%d")
        if end_date:
            params["restrict_end"] = end_date.strftime("%Y-%m-%d")

        return self._request("data", params)

    def get_category_data(
        self,
        start_date=None,
        end_date=None,
        resolution="day"
    ):
        """Get data grouped by category."""
        params = {
            "restrict_kind": "category",
            "resolution_time": resolution
        }

        if start_date:
            params["restrict_begin"] = start_date.strftime("%Y-%m-%d")
        if end_date:
            params["restrict_end"] = end_date.strftime("%Y-%m-%d")

        return self._request("data", params)

    def get_activity_data(
        self,
        start_date=None,
        end_date=None,
        resolution="day"
    ):
        """Get detailed activity data."""
        params = {
            "restrict_kind": "activity",
            "resolution_time": resolution
        }

        if start_date:
            params["restrict_begin"] = start_date.strftime("%Y-%m-%d")
        if end_date:
            params["restrict_end"] = end_date.strftime("%Y-%m-%d")

        return self._request("data", params)

    def get_focus_time(self, start_date=None, end_date=None):
        """Calculate focus time from activities."""
        productivity = self.get_productivity_data(start_date, end_date)

        focus_time = 0
        total_time = 0

        for row in productivity.get("rows", []):
            # row format: [rank, time_seconds, productivity]
            time_seconds = row[1]
            productivity_score = row[2]  # -2 to 2

            total_time += time_seconds
            if productivity_score >= 1:  # Productive or very productive
                focus_time += time_seconds

        return {
            "focus_time_seconds": focus_time,
            "focus_time_hours": focus_time / 3600,
            "total_time_seconds": total_time,
            "total_time_hours": total_time / 3600,
            "focus_percentage": (focus_time / total_time * 100) if total_time > 0 else 0
        }


# Example usage
if __name__ == "__main__":
    client = RescueTimeClient()

    # Get today's summary
    today = client.get_daily_summary()
    print("Today's Activity:")
    for row in today.get("rows", [])[:5]:
        print(f"  {row[3]}: {row[1] / 60:.0f} minutes")

    # Get focus time for last week
    start = datetime.now() - timedelta(days=7)
    end = datetime.now()

    focus = client.get_focus_time(start, end)
    print(f"\nFocus Time (Last 7 Days):")
    print(f"  Focus: {focus['focus_time_hours']:.1f} hours")
    print(f"  Total: {focus['total_time_hours']:.1f} hours")
    print(f"  Focus Rate: {focus['focus_percentage']:.1f}%")

5. RescueTime - FocusTime Triggers

FocusTime API:

# Start FocusTime session (requires Premium)
curl -s -X POST "https://www.rescuetime.com/anapi/focustime/start" \
    -d "key=$RESCUETIME_API_KEY" \
    -d "duration=60" | jq  # 60 minutes

# End FocusTime session
curl -s -X POST "https://www.rescuetime.com/anapi/focustime/end" \
    -d "key=$RESCUETIME_API_KEY" | jq

# Get current FocusTime status
curl -s "https://www.rescuetime.com/anapi/focustime/status" \
    -d "key=$RESCUETIME_API_KEY" | jq

Python - FocusTime:

class RescueTimeFocusTime:
    """RescueTime FocusTime API client (Premium required)."""

    BASE_URL = "https://www.rescuetime.com/anapi/focustime"

    def __init__(self, api_key=None):
        self.api_key = api_key or os.environ.get("RESCUETIME_API_KEY")

    def start_focus(self, duration_minutes=60):
        """Start a FocusTime session."""
        response = requests.post(
            f"{self.BASE_URL}/start",
            data={
                "key": self.api_key,
                "duration": duration_minutes
            }
        )
        return response.json()

    def end_focus(self):
        """End current FocusTime session."""
        response = requests.post(
            f"{self.BASE_URL}/end",
            data={"key": self.api_key}
        )
        return response.json()

    def get_status(self):
        """Get current FocusTime status."""
        response = requests.get(
            f"{self.BASE_URL}/status",
            params={"key": self.api_key}
        )
        return response.json()

6. Automated Time Logging

Log Time from Scripts:

#!/usr/bin/env python3
"""auto_time_logger.py - Automated time logging"""

import os
import subprocess
from datetime import datetime, timedelta
from toggl_client import TogglClient  # From earlier example

def log_git_commit_time(repo_path, workspace_id, project_id=None):
    """
    Log time entries based on git commits.
    Estimates time between commits.
    """
    client = TogglClient()

    # Get recent commits
    result = subprocess.run(
        ["git", "-C", repo_path, "log", "--format=%H|%s|%ai", "-n", "20"],
        capture_output=True,
        text=True
    )

    commits = []
    for line in result.stdout.strip().split("\n"):
        if "|" in line:
            hash_val, message, date_str = line.split("|", 2)
            commit_date = datetime.strptime(
                date_str.strip()[:19],
                "%Y-%m-%d %H:%M:%S"
            )
            commits.append({
                "hash": hash_val,
                "message": message,
                "date": commit_date
            })

    # Create time entries between commits
    for i in range(len(commits) - 1):
        current = commits[i]
        previous = commits[i + 1]

        duration = (current["date"] - previous["date"]).total_seconds()

        # Cap at 4 hours max
        duration = min(duration, 4 * 3600)

        # Skip very short intervals
        if duration < 300:  # Less than 5 minutes
            continue

        entry = client.create_time_entry(
            workspace_id=workspace_id,
            description=f"Git: {current['message'][:100]}",
            start_time=previous["date"],
            duration_seconds=int(duration),
            project_id=project_id,
            tags=["git", "auto-logged"]
        )

        print(f"Logged: {current['message'][:50]}... ({duration/3600:.1f}h)")


def log_pomodoro_session(
    workspace_id,
    description,
    project_id=None,
    duration_minutes=25
):
    """Log a completed Pomodoro session."""
    client = TogglClient()

    end_time = datetime.utcnow()
    start_time = end_time - timedelta(minutes=duration_minutes)

    entry = client.create_time_entry(
        workspace_id=workspace_id,
        description=description,
        start_time=start_time,
        duration_seconds=duration_minutes * 60,
        project_id=project_id,
        tags=["pomodoro"]
    )

    print(f"Logged Pomodoro: {description} ({duration_minutes} min)")
    return entry


def log_meeting(
    workspace_id,
    title,
    start_time,
    end_time,
    project_id=None
):
    """Log a meeting time entry."""
    client = TogglClient()

    duration = (end_time - start_time).total_seconds()

    entry = client.create_time_entry(
        workspace_id=workspace_id,
        description=f"Meeting: {title}",
        start_time=start_time,
        duration_seconds=int(duration),
        project_id=project_id,
        tags=["meeting"]
    )

    print(f"Logged meeting: {title} ({duration/3600:.1f}h)")
    return entry

Complete Examples

Example 1: Weekly Time Report Generator

#!/usr/bin/env python3
"""weekly_report.py - Generate weekly time reports"""

import os
from datetime import datetime, timedelta
from collections import defaultdict
import json

# Assuming TogglClient and TogglReports from earlier examples

class WeeklyReportGenerator:
    """Generate comprehensive weekly time reports."""

    def __init__(self, toggl_token=None, rescuetime_key=None):
        self.toggl = TogglClient(toggl_token)
        if rescuetime_key:
            self.rescuetime = RescueTimeClient(rescuetime_key)
        else:
            self.rescuetime = None

    def get_week_dates(self, weeks_ago=0):
        """Get start and end of a week."""
        today = datetime.now().date()
        start = today - timedelta(days=today.weekday() + (7 * weeks_ago))
        end = start + timedelta(days=6)
        return datetime.combine(start, datetime.min.time()), \
               datetime.combine(end, datetime.max.time())

    def generate_toggl_report(self, start_date, end_date):
        """Generate Toggl time report."""
        entries = self.toggl.get_time_entries(start_date, end_date)

        # Aggregate by project and day
        by_project = defaultdict(float)
        by_day = defaultdict(float)
        by_tag = defaultdict(float)
        total_seconds = 0

        for entry in entries or []:
            duration = entry.get("duration", 0)
            if duration < 0:  # Running entry
                duration = (datetime.utcnow() - datetime.fromisoformat(
                    entry["start"].replace("Z", "")
                )).total_seconds()

            total_seconds += duration

            # By project
            project_id = entry.get("project_id", "No Project")
            by_project[project_id] += duration

            # By day
            day = entry["start"][:10]
            by_day[day] += duration

            # By tags
            for tag in entry.get("tags", []):
                by_tag[tag] += duration

        return {
            "total_hours": total_seconds / 3600,
            "by_project": {k: v / 3600 for k, v in by_project.items()},
            "by_day": {k: v / 3600 for k, v in sorted(by_day.items())},
            "by_tag": {k: v / 3600 for k, v in by_tag.items()},
            "entry_count": len(entries or [])
        }

    def generate_rescuetime_report(self, start_date, end_date):
        """Generate RescueTime productivity report."""
        if not self.rescuetime:
            return None

        # Get category data
        category_data = self.rescuetime.get_category_data(start_date, end_date)

        categories = defaultdict(float)
        for row in category_data.get("rows", []):
            category = row[3]  # Category name
            seconds = row[1]
            categories[category] += seconds / 3600

        # Get productivity data
        focus = self.rescuetime.get_focus_time(start_date, end_date)

        return {
            "focus_hours": focus["focus_time_hours"],
            "total_hours": focus["total_time_hours"],
            "focus_percentage": focus["focus_percentage"],
            "by_category": dict(sorted(
                categories.items(),
                key=lambda x: x[1],
                reverse=True
            )[:10])
        }

    def generate_full_report(self, weeks_ago=0):
        """Generate complete weekly report."""
        start, end = self.get_week_dates(weeks_ago)

        report = {
            "period": {
                "start": start.strftime("%Y-%m-%d"),
                "end": end.strftime("%Y-%m-%d"),
                "week_number": start.isocalendar()[1]
            },
            "generated_at": datetime.now().isoformat(),
            "toggl": self.generate_toggl_report(start, end),
            "rescuetime": self.generate_rescuetime_report(start, end)
        }

        return report

    def generate_markdown_report(self, report):
        """Generate markdown formatted report."""
        period = report["period"]

        md = f"""# Weekly Time Report

**Week {period['week_number']}:** {period['start']} to {period['end']}
**Generated:** {report['generated_at'][:10]}

## Tracked Time (Toggl)

| Metric | Value |
|--------|-------|
| Total Hours | {report['toggl']['total_hours']:.1f} |
| Time Entries | {report['toggl']['entry_count']} |

### Hours by Day

| Day | Hours |
|-----|-------|
"""

        for day, hours in report["toggl"]["by_day"].items():
            md += f"| {day} | {hours:.1f} |\n"

        md += "\n### Hours by Project\n\n"
        for project, hours in sorted(
            report["toggl"]["by_project"].items(),
            key=lambda x: x[1],
            reverse=True
        )[:5]:
            md += f"- **{project}**: {hours:.1f} hours\n"

        if report["rescuetime"]:
            rt = report["rescuetime"]
            md += f"""

## Productivity (RescueTime)

| Metric | Value |
|--------|-------|
| Focus Time | {rt['focus_hours']:.1f} hours |
| Total Tracked | {rt['total_hours']:.1f} hours |
| Focus Rate | {rt['focus_percentage']:.1f}% |

### Top Categories

"""
            for category, hours in list(rt["by_category"].items())[:5]:
                md += f"- **{category}**: {hours:.1f} hours\n"

        return md


# Example usage
if __name__ == "__main__":
    generator = WeeklyReportGenerator(
        toggl_token=os.environ.get("TOGGL_API_TOKEN"),
        rescuetime_key=os.environ.get("RESCUETIME_API_KEY")
    )

    # Generate report for current week
    report = generator.generate_full_report(weeks_ago=0)

    # Save JSON
    with open("weekly_report.json", "w") as f:
        json.dump(report, f, indent=2)

    # Generate markdown
    md_report = generator.generate_markdown_report(report)
    with open("weekly_report.md", "w") as f:
        f.write(md_report)

    print("Weekly report generated!")
    print(f"Total tracked: {report['toggl']['total_hours']:.1f} hours")

Example 2: Project Time Attribution

#!/usr/bin/env python3
"""project_attribution.py - Attribute time to projects automatically"""

import os
import re
from datetime import datetime, timedelta
from collections import defaultdict

class ProjectTimeAttributor:
    """Automatically attribute time entries to projects."""

    def __init__(self, toggl_token):
        self.toggl = TogglClient(toggl_token)
        self.workspace_id = None
        self.projects = {}
        self.rules = []

    def load_workspace(self):
        """Load workspace and projects."""
        workspaces = self.toggl.get_workspaces()
        self.workspace_id = workspaces[0]["id"]

        projects = self.toggl.get_projects(self.workspace_id)
        self.projects = {p["name"]: p["id"] for p in projects}

        print(f"Loaded {len(self.projects)} projects")

    def add_rule(self, pattern, project_name, tags=None):
        """
        Add attribution rule.

        Args:
            pattern: Regex pattern to match description
            project_name: Project to assign
            tags: Optional tags to add
        """
        if project_name not in self.projects:
            print(f"Warning: Project '{project_name}' not found")
            return

        self.rules.append({
            "pattern": re.compile(pattern, re.IGNORECASE),
            "project_id": self.projects[project_name],
            "project_name": project_name,
            "tags": tags or []
        })

    def process_entries(self, days_back=7, dry_run=True):
        """
        Process time entries and apply attribution rules.

        Args:
            days_back: Number of days to process
            dry_run: If True, don't make changes

        Returns:
            Summary of changes
        """
        start = datetime.utcnow() - timedelta(days=days_back)
        end = datetime.utcnow()

        entries = self.toggl.get_time_entries(start, end)

        changes = {
            "processed": 0,
            "attributed": 0,
            "skipped": 0,
            "details": []
        }

        for entry in entries or []:
            changes["processed"] += 1

            # Skip if already has project
            if entry.get("project_id"):
                changes["skipped"] += 1
                continue

            description = entry.get("description", "")

            # Try each rule
            for rule in self.rules:
                if rule["pattern"].search(description):
                    change = {
                        "entry_id": entry["id"],
                        "description": description[:50],
                        "project": rule["project_name"],
                        "tags": rule["tags"]
                    }

                    if not dry_run:
                        update_data = {"project_id": rule["project_id"]}
                        if rule["tags"]:
                            existing_tags = entry.get("tags", [])
                            update_data["tags"] = list(set(existing_tags + rule["tags"]))

                        self.toggl.update_time_entry(
                            self.workspace_id,
                            entry["id"],
                            **update_data
                        )

                    changes["attributed"] += 1
                    changes["details"].append(change)
                    break

        return changes


# Example usage
if __name__ == "__main__":
    attributor = ProjectTimeAttributor(os.environ["TOGGL_API_TOKEN"])
    attributor.load_workspace()

    # Define attribution rules
    attributor.add_rule(
        r"(meeting|standup|sync)",
        "Internal - Meetings",
        tags=["meeting"]
    )

    attributor.add_rule(
        r"(code review|pr review|review)",
        "Development",
        tags=["review"]
    )

    attributor.add_rule(
        r"(bug|fix|hotfix)",
        "Development",
        tags=["bugfix"]
    )

    attributor.add_rule(
        r"(docs|documentation|readme)",
        "Documentation",
        tags=["docs"]
    )

    attributor.add_rule(
        r"(email|slack|communication)",
        "Internal - Communication",
        tags=["communication"]
    )

    # Process entries (dry run first)
    print("\n=== DRY RUN ===")
    changes = attributor.process_entries(days_back=7, dry_run=True)

    print(f"Processed: {changes['processed']}")
    print(f"Would attribute: {changes['attributed']}")
    print(f"Already assigned: {changes['skipped']}")

    if changes["details"]:
        print("\nChanges:")
        for change in changes["details"][:10]:
            print(f"  {change['description'][:30]}... -> {change['project']}")

    # Uncomment to apply changes
    # print("\n=== APPLYING CHANGES ===")
    # changes = attributor.process_entries(days_back=7, dry_run=False)

Example 3: Productivity Dashboard Data

#!/usr/bin/env python3
"""productivity_dashboard.py - Generate dashboard data"""

import os
from datetime import datetime, timedelta
import json

class ProductivityDashboard:
    """Generate data for productivity dashboard."""

    def __init__(self, toggl_token, rescuetime_key=None):
        self.toggl = TogglClient(toggl_token)
        self.rescuetime = RescueTimeClient(rescuetime_key) if rescuetime_key else None

    def get_daily_metrics(self, date=None):
        """Get metrics for a single day."""
        if date is None:
            date = datetime.now().date()

        start = datetime.combine(date, datetime.min.time())
        end = datetime.combine(date, datetime.max.time())

        metrics = {
            "date": date.isoformat(),
            "toggl": {},
            "rescuetime": {}
        }

        # Toggl metrics
        entries = self.toggl.get_time_entries(start, end) or []
        total_seconds = sum(
            e.get("duration", 0) for e in entries
            if e.get("duration", 0) > 0
        )

        metrics["toggl"] = {
            "total_hours": total_seconds / 3600,
            "entry_count": len(entries),
            "billable_hours": sum(
                e.get("duration", 0) / 3600
                for e in entries
                if e.get("billable") and e.get("duration", 0) > 0
            )
        }

        # RescueTime metrics
        if self.rescuetime:
            focus = self.rescuetime.get_focus_time(
                datetime.combine(date, datetime.min.time()),
                datetime.combine(date, datetime.max.time())
            )

            metrics["rescuetime"] = {
                "focus_hours": focus["focus_time_hours"],
                "total_hours": focus["total_time_hours"],
                "focus_rate": focus["focus_percentage"]
            }

        return metrics

    def get_weekly_trend(self, weeks=4):
        """Get weekly trend data."""
        trends = []
        today = datetime.now().date()

        for week_offset in range(weeks):
            week_start = today - timedelta(
                days=today.weekday() + (7 * week_offset)
            )
            week_end = week_start + timedelta(days=6)

            start_dt = datetime.combine(week_start, datetime.min.time())
            end_dt = datetime.combine(week_end, datetime.max.time())

            # Toggl data
            entries = self.toggl.get_time_entries(start_dt, end_dt) or []
            total_hours = sum(
                e.get("duration", 0) / 3600
                for e in entries
                if e.get("duration", 0) > 0
            )

            week_data = {
                "week_start": week_start.isoformat(),
                "week_number": week_start.isocalendar()[1],
                "toggl_hours": round(total_hours, 1)
            }

            # RescueTime data
            if self.rescuetime:
                focus = self.rescuetime.get_focus_time(start_dt, end_dt)
                week_data["focus_hours"] = round(focus["focus_time_hours"], 1)
                week_data["focus_rate"] = round(focus["focus_percentage"], 1)

            trends.append(week_data)

        return list(reversed(trends))

    def get_project_breakdown(self, days=30):
        """Get time breakdown by project."""
        start = datetime.utcnow() - timedelta(days=days)
        end = datetime.utcnow()

        entries = self.toggl.get_time_entries(start, end) or []

        by_project = defaultdict(float)
        for entry in entries:
            duration = entry.get("duration", 0)
            if duration > 0:
                project = entry.get("project_id", "No Project")
                by_project[project] += duration / 3600

        # Sort by hours
        sorted_projects = sorted(
            by_project.items(),
            key=lambda x: x[1],
            reverse=True
        )

        return [
            {"project_id": p, "hours": round(h, 1)}
            for p, h in sorted_projects
        ]

    def generate_dashboard_data(self):
        """Generate complete dashboard data."""
        today = datetime.now().date()

        return {
            "generated_at": datetime.now().isoformat(),
            "today": self.get_daily_metrics(today),
            "yesterday": self.get_daily_metrics(today - timedelta(days=1)),
            "weekly_trend": self.get_weekly_trend(weeks=4),
            "project_breakdown": self.get_project_breakdown(days=30)
        }


# Example usage
if __name__ == "__main__":
    dashboard = ProductivityDashboard(
        toggl_token=os.environ["TOGGL_API_TOKEN"],
        rescuetime_key=os.environ.get("RESCUETIME_API_KEY")
    )

    data = dashboard.generate_dashboard_data()

    # Save dashboard data
    with open("dashboard_data.json", "w") as f:
        json.dump(data, f, indent=2)

    # Print summary
    print("Dashboard Data Generated")
    print(f"\nToday: {data['today']['toggl']['total_hours']:.1f} hours tracked")

    if data['today'].get('rescuetime'):
        print(f"Focus rate: {data['today']['rescuetime']['focus_rate']:.1f}%")

    print(f"\nWeekly Trend:")
    for week in data['weekly_trend']:
        print(f"  Week {week['week_number']}: {week['toggl_hours']} hours")

Integration Examples

Time Tracking with GitHub Actions

# .github/workflows/log-time.yml
name: Log Development Time

on:
  push:
    branches: [main, develop]

jobs:
  log-time:
    runs-on: ubuntu-latest
    steps:
      - name: Log commit time to Toggl
        env:
          TOGGL_API_TOKEN: ${{ secrets.TOGGL_API_TOKEN }}
          TOGGL_WORKSPACE_ID: ${{ secrets.TOGGL_WORKSPACE_ID }}
          TOGGL_PROJECT_ID: ${{ secrets.TOGGL_PROJECT_ID }}
        run: |
          # Log 30 minute entry for each commit
          DESCRIPTION="Commit: ${{ github.event.head_commit.message }}"
          START_TIME=$(date -u -d '30 minutes ago' +%Y-%m-%dT%H:%M:%S.000Z)

          curl -s -X POST -u "$TOGGL_API_TOKEN:api_token" \
            -H "Content-Type: application/json" \
            "https://api.track.toggl.com/api/v9/workspaces/$TOGGL_WORKSPACE_ID/time_entries" \
            -d '{
              "created_with": "github_actions",
              "description": "'"${DESCRIPTION:0:100}"'",
              "workspace_id": '"$TOGGL_WORKSPACE_ID"',
              "project_id": '"$TOGGL_PROJECT_ID"',
              "start": "'"$START_TIME"'",
              "duration": 1800,
              "tags": ["github", "automated"]
            }'

Slack Daily Summary

#!/usr/bin/env python3
"""slack_time_summary.py - Post daily time summary to Slack"""

import os
import requests
from datetime import datetime, timedelta

def post_daily_summary(toggl_token, slack_webhook):
    """Post daily time summary to Slack."""
    toggl = TogglClient(toggl_token)

    today = datetime.now().date()
    start = datetime.combine(today, datetime.min.time())
    end = datetime.combine(today, datetime.max.time())

    entries = toggl.get_time_entries(start, end) or []

    total_hours = sum(
        e.get("duration", 0) / 3600
        for e in entries
        if e.get("duration", 0) > 0
    )

    # Build message
    message = {
        "blocks": [
            {
                "type": "header",
                "text": {
                    "type": "plain_text",
                    "text": f"Daily Time Summary - {today}"
                }
            },
            {
                "type": "section",
                "fields": [
                    {
                        "type": "mrkdwn",
                        "text": f"*Total Hours:*\n{total_hours:.1f}"
                    },
                    {
                        "type": "mrkdwn",
                        "text": f"*Entries:*\n{len(entries)}"
                    }
                ]
            }
        ]
    }

    # Add top activities
    if entries:
        activities = "\n".join([
            f"- {e.get('description', 'No description')[:40]}: "
            f"{e.get('duration', 0)/3600:.1f}h"
            for e in sorted(entries, key=lambda x: x.get("duration", 0), reverse=True)[:5]
        ])

        message["blocks"].append({
            "type": "section",
            "text": {
                "type": "mrkdwn",
                "text": f"*Top Activities:*\n{activities}"
            }
        })

    # Post to Slack
    response = requests.post(slack_webhook, json=message)
    return response.status_code == 200


if __name__ == "__main__":
    success = post_daily_summary(
        toggl_token=os.environ["TOGGL_API_TOKEN"],
        slack_webhook=os.environ["SLACK_WEBHOOK_URL"]
    )
    print("Posted to Slack" if success else "Failed to post")

Best Practices

1. Handle Rate Limits

import time
from functools import wraps

def rate_limited(max_per_second=1):
    """Rate limiting decorator."""
    min_interval = 1.0 / max_per_second
    last_call = [0.0]

    def decorator(func):
        @wraps(func)
        def wrapper(*args, **kwargs):
            elapsed = time.time() - last_call[0]
            if elapsed < min_interval:
                time.sleep(min_interval - elapsed)
            result = func(*args, **kwargs)
            last_call[0] = time.time()
            return result
        return wrapper
    return decorator

2. Cache API Responses

from functools import lru_cache
from datetime import datetime

@lru_cache(maxsize=100)
def get_projects_cached(workspace_id, cache_key=None):
    """Get projects with caching."""
    return toggl.get_projects(workspace_id)

# Invalidate cache daily
cache_key = datetime.now().strftime("%Y-%m-%d")
projects = get_projects_cached(workspace_id, cache_key)

3. Validate Time Entries

def validate_time_entry(entry):
    """Validate time entry before creating."""
    if not entry.get("description"):
        raise ValueError("Description is required")

    duration = entry.get("duration", 0)
    if duration > 12 * 3600:  # More than 12 hours
        raise ValueError("Duration exceeds 12 hours")

    if duration < 60:  # Less than 1 minute
        raise ValueError("Duration too short")

    return True

Troubleshooting

Common Issues

Issue: 403 Forbidden (Toggl)

# Check API token
curl -u "YOUR_TOKEN:api_token" "https://api.track.toggl.com/api/v9/me"

# Ensure token has proper permissions
# Regenerate token at: https://track.toggl.com/profile

Issue: Empty response (RescueTime)

# Check if data exists for date range
# RescueTime only has data when the client is running

# Verify API key
curl "https://www.rescuetime.com/anapi/data?key=YOUR_KEY&format=json"

Issue: Time zone issues

# Always use UTC for Toggl API
from datetime import datetime, timezone

start = datetime.now(timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.000Z")

Issue: Duration calculation

# Running entries have negative duration
entry = toggl.get_current_entry()
if entry and entry.get("duration", 0) < 0:
    # Calculate actual duration
    start = datetime.fromisoformat(entry["start"].replace("Z", "+00:00"))
    duration = (datetime.now(timezone.utc) - start).total_seconds()

Version History

  • 1.0.0 (2026-01-17): Initial release
    • Toggl Track API integration
    • RescueTime API integration
    • Time entry automation
    • Reports and analytics
    • Project attribution
    • Weekly report generator
    • Productivity dashboard
    • GitHub Actions integration
    • Slack integration

Resources


Automate your time tracking workflows with Toggl and RescueTime APIs!

Weekly Installs
1
Installed on
windsurf1
opencode1
codex1
claude-code1
antigravity1
gemini-cli1