
Wos Export
- 44 installs
- 105 repo stars
- Updated March 13, 2026
- cookjohn/wos-skills
Exports Web of Science records to Zotero, RIS, BibTeX, or Excel by WoS IDs or the current results set.
About
Exports WoS paper records via direct Zotero push or UI file export in multiple formats. A developer uses it to save records to a reference manager or spreadsheet.
- Preferred Zotero push mode needs no UI interaction
- Supports RIS, BibTeX, and Excel file export
Wos Export by the numbers
- 44 all-time installs (skills.sh)
- Ranked #1,120 of 2,715 Automation & Workflows skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/cookjohn/wos-skills --skill wos-exportAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 44 |
|---|---|
| repo stars | ★ 105 |
| Last updated | March 13, 2026 |
| Repository | cookjohn/wos-skills ↗ |
What it does
Exports Web of Science records to Zotero, RIS, BibTeX, or Excel by WoS IDs or the current results set.
Files
WoS Export
Export WoS paper records. Two modes: direct Zotero push (preferred) or file export via UI.
Mode A: Push to Zotero (preferred, no UI interaction)
Collect paper metadata (from prior search/detail results) and push to Zotero via the push_to_zotero.py script.
Step 1: Prepare Paper Data
Build a JSON object from data already available in the conversation (from wos-search API results or wos-paper-detail extraction). The script accepts WoS paper fields directly:
{
"title": "Value co-creation in service logic",
"authors": "Grönroos, C",
"source": "MARKETING THEORY",
"year": 2011,
"volume": "11",
"issue": "3",
"pages": "279-301",
"doi": "10.1177/1470593111408177",
"issn": "1470-5931",
"abstract": "The underpinning logic of...",
"language": "English",
"accessionNumber": "WOS:000295471900004",
"authorKeywords": ["value co-creation", "service logic"],
"keywordsPlus": ["DOMINANT LOGIC"],
"citedCount": "992",
"alldbCited": "1,254",
"jif": "2.8",
"jifYear": "2024",
"jcrQuartile": "Q3",
"researchAreas": "Business & Economics",
"wosCategories": "Business",
"docType": "Article"
}For multiple papers, wrap in an array or {"items": [...]}.
Step 2: Push via Script
echo '{JSON_DATA}' | python "e:\wos-skills\.claude\skills\wos-export\scripts\push_to_zotero.py"Or save to temp file first (recommended for large data or Chinese characters):
python "e:\wos-skills\.claude\skills\wos-export\scripts\push_to_zotero.py" /tmp/wos_export.jsonStep 3: Report Result
OK: Saved (session: xxx)→ successOK: Already saved (session: xxx)→ idempotent, no duplicatesError: Zotero not running→ tell user to start Zotero desktop
The script auto-detects Zotero's currently selected collection. User can change target by selecting a different folder in Zotero before exporting.
To list collections: python push_to_zotero.py --list
Mode B: File Export via UI (fallback)
When Zotero is not available or user specifically wants a file.
Prerequisites
Browser must be on a WoS results page (/summary/...) or full record page (/full-record/...).
Supported Formats
| Format | Menu Item | File |
|---|---|---|
| RIS | "RIS (other reference software)" | .ris |
| BibTeX | "BibTeX" | .bib |
| Excel | "Excel" | .xlsx |
| Plain Text | "Plain text file" | .txt |
| Fast 5000 | "Fast 5000" | Quick export |
Steps
1. take_snapshot → find Export button (button with haspopup="menu" containing "Export") → click 2. take_snapshot → find format menuitem → click 3. Handle export dialog (record range, content options) → click download button
Notes on UI Export
- Uses 3-4 tool calls: snapshot + click Export + snapshot + click format + dialog
- "Fast 5000" exports up to 5000 records with basic fields
- On full-record page, exports only the current record
Zotero Script Features
- Deterministic session ID: Same papers always produce the same session ID (content hash). Prevents duplicates on retry (409 → treated as success).
- WoS metadata mapping: Automatically maps WoS fields to Zotero's journalArticle schema.
- Extra field: Stores WoS-specific data (WoS ID, citation counts, JIF, JCR quartile, research areas) in Zotero's Extra field.
- Author parsing: Handles WoS standard format "LastName, Initials" → Zotero firstName/lastName.
- Keywords: Both Author Keywords and Keywords Plus are added as Zotero tags.
Notes
- Mode A (Zotero push) requires no browser interaction — uses data from prior API calls
- Mode A works even when browser is not on a WoS page
- Script path:
e:\wos-skills\.claude\skills\wos-export\scripts\push_to_zotero.py - Zotero must be running on localhost:23119
#!/usr/bin/env python3
"""Push WoS paper data to Zotero via local Connector API (localhost:23119).
Session strategy: deterministic sessionID derived from content hash.
- 201 = saved successfully
- 409 = SESSION_EXISTS = already saved (idempotent, treat as success)
Input: JSON from stdin or file argument. Accepts either:
1. Raw WoS API record format (from evaluate_script)
2. Pre-built Zotero item format (itemType present)
3. Wrapper with "items" array (direct saveItems format)
"""
import json
import sys
import io
import hashlib
import urllib.request
import urllib.error
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8')
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8')
ZOTERO_API = 'http://127.0.0.1:23119/connector'
HTTP_TIMEOUT = 15
def zotero_request(endpoint, data=None, timeout=HTTP_TIMEOUT):
url = f'{ZOTERO_API}/{endpoint}'
body = json.dumps(data or {}, ensure_ascii=False).encode('utf-8')
req = urllib.request.Request(url, data=body, headers={
'Content-Type': 'application/json',
'X-Zotero-Connector-API-Version': '3'
})
try:
resp = urllib.request.urlopen(req, timeout=timeout)
text = resp.read().decode('utf-8')
return resp.status, json.loads(text) if text else None
except urllib.error.HTTPError as e:
resp_body = e.read().decode('utf-8', errors='replace')
try:
return e.code, json.loads(resp_body) if resp_body else None
except json.JSONDecodeError:
return e.code, {'error': resp_body}
except urllib.error.URLError:
return 0, None
except TimeoutError:
return -1, {'error': f'Timeout ({timeout}s)'}
def make_session_id(items):
key = '|'.join(sorted(item.get('title', '') for item in items))
return hashlib.md5(key.encode('utf-8', errors='surrogateescape')).hexdigest()[:12]
def build_zotero_item(paper):
"""Build Zotero journalArticle item from WoS paper data."""
# Handle authors - accept both string ("A; B; C") and list formats
authors_raw = paper.get('authors', [])
if isinstance(authors_raw, str):
authors_raw = [a.strip() for a in authors_raw.split(';') if a.strip()]
creators = []
for name in authors_raw:
name = name.strip()
if not name:
continue
# WoS standard format: "LastName, FirstInitials" e.g. "Gronroos, C"
if ',' in name:
parts = name.split(',', 1)
creators.append({
'lastName': parts[0].strip(),
'firstName': parts[1].strip(),
'creatorType': 'author'
})
else:
creators.append({'name': name, 'creatorType': 'author'})
# Build date from year + published
date = paper.get('published', '') or str(paper.get('year', ''))
item = {
'itemType': 'journalArticle',
'title': paper.get('title', ''),
'abstractNote': paper.get('abstract', ''),
'date': date,
'language': paper.get('language', 'en'),
'libraryCatalog': 'Web of Science',
'publicationTitle': paper.get('source', ''),
'volume': str(paper.get('volume', '') or ''),
'issue': str(paper.get('issue', '') or ''),
'pages': str(paper.get('pages', '') or ''),
'DOI': paper.get('doi', ''),
'ISSN': paper.get('issn', ''),
'creators': creators,
'tags': [],
'attachments': [],
}
# URL
wos_id = paper.get('accessionNumber', '') or paper.get('wosId', '')
if wos_id:
item['url'] = f'https://www.webofscience.com/wos/woscc/full-record/{wos_id}'
# Keywords as tags
for kw in paper.get('authorKeywords', []):
item['tags'].append({'tag': kw, 'type': 1})
for kw in paper.get('keywordsPlus', []):
item['tags'].append({'tag': kw, 'type': 1})
# Extra field - WoS-specific metadata
extra_parts = []
if wos_id:
extra_parts.append(f'WoS ID: {wos_id}')
cited = paper.get('citedCount', '') or paper.get('citations', '')
if cited:
alldb = paper.get('alldbCited', '') or paper.get('citationsAll', '')
if alldb:
extra_parts.append(f'Cited: {cited} (WOSCC) / {alldb} (All DB)')
else:
extra_parts.append(f'Cited: {cited}')
if paper.get('jif'):
jif_str = f'JIF: {paper["jif"]}'
if paper.get('jifYear'):
jif_str += f' ({paper["jifYear"]})'
extra_parts.append(jif_str)
if paper.get('jcrQuartile'):
extra_parts.append(f'JCR: {paper["jcrQuartile"]}')
if paper.get('researchAreas'):
extra_parts.append(f'Research Areas: {paper["researchAreas"]}')
if paper.get('wosCategories'):
extra_parts.append(f'WoS Categories: {paper["wosCategories"]}')
if paper.get('docType'):
extra_parts.append(f'Document Type: {paper["docType"]}')
if extra_parts:
item['extra'] = '\n'.join(extra_parts)
return item
def save_items(items, uri=''):
session_id = make_session_id(items)
for i, item in enumerate(items):
if 'id' not in item:
item['id'] = f'wos_{session_id}_{i}'
data = {'sessionID': session_id, 'uri': uri, 'items': items}
status, resp = zotero_request('saveItems', data)
if status == 201:
return 201, f'Saved (session: {session_id})'
elif status == 409:
return 201, f'Already saved (session: {session_id})'
elif status == 500:
if resp and 'libraryEditable' in str(resp):
return 500, 'Library is read-only'
return 500, f'Zotero error: {resp}'
elif status == 0:
return 0, 'Zotero not running'
elif status == -1:
return -1, f'Timeout ({HTTP_TIMEOUT}s)'
else:
return status, f'HTTP {status}'
def main():
if len(sys.argv) > 1 and sys.argv[1] == '--list':
status, data = zotero_request('getSelectedCollection')
if status != 200 or not data:
print('Error: Cannot connect to Zotero.')
sys.exit(1)
print(f'Current collection: {data.get("name", "?")}')
for t in data.get('targets', []):
indent = ' ' * t.get('level', 0)
print(f' {indent}{t["name"]} (ID: {t["id"]})')
return
# Check Zotero
status, _ = zotero_request('ping')
if status == 0:
print('Error: Zotero not running.')
sys.exit(1)
col_status, col = zotero_request('getSelectedCollection')
if col_status == 200 and col:
print(f'Zotero collection: {col.get("name", "?")}')
# Read input
if len(sys.argv) > 1 and sys.argv[1] != '--list':
with open(sys.argv[1], 'r', encoding='utf-8') as f:
paper_data = json.load(f)
else:
paper_data = json.load(sys.stdin)
# Handle wrapper format {"items": [...]}
if isinstance(paper_data, dict) and 'items' in paper_data:
items = paper_data['items']
# If items already have itemType, use directly
if items and 'itemType' in items[0]:
status, msg = save_items(items, paper_data.get('uri', ''))
else:
built = [build_zotero_item(p) for p in items]
status, msg = save_items(built, paper_data.get('uri', ''))
if status == 201:
print(f'OK: {msg} ({len(items)} papers)')
else:
print(f'Error: {msg}')
sys.exit(1)
return
# Handle single or array
papers = paper_data if isinstance(paper_data, list) else [paper_data]
items = []
for p in papers:
if 'itemType' in p:
items.append(p)
else:
items.append(build_zotero_item(p))
if not items:
print('Error: No valid paper data.')
sys.exit(1)
uri = papers[0].get('url', '')
status, msg = save_items(items, uri)
if status == 201:
print(f'OK: {msg} ({len(items)} papers)')
for item in items:
print(f' - {item.get("title", "?")}')
else:
print(f'Error: {msg}')
sys.exit(1)
if __name__ == '__main__':
main()