Back to skills
SkillHub ClubAnalyze Data & AIFull StackBackendData / AI

youtrack

Interact with YouTrack project management system via REST API. Read projects and issues, create tasks, generate invoices from time tracking data, and manage knowledge base articles. Use for reading projects and work items, creating or updating issues, generating client invoices from time tracking, and working with knowledge base articles.

Packaged view

This page reorganizes the original catalog entry around fit, installability, and workflow context first. The original raw source lives below.

Stars
3,086
Hot score
99
Updated
March 20, 2026
Overall rating
C4.0
Composite score
4.0
Best-practice grade
B73.6

Install command

npx @skill-hub/cli install openclaw-skills-youtrack-digisal

Repository

openclaw/skills

Skill path: skills/digisal/youtrack-digisal

Interact with YouTrack project management system via REST API. Read projects and issues, create tasks, generate invoices from time tracking data, and manage knowledge base articles. Use for reading projects and work items, creating or updating issues, generating client invoices from time tracking, and working with knowledge base articles.

Open repository

Best for

Primary workflow: Analyze Data & AI.

Technical facets: Full Stack, Backend, Data / AI.

Target audience: everyone.

License: Unknown.

Original source

Catalog source: SkillHub Club.

Repository owner: openclaw.

This is still a mirrored public skill entry. Review the repository before installing into production workflows.

What it helps with

  • Install youtrack into Claude Code, Codex CLI, Gemini CLI, or OpenCode workflows
  • Review https://github.com/openclaw/skills before adding youtrack to shared team environments
  • Use youtrack for development workflows

Works across

Claude CodeCodex CLIGemini CLIOpenCode

Favorites: 0.

Sub-skills: 0.

Aggregator: No.

Original source / Raw SKILL.md

---
name: youtrack
description: Interact with YouTrack project management system via REST API. Read projects and issues, create tasks, generate invoices from time tracking data, and manage knowledge base articles. Use for reading projects and work items, creating or updating issues, generating client invoices from time tracking, and working with knowledge base articles.
---

# YouTrack

YouTrack integration for project management, time tracking, and knowledge base.

## Quick Start

### Authentication

To generate a permanent token:
1. From the main navigation menu, select **Administration** > **Access Management** > **Users**
2. Find your user and click to open settings
3. Generate a new permanent API token
4. Set the token as an environment variable:

```bash
export YOUTRACK_TOKEN=your-permanent-token-here
```

**Important:** Configure your hourly rate (default $100/hour) by passing `--rate` to invoice_generator.py or updating `hourly_rate` parameter in your code.

Then use any YouTrack script:

```bash
# List all projects
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-projects

# List issues in a project
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "project: MyProject"

# Generate invoice for a project
python3 scripts/invoice_generator.py --url https://your-instance.youtrack.cloud --project MyProject --month "January 2026" --from-date "2026-01-01"
```

## Python Scripts

### `scripts/youtrack_api.py`

Core API client for all YouTrack operations.

**In your Python code:**
```python
from youtrack_api import YouTrackAPI

api = YouTrackAPI('https://your-instance.youtrack.cloud', token='your-token')

# Projects
projects = api.get_projects()
project = api.get_project('project-id')

# Issues
issues = api.get_issues(query='project: MyProject')
issue = api.get_issue('issue-id')

# Create issue
api.create_issue('project-id', 'Summary', 'Description')

# Work items (time tracking)
work_items = api.get_work_items('issue-id')
issue_with_time = api.get_issue_with_work_items('issue-id')

# Knowledge base
articles = api.get_articles()
article = api.get_article('article-id')
api.create_article('project-id', 'Title', 'Content')
```

**CLI usage:**
```bash
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud \
    --token YOUR_TOKEN \
    --list-projects

python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud \
    --get-issue ABC-123

python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud \
    --get-articles
```

### `scripts/invoice_generator.py`

Generate client invoices from time tracking data.

**In your Python code:**
```python
from youtrack_api import YouTrackAPI
from invoice_generator import InvoiceGenerator

api = YouTrackAPI('https://your-instance.youtrack.cloud', token='your-token')
generator = InvoiceGenerator(api, hourly_rate=100.0)

# Get time data for a project
project_data = generator.get_project_time_data('project-id', from_date='2026-01-01')

# Generate invoice
invoice_text = generator.generate_invoice_text(project_data, month='January 2026')
print(invoice_text)
```

**CLI usage:**
```bash
python3 scripts/invoice_generator.py \
    --url https://your-instance.youtrack.cloud \
    --project MyProject \
    --from-date 2026-01-01 \
    --month "January 2026" \
    --rate 100 \
    --format text
```

Save the text output and print to PDF for clients.

## Common Workflows

### 1. List All Projects

```bash
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-projects
```

### 2. Find Issues in a Project

```bash
# All issues in a project
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "project: MyProject"

# Issues updated since a date
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "project: MyProject updated >= 2026-01-01"

# Issues assigned to you
python3 scripts/youtrack_api.py --url https://your-instance.youtrack.cloud --list-issues "assignee: me"
```

### 3. Create a New Issue

```python
from youtrack_api import YouTrackAPI

api = YouTrackAPI('https://your-instance.youtrack.cloud')
api.create_issue(
    project_id='MyProject',
    summary='Task title',
    description='Task description'
)
```

### 4. Generate Monthly Invoice

```bash
# Generate invoice for January 2026
python3 scripts/invoice_generator.py \
    --url https://your-instance.youtrack.cloud \
    --project ClientProject \
    --from-date 2026-01-01 \
    --month "January 2026" \
    --rate 100 \
    --format text > invoice.txt
```

Save the text output and print to PDF for clients.

### 5. Read Knowledge Base

```python
from youtrack_api import YouTrackAPI

api = YouTrackAPI('https://your-instance.youtrack.cloud')

# All articles
articles = api.get_articles()

# Articles for specific project
articles = api.get_articles(project_id='MyProject')

# Get specific article
article = api.get_article('article-id')
```

## Billing Logic

Invoice generator uses this calculation:

1. Sum all time tracked per issue (in minutes)
2. Convert to 30-minute increments (round up)
3. Minimum charge is 30 minutes (at configured rate/2)
4. Multiply by rate (default $100/hour = $50 per half-hour)

Examples:
- 15 minutes → $50 (30 min minimum)
- 35 minutes → $100 (rounded to 60 min)
- 60 minutes → $100
- 67 minutes → $150 (rounded to 90 min)

## Environment Variables

- `YOUTRACK_TOKEN`: Your permanent API token (recommended over passing as argument)
- Set with `export YOUTRACK_TOKEN=your-token`

## API Details

See `REFERENCES.md` for:
- Complete API endpoint documentation
- Query language examples
- Field IDs and structures

## Error Handling

Scripts will raise errors for:
- Missing or invalid token
- Network issues
- API errors (404, 403, etc.)

Check stderr for error details.


---

## Referenced Files

> The following files are referenced in this skill and included for context.

### scripts/youtrack_api.py

```python
#!/usr/bin/env python3
"""
YouTrack REST API Client
Handles authentication and basic API calls for YouTrack Cloud instances.
"""

import os
import sys
import json
import argparse
from urllib.parse import urljoin
from typing import Optional, Dict, List, Any
import urllib.request
import urllib.error
from datetime import datetime


class YouTrackAPI:
    """Simple YouTrack REST API client."""

    def __init__(self, base_url: str, token: Optional[str] = None):
        """
        Initialize YouTrack API client.

        Args:
            base_url: Your YouTrack instance URL (e.g., https://sl.youtrack.cloud)
            token: Permanent API token (or set YOUTRACK_TOKEN env var)
        """
        # Normalize base URL
        self.base_url = base_url.rstrip('/')
        self.token = token or os.environ.get('YOUTRACK_TOKEN')

        if not self.token:
            raise ValueError(
                "YouTrack token required. Set YOUTRACK_TOKEN env var or pass as argument."
            )

        # Set up headers with bearer token auth
        self.headers = {
            'Authorization': f'Bearer {self.token}',
            'Accept': 'application/json',
            'Content-Type': 'application/json'
        }

    def _make_request(self, method: str, endpoint: str, data: Optional[Dict] = None) -> Dict[str, Any]:
        """
        Make an authenticated API request.

        Args:
            method: HTTP method (GET, POST, PUT, DELETE)
            endpoint: API endpoint (e.g., '/api/issues')
            data: Request body for POST/PUT

        Returns:
            Parsed JSON response
        """
        url = urljoin(self.base_url, endpoint)

        req_data = None
        if data is not None:
            req_data = json.dumps(data).encode('utf-8')

        req = urllib.request.Request(
            url,
            data=req_data,
            headers=self.headers,
            method=method
        )

        try:
            with urllib.request.urlopen(req) as response:
                if response.status >= 400:
                    error_body = response.read().decode('utf-8')
                    raise RuntimeError(f"API Error {response.status}: {error_body}")

                result = response.read().decode('utf-8')
                if result:
                    return json.loads(result)
                return {}
        except urllib.error.HTTPError as e:
            error_body = e.read().decode('utf-8') if e.fp else ''
            raise RuntimeError(f"HTTP Error {e.code}: {error_body}")
        except Exception as e:
            raise RuntimeError(f"Request failed: {e}")

    # Projects

    def get_projects(self) -> List[Dict]:
        """Get all projects."""
        result = self._make_request('GET', '/api/admin/projects?fields=id,name,shortName')
        return result if isinstance(result, list) else []

    def get_project(self, project_id: str) -> Dict:
        """Get a specific project by ID."""
        return self._make_request('GET', f'/api/admin/projects/{project_id}?fields=id,name,shortName,description')

    # Issues

    def get_issues(self, query: Optional[str] = None, fields: str = 'id,summary,description,created,updated,project(id,name),customFields(name,value)') -> List[Dict]:
        """
        Get issues, optionally filtered by a query.

        Args:
            query: YouTrack query language (e.g., 'project: MyProject')
            fields: Comma-separated list of fields to return

        Returns:
            List of issues
        """
        params = {'fields': fields}
        if query:
            params['query'] = query

        # Build query string
        query_string = '&'.join(f'{k}={urllib.parse.quote(str(v))}' for k, v in params.items())
        endpoint = f'/api/issues?{query_string}'

        result = self._make_request('GET', endpoint)
        return result if isinstance(result, list) else []

    def get_issue(self, issue_id: str) -> Dict:
        """Get a specific issue by ID."""
        return self._make_request('GET', f'/api/issues/{issue_id}')

    def create_issue(self, project_id: str, summary: str, description: str = '') -> Dict:
        """
        Create a new issue.

        Args:
            project_id: Project ID or short name
            summary: Issue summary
            description: Issue description

        Returns:
            Created issue
        """
        data = {
            'project': {'id': project_id},
            'summary': summary,
            'description': description
        }
        return self._make_request('POST', '/api/issues', data)

    def update_issue(self, issue_id: str, summary: Optional[str] = None, description: Optional[str] = None) -> Dict:
        """Update an issue's summary and/or description."""
        data = {}
        if summary is not None:
            data['summary'] = summary
        if description is not None:
            data['description'] = description
        return self._make_request('POST', f'/api/issues/{issue_id}', data)

    # Time Tracking

    def get_work_items(self, issue_id: str) -> List[Dict]:
        """Get all work items (time entries) for an issue."""
        result = self._make_request('GET', f'/api/issues/{issue_id}/timeTracking/workItems?fields=id,date,duration(minutes),author(name),text')
        # Convert date from milliseconds to ISO format
        for wi in result:
            if 'date' in wi and wi['date']:
                wi['date'] = datetime.fromtimestamp(wi['date'] / 1000).isoformat()
        return result if isinstance(result, list) else []

    def get_issue_with_work_items(self, issue_id: str) -> Dict:
        """Get an issue with all its work items included."""
        issue = self.get_issue(issue_id)
        work_items = self.get_work_items(issue_id)
        issue['workItems'] = work_items
        return issue

    # Knowledge Base (Articles)

    def get_articles(self, project_id: Optional[str] = None) -> List[Dict]:
        """
        Get knowledge base articles.

        Args:
            project_id: Optional project ID to filter by

        Returns:
            List of articles
        """
        endpoint = '/api/articles'
        if project_id:
            endpoint += f'?project={project_id}'

        result = self._make_request('GET', endpoint)
        return result if isinstance(result, list) else []

    def get_article(self, article_id: str) -> Dict:
        """Get a specific article by ID."""
        return self._make_request('GET', f'/api/articles/{article_id}')

    def create_article(self, project_id: str, title: str, content: str) -> Dict:
        """
        Create a new knowledge base article.

        Args:
            project_id: Project ID
            title: Article title
            content: Article content

        Returns:
            Created article
        """
        data = {
            'project': {'id': project_id},
            'title': title,
            'content': content
        }
        return self._make_request('POST', '/api/articles', data)


def main():
    """CLI interface for testing the YouTrack API."""
    parser = argparse.ArgumentParser(description='YouTrack API Client')
    parser.add_argument('--url', required=True, help='YouTrack instance URL')
    parser.add_argument('--token', help='API token (or set YOUTRACK_TOKEN env var)')
    parser.add_argument('--list-projects', action='store_true', help='List all projects')
    parser.add_argument('--list-issues', help='List issues (optional query)')
    parser.add_argument('--get-issue', help='Get specific issue ID')
    parser.add_argument('--get-articles', action='store_true', help='List articles')

    args = parser.parse_args()

    try:
        api = YouTrackAPI(args.url, args.token)

        if args.list_projects:
            projects = api.get_projects()
            print(json.dumps(projects, indent=2))
        elif args.list_issues is not None:
            issues = api.get_issues(query=args.list_issues)
            print(json.dumps(issues, indent=2))
        elif args.get_issue:
            issue = api.get_issue_with_work_items(args.get_issue)
            print(json.dumps(issue, indent=2))
        elif args.get_articles:
            articles = api.get_articles()
            print(json.dumps(articles, indent=2))
        else:
            print("No action specified. Use --help for options.")
    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()

```

### scripts/invoice_generator.py

```python
#!/usr/bin/env python3
"""
YouTrack Invoice Generator
Generates invoices from time tracking data in YouTrack projects.
"""

import os
import sys
import json
import argparse
from datetime import datetime, timedelta
from typing import List, Dict, Any, Optional
from youtrack_api import YouTrackAPI


class InvoiceGenerator:
    """Generate invoices from YouTrack time tracking data."""

    def __init__(self, api: YouTrackAPI, hourly_rate: float = 100.0):
        """
        Initialize invoice generator.

        Args:
            api: YouTrackAPI instance
            hourly_rate: Rate per hour (default $100)
        """
        self.api = api
        self.hourly_rate = hourly_rate
        self.rate_per_half_hour = hourly_rate / 2

    def get_project_time_data(self, project_id: str, from_date: Optional[str] = None) -> Dict[str, Any]:
        """
        Get time tracking data for a project.

        Args:
            project_id: Project ID
            from_date: Optional start date (not currently supported in REST API)

        Returns:
            Dictionary with time data per issue
        """
        # Build query for project issues
        # Note: Date filtering with 'updated:' syntax not supported in REST API
        # Use 'updated >= YYYY-MM-DD' format instead
        query = f'project: {project_id}'
        if from_date:
            query += f' updated >= {from_date}'

        issues = self.api.get_issues(query=query)

        project_data = {
            'project': None,
            'issues': [],
            'total_minutes': 0,
            'total_hours': 0,
            'total_cost': 0
        }

        # Get project info
        try:
            project_data['project'] = self.api.get_project(project_id)
        except:
            # Fallback: use project name from first issue
            if issues:
                project_data['project'] = {'name': issues[0].get('project', {}).get('name', project_id)}

        for issue in issues:
            issue_id = issue.get('id')
            issue_with_time = self.api.get_issue_with_work_items(issue_id)

            work_items = issue_with_time.get('workItems', [])
            total_minutes = sum(
                wi.get('duration', {}).get('minutes', 0)
                for wi in work_items
            )

            if total_minutes > 0:
                hours = total_minutes / 60
                cost = self._calculate_cost(total_minutes)

                project_data['issues'].append({
                    'id': issue_id,
                    'summary': issue.get('summary', 'No summary'),
                    'description': issue.get('description', ''),
                    'work_items': work_items,
                    'total_minutes': total_minutes,
                    'total_hours': hours,
                    'cost': cost
                })

                project_data['total_minutes'] += total_minutes
                project_data['total_cost'] += cost

        project_data['total_hours'] = project_data['total_minutes'] / 60

        return project_data

    def _calculate_cost(self, minutes: int) -> float:
        """
        Calculate cost based on time, billed in 30-minute increments.

        Args:
            minutes: Total minutes

        Returns:
            Total cost
        """
        # Convert to 30-minute increments, round up
        half_hour_units = (minutes + 29) // 30
        # Minimum 30 minutes (1 half-hour unit)
        half_hour_units = max(half_hour_units, 1)
        return half_hour_units * self.rate_per_half_hour

    def generate_invoice_text(self, project_data: Dict[str, Any], month: Optional[str] = None) -> str:
        """
        Generate invoice as plain text (can be printed to PDF).

        Args:
            project_data: Project data from get_project_time_data()
            month: Optional month label (e.g., "January 2026")

        Returns:
            Invoice text
        """
        project = project_data['project'] or {}
        project_name = project.get('name', 'Unknown Project')

        lines = []
        lines.append("=" * 70)
        lines.append(f"INVOICE - {project_name}")

        if month:
            lines.append(f"Period: {month}")

        lines.append("")
        lines.append("WORK ITEMS")
        lines.append("-" * 70)

        for issue in project_data['issues']:
            lines.append("")
            lines.append(f"Task: {issue['summary']}")
            lines.append(f"ID: {issue['id']}")

            if issue['description']:
                desc = issue['description'][:200] + "..." if len(issue['description']) > 200 else issue['description']
                lines.append(f"Description: {desc}")

            for wi in issue['work_items']:
                duration = wi.get('duration', {})
                mins = duration.get('minutes', 0)
                date = wi.get('date', '')
                author = wi.get('author', {}).get('name', 'Unknown')
                lines.append(f"  - {date}: {mins} min ({author})")

            lines.append(f"  Task total: {issue['total_hours']:.2f} hours (${issue['cost']:.2f})")

        lines.append("")
        lines.append("-" * 70)
        lines.append(f"TOTAL: {project_data['total_hours']:.2f} hours")
        lines.append(f"TOTAL COST: ${project_data['total_cost']:.2f}")
        lines.append("=" * 70)

        return "\n".join(lines)

    def generate_invoice_json(self, project_data: Dict[str, Any], month: Optional[str] = None) -> str:
        """
        Generate invoice as JSON for programmatic use.

        Args:
            project_data: Project data from get_project_time_data()
            month: Optional month label

        Returns:
            JSON string
        """
        invoice = {
            'project': project_data['project'],
            'period': month,
            'items': project_data['issues'],
            'summary': {
                'total_minutes': project_data['total_minutes'],
                'total_hours': project_data['total_hours'],
                'total_cost': project_data['total_cost']
            }
        }
        return json.dumps(invoice, indent=2)


def main():
    """CLI interface for generating invoices."""
    parser = argparse.ArgumentParser(description='YouTrack Invoice Generator')
    parser.add_argument('--url', required=True, help='YouTrack instance URL')
    parser.add_argument('--token', help='API token (or set YOUTRACK_TOKEN env var)')
    parser.add_argument('--project', required=True, help='Project ID to generate invoice for')
    parser.add_argument('--from-date', help='Start date (YYYY-MM-DD)')
    parser.add_argument('--month', help='Month label (e.g., "January 2026")')
    parser.add_argument('--rate', type=float, default=100.0, help='Hourly rate (default: 100)')
    parser.add_argument('--format', choices=['text', 'json'], default='text', help='Output format')

    args = parser.parse_args()

    try:
        api = YouTrackAPI(args.url, args.token)
        generator = InvoiceGenerator(api, hourly_rate=args.rate)

        project_data = generator.get_project_time_data(args.project, args.from_date)

        if args.format == 'text':
            output = generator.generate_invoice_text(project_data, args.month)
        else:
            output = generator.generate_invoice_json(project_data, args.month)

        print(output)

    except Exception as e:
        print(f"Error: {e}", file=sys.stderr)
        sys.exit(1)


if __name__ == '__main__':
    main()

```



---

## Skill Companion Files

> Additional files collected from the skill directory layout.

### _meta.json

```json
{
  "owner": "digisal",
  "slug": "youtrack-digisal",
  "displayName": "YouTrack Project Management",
  "latest": {
    "version": "1.0.1",
    "publishedAt": 1769572337862,
    "commit": "https://github.com/clawdbot/skills/commit/62ea9dc7577ed1e4c2a99c7d470bc5afbc58d356"
  },
  "history": []
}

```

youtrack | SkillHub