Yandex Tracker

v1.0.1

Work with Yandex Tracker (issues, queues, comments, attachments, links, search, bulk operations) via Python yandex_tracker_client. Use when the user asks to...

0· 424·0 current·0 all-time
byАртём Ковальчук@kandler3
Security Scan
VirusTotalVirusTotal
Benign
View report →
OpenClawOpenClaw
Benign
high confidence
Purpose & Capability
Name/description (Yandex Tracker) align with requirements: python3 and a TRACKER_TOKEN plus one org id are exactly what a Tracker client needs. Required binaries and env vars are proportionate to the described capabilities.
Instruction Scope
SKILL.md instructs the agent to write and run self-contained Python scripts that read TRACKER_TOKEN and an org id from environment variables and call the yandex_tracker_client API. The instructions reference /tmp for temporary scripts (reasonable) and do not ask to read unrelated system files or exfiltrate data to unexpected endpoints.
Install Mechanism
Installation is a pip package from PyPI (yandex_tracker_client) which is expected for a Python client. Note: registry metadata said 'no install spec' while SKILL.md contains an install block — a minor inconsistency in metadata but not a functional red flag. Installing a PyPI package writes code to the environment; ensure you trust the package source and run in an isolated environment if needed.
Credentials
Only TRACKER_TOKEN and one org id variable are required. The skill explicitly recommends least-privilege or short-lived tokens. No unrelated credentials or broad environment access are requested.
Persistence & Privilege
The skill is not always-enabled and is user-invocable; it does not request elevated persistence or modify other skills' configurations. Autonomous invocation is allowed (platform default) but is not combined with other suspicious privileges.
Assessment
This skill appears coherent for managing Yandex Tracker via the official Python client. Before installing: 1) Provide a least-privilege TRACKER_TOKEN (prefer short-lived IAM tokens) and the correct org id. 2) Expect the agent to pip-install yandex_tracker_client into its Python environment — if you have policy concerns, run the agent in an isolated environment or preinstall/inspect the package. 3) Confirm where you store TRACKER_TOKEN in openclaw.json and who can read that file. 4) Note the small metadata mismatch (SKILL.md lists a pip install step though the registry said no install spec) — that’s likely benign but worth correcting. 5) Rotate tokens and restrict scopes if you decide to grant access.

Like a lobster shell, security has layers — review code before you run it.

Runtime requirements

📋 Clawdis
Binspython3
EnvTRACKER_TOKEN
issuesvk97cjww5dq56wt36wwc1b5kk7981sqvrlatestvk97ak8ykkfe278ptzkx39ggxyh81s1gvproject-managementvk97cjww5dq56wt36wwc1b5kk7981sqvrpythonvk97cjww5dq56wt36wwc1b5kk7981sqvrtrackervk97cjww5dq56wt36wwc1b5kk7981sqvryandexvk97cjww5dq56wt36wwc1b5kk7981sqvr
424downloads
0stars
2versions
Updated 1mo ago
v1.0.1
MIT-0

Yandex Tracker (Python Client)

Use yandex_tracker_client to interact with Yandex Tracker API v2.

How to work with this skill

Write and execute Python scripts to fulfill user requests. The workflow:

  1. Write a self-contained Python script that initializes the client, performs all needed API calls, aggregates and formats the result, then prints it.
  2. Save to /tmp/tracker_script.py and run: python3 /tmp/tracker_script.py
  3. For simple one-liners it's fine to use python3 -c "...", but prefer a file for anything multi-step.

Aggregation: The API returns lazy iterables — always collect into lists when you need to count, sort, filter, or display summaries. Combine multiple queries in one script (e.g. fetch issues then fetch comments for each, or join data from two queues) rather than making separate tool calls. Print structured output so the result is easy to read.

# Example: aggregate issues by assignee across a queue
issues = list(client.issues.find(filter={'queue': 'QUEUE'}, per_page=100))
from collections import Counter
counts = Counter(str(i.assignee) for i in issues)
for assignee, n in counts.most_common():
    print(f'{n:3d}  {assignee}')

Credentials

Required env (declare in skill metadata; set in openclaw.jsonenv):

  • TRACKER_TOKENRequired. Use a least-privilege OAuth token (oauth.yandex.ru) with only Tracker scope, or a temporary IAM token for Yandex Cloud. Do not use broad admin tokens.
  • One of: TRACKER_ORG_ID (Yandex 360, numeric) or TRACKER_CLOUD_ORG_ID (Yandex Cloud, string)

Client initialization (boilerplate — always include)

import os
from yandex_tracker_client import TrackerClient

token = os.environ['TRACKER_TOKEN']
org_id = os.environ.get('TRACKER_ORG_ID')
cloud_org_id = os.environ.get('TRACKER_CLOUD_ORG_ID')

if cloud_org_id:
    client = TrackerClient(token=token, cloud_org_id=cloud_org_id)
else:
    client = TrackerClient(token=token, org_id=int(org_id))

Get issue by key

issue = client.issues['QUEUE-42']
print(issue.key, issue.summary, issue.status.id, issue.assignee.login if issue.assignee else None)

Custom fields

Real queues almost always have custom fields (story points, business fields, etc.). Their keys look like customFieldId (camelCase) and are queue-specific.

# Read — access by attribute name (same as standard fields)
print(issue.storyPoints)       # returns None if absent
print(issue.as_dict())         # dump all fields including custom ones — use to discover keys

# Update
issue.update(storyPoints=5, myCustomField='value')

# Filter by custom field in find()
issues = client.issues.find(filter={'queue': 'QUEUE', 'storyPoints': {'from': 3}})

# Discover all fields available in a queue (id is the key to use)
for f in client.fields.get_all():
    print(f.id, f.name)

Use issue.as_dict() on a real issue to discover which custom field keys the queue uses before writing update/filter code.

issues.find() — search

# Tracker Query Language string (copy from Tracker UI)
issues = client.issues.find('Queue: QUEUE Assignee: me() Status: inProgress')

# Structured filter (dict)
issues = client.issues.find(
    filter={
        'queue': 'QUEUE',               # queue key
        'assignee': 'user_login',       # login or 'me()'
        'author': 'user_login',
        'status': 'inProgress',         # status .id
        'type': 'bug',                  # issue type .id
        'priority': 'critical',         # priority .id
        'tags': ['backend', 'urgent'],  # all tags must match
        'created': {'from': '2026-01-01', 'to': '2026-02-01'},
        'updated': {'from': '2026-01-15'},
        'deadline': {'to': '2026-03-01'},
        'followers': 'user_login',
        'components': 'component_name',
    },
    order=['-updatedAt', '+priority'],  # prefix: - desc, + asc
    per_page=100,                       # max 100 per page; pagination is automatic
)
# Iterating auto-fetches all pages; wrap in list() to materialise
issues = list(issues)

# Batch fetch specific issues by key
issues = list(client.issues.find(keys=['QUEUE-1', 'QUEUE-2', 'QUEUE-3']))

issues.create()

issue = client.issues.create(
    queue='QUEUE',              # required
    summary='Bug: login fails', # required
    type={'name': 'Bug'},       # or {'id': 'bug'} — use client.issue_types.get_all()
    description='Steps...',
    assignee='user_login',
    priority='critical',        # id: 'blocker','critical','major','normal','minor','trivial'
    followers=['login1', 'login2'],
    tags=['backend', 'urgent'],
    components=['component_name'],
    parent='QUEUE-10',          # parent issue key
    sprint={'id': 123},         # sprint id
)
print(issue.key)

issue.update()

issue.update(
    summary='New title',
    description='Updated text',
    assignee='other_login',
    priority='minor',
    # Lists: pass full replacement OR mutation dict
    tags=['new_tag'],                         # replace entirely
    tags={'add': ['tag1'], 'remove': ['tag2']},  # partial mutation
    followers={'add': ['login1']},
    components={'add': ['comp'], 'remove': []},
)

issue.transitions — status changes

# Always list first — transition IDs are queue-specific, never guess
for t in issue.transitions.get_all():
    print(t.id, t.to.id, t.to.display)

# Execute — all kwargs optional
issue.transitions['close'].execute(
    comment='Fixed in v2.3',
    resolution='fixed',  # 'fixed','wontFix','duplicate','invalid','later' — queue-dependent
)

issue.comments

for c in list(issue.comments.get_all()):
    print(c.id, c.createdBy.login, c.text)

# Create
issue.comments.create(
    text='Fixed in v2.3',
    summonees=['login1', 'login2'],   # triggers @mention notification
    attachments=['path/to/file.png'], # file paths — auto-uploaded, converted to IDs
)

issue.comments[42].update(text='Corrected note', summonees=['login1'])
issue.comments[42].delete()

issue.links

for link in issue.links:
    print(link.type.id, link.direction, link.object.key)

# relationship values (standard):
# 'relates', 'blocks', 'is blocked by',
# 'duplicates', 'is duplicated by',
# 'depends on', 'is dependent of',
# 'is subtask for', 'is parent task for'
issue.links.create(issue='OTHER-10', relationship='relates')
issue.links[42].delete()

issue.attachments

for a in issue.attachments:
    print(a.id, a.name, a.mimetype, a.size)

issue.attachments.create('/path/to/file.txt')   # upload by path
a.download_to('/tmp/')                           # download to dir
issue.attachments[42].delete()

issue.worklog

# Create worklog entry
issue.worklog.create(
    duration='PT1H30M',              # ISO 8601: PT30M, PT2H, P1D, P1DT2H30M
    comment='Fixed auth bug',        # optional
    start='2026-02-24T10:00:00+03:00',  # optional, defaults to now
)

for w in list(issue.worklog.get_all()):
    print(w.id, w.duration, w.comment, w.createdBy.login)

issue.worklog[42].update(duration='PT2H', comment='Revised estimate')
issue.worklog[42].delete()

# Fetch worklogs across multiple issues at once
entries = client.worklog.find(issue=['QUEUE-1', 'QUEUE-2'], createdBy='me()')

Queues

queue = client.queues['QUEUE']
print(queue.key, queue.name, queue.lead.login)

for q in client.queues.get_all():
    print(q.key, q.name)

Bulk operations

# issues arg: list of keys OR list of issue objects from find()

# Bulk update — any issue field as kwarg
bc = client.bulkchange.update(
    ['QUEUE-1', 'QUEUE-2', 'QUEUE-3'],
    priority='minor',
    assignee='user_login',
    tags={'add': ['reviewed'], 'remove': ['draft']},
)
bc.wait()
print(bc.status)  # 'COMPLETE' or 'FAILED'

# Bulk transition — transition id + optional field values
bc = client.bulkchange.transition(
    ['QUEUE-1', 'QUEUE-2'],
    'close',                # transition id
    resolution='wontFix',   # optional extra fields
)
bc.wait()

# Bulk move to another queue
bc = client.bulkchange.move(
    ['QUEUE-1', 'QUEUE-2'],
    'NEWQUEUE',
    move_all_fields=False,        # copy all field values
    move_to_initial_status=False, # reset to initial status
)
bc.wait()

Object fields reference

All objects are dynamic — accessing a missing attribute returns None, not AttributeError.
Call .as_dict() on any object to get a plain dict.

Reference fields (status, priority, assignee, queue, type…) are Reference objects — access .id, .display, .key, or .login directly without a second request.

Issue

AttributeNotes
keystr'QUEUE-42'
summarystr
descriptionstr | None
statusReference → .id e.g. 'inProgress', .display localized name
priorityReference → .id e.g. 'normal' / 'critical'
typeReference → .id e.g. 'bug' / 'task'
queueReference → .key e.g. 'QUEUE'
assigneeReference | None → .login, .display
reporterReference → .login, .display
createdByReference → .login
createdAtstr ISO-8601
updatedAtstr ISO-8601
deadlinestr | None — date '2026-03-01'
tagslist[str]
followerslist[Reference] → each .login
componentslist[Reference] → each .display
fixVersionslist[Reference] → each .display
sprintlist[Reference] | None → each .display
parentReference | None → .key
votesint

Comment

AttributeNotes
idint
textstr
textHtmlstr
createdByReference → .login, .display
createdAt / updatedAtstr ISO-8601
summoneeslist[Reference]
attachmentslist[Reference]

Link

AttributeNotes
idint
typeReference → .id e.g. 'relates', 'blocks', 'is blocked by'
directionstr'inward' / 'outward'
objectReference → .key, .display (the linked issue)
createdByReference → .login
createdAtstr ISO-8601

Attachment

AttributeNotes
idint
namestr — filename
contentstr — download URL
mimetypestr
sizeint — bytes
createdByReference → .login
createdAtstr ISO-8601

Transition

AttributeNotes
idstr — transition key, e.g. 'close', 'start_progress'
toReference → .id, .display (target status)
screenReference | None

Worklog entry

AttributeNotes
idint
issueReference → .key
commentstr | None
startstr ISO-8601 datetime
durationstr ISO-8601 duration e.g. 'PT1H30M'
createdByReference → .login
createdAtstr ISO-8601

Queue

AttributeNotes
idint
keystr
namestr
descriptionstr | None
leadReference → .login, .display
assignAutobool
defaultTypeReference
defaultPriorityReference
teamUserslist[Reference] → each .login

User

AttributeNotes
uidint
loginstr
firstName / lastNamestr
displaystr — full name
emailstr

BulkChange

AttributeNotes
idstr
statusstr'COMPLETE' / 'FAILED' / 'PROCESSING'
statusTextstr
executionChunkPercentint
executionIssuePercentint

Users

# Find user by login, email, or display name — use when user says "assign to Иванов"
for u in client.users.get_all():
    print(u.login, u.display, u.email)

# Get current user
me = client.myself
print(me.login)

Sprints

# Boards list
for b in client.boards.get_all():
    print(b.id, b.name)

# Sprints for a board
for s in client.boards[123].sprints.get_all():
    print(s.id, s.name, s.status)  # status: 'active','closed','draft'

# Assign issue to sprint by id (found above)
issue.update(sprint={'id': 456})

Error handling

from yandex_tracker_client import exceptions

try:
    issue = client.issues['QUEUE-99999']
except exceptions.NotFound:
    print('Issue not found')
except exceptions.Forbidden:
    print('No access to this queue or issue')
except exceptions.BadRequest as e:
    print('Invalid field or value:', e)
except exceptions.Conflict:
    # Concurrent modification — re-fetch and retry
    issue = client.issues['QUEUE-42']
    issue.update(...)

Available exception classes: NotFound, Forbidden, BadRequest, Conflict, TrackerClientError (base).

Notes

  • org_id must be an int; cloud_org_id is a string.
  • For Yandex Cloud orgs: use cloud_org_id= instead of org_id=, and optionally iam_token= for temporary IAM tokens instead of token=.
  • To get valid resolution IDs for a queue: [r.id for r in client.resolutions.get_all()].
  • Print results clearly — use formatted strings, tables, or JSON so the user gets a readable summary.

Comments

Loading comments...