Install
$ agentstack add skill-sage-bionetworks-agent-skills-synapse-data-curation ✓ scanned · ✓ verified, works with Claude Code, Cursor, and more.
Security review
✓ PassedNo issues found. Passed automated security review. · v0.1.0 How review works →
- ✓ Prompt-injection patterns
- ✓ Secret / credential exfiltration
- ✓ Dangerous shell & filesystem operations
- ✓ Untrusted network calls
- ✓ Known-malicious package signatures
What it can access
- ● Network access Used
- ✓ Filesystem access No
- ✓ Shell / process execution No
- ● Environment & secrets Used
- ✓ Dynamic code execution No
From automated source analysis of v0.1.0. “Used” means the capability is present in the source — more access means more to trust, not that it’s unsafe.
Verified badge
Passed review? Show it. Paste this badge into your README, it links to the public security report.
Reliability & compatibility
Declared compatibility
Compatibility is declared by the source manifest. End-to-end runtime verification is coming, see below.
We're building live execution health for every listing: tool-call success rate, median latency, uptime, and last-checked timestamps, measured, not self-reported. It isn't live yet, so we don't show numbers we can't stand behind.
How agent discovery & health will work →About
Synapse Data Curation
Overview
Advanced patterns for curating data in Synapse (synapse.org) via the REST API. Covers the annotations2 endpoint with etag/optimistic-locking, Dataset entity creation with dynamically-derived column schemas, JSON Schema binding for metadata validation, team permissions, and wiki management. Complements the synapse-python-client skill, which covers the high-level synapseclient.models SDK.
Installation
pip install synapseclient pyyaml
Authentication
import synapseclient
# Via environment variable (recommended for automated workflows)
syn = synapseclient.Synapse()
syn.login(authToken=os.environ["SYNAPSE_AUTH_TOKEN"], silent=True)
# Via cached credentials (~/.synapseConfig)
syn = synapseclient.Synapse()
syn.login(silent=True)
Core Concepts
- annotations2: The REST endpoint for reading and writing entity annotations. Uses etag-based optimistic locking — always re-fetch before PUT.
- Dataset entity:
org.sagebionetworks.repo.model.table.Dataset— a typed table entity that must be a direct child of its project and whosecolumnIdsshould be derived dynamically from the annotation keys present on its files. - Schema binding: JSON Schema is bound to a files folder (not the Dataset entity) to enable validation in Curator Grid.
- 412 Precondition Failed: The server rejects a PUT when your etag is stale. Always retry after re-fetching.
Common Operations
Reading and Writing Annotations
import json, time
def set_annotations(syn, entity_id: str, fields: dict, max_attempts: int = 3):
"""Set annotations on an entity, handling 412 stale-etag retries."""
for attempt in range(max_attempts):
try:
ann = syn.restGET(f"/entity/{entity_id}/annotations2")
for k, v in fields.items():
if isinstance(v, list):
ann["annotations"][k] = {"type": "STRING", "value": v}
elif isinstance(v, str):
ann["annotations"][k] = {"type": "STRING", "value": [v]}
elif isinstance(v, int):
ann["annotations"][k] = {"type": "INTEGER", "value": [v]}
elif isinstance(v, float):
ann["annotations"][k] = {"type": "DOUBLE", "value": [v]}
syn.restPUT(f"/entity/{entity_id}/annotations2", json.dumps(ann))
return
except Exception as e:
if "412" in str(e) and attempt str:
"""Create a Dataset entity as a direct child of project_id."""
# 1. Collect all annotation keys from the files
all_keys = {}
for f in files:
fa = syn.restGET(f"/entity/{f['id']}/annotations2")
for k, v in fa.get("annotations", {}).items():
if k not in all_keys:
all_keys[k] = v
# 2. Create columns (LONG must map to INTEGER — Synapse has no LONG type)
col_ids = []
for col_name in sorted(all_keys):
if col_name in EXCLUDE_COLS:
continue
val = all_keys[col_name]
val_type = val.get("type", "STRING") if isinstance(val, dict) else "STRING"
col_type = {"LONG": "INTEGER"}.get(val_type, val_type) \
if val_type in ("DOUBLE", "INTEGER", "LONG") else "STRING"
col_body = {
"name": col_name,
"columnType": col_type,
"maximumSize": 1000 if col_name in HIGH_CARDINALITY else 250,
}
if col_name in FACET_COLS and col_type == "STRING":
col_body["facetType"] = "enumeration"
col_ids.append(syn.restPOST("/column", json.dumps(col_body))["id"])
# 3. Create Dataset
body = {
"name": dataset_name,
"parentId": project_id,
"concreteType": "org.sagebionetworks.repo.model.table.Dataset",
"columnIds": col_ids,
"items": [{"entityId": f["id"], "versionNumber": 1} for f in files],
}
dataset = syn.restPOST("/entity", json.dumps(body))
dataset_id = dataset["id"]
# 4. Set annotations
set_annotations(syn, dataset_id, dataset_annotations)
# 5. Mint a stable version
syn.restPOST(f"/entity/{dataset_id}/version", json.dumps({}))
return dataset_id
When updating an existing Dataset, always update annotations2 before the entity PUT to avoid a cascading 412:
for attempt in range(3):
try:
ds_ann = syn.restGET(f"/entity/{dataset_id}/annotations2")
syn.restPUT(f"/entity/{dataset_id}/annotations2", json.dumps(ds_ann)) # refresh etag
ds = syn.restGET(f"/entity/{dataset_id}")
ds["columnIds"] = new_col_ids
syn.restPUT(f"/entity/{dataset_id}", json.dumps(ds))
break
except Exception as e:
if "412" in str(e) and attempt dict:
"""Bind a JSON Schema to a folder for metadata validation."""
body = {
"concreteType": "org.sagebionetworks.repo.model.schema.BindSchemaToEntityRequest",
"entityId": folder_id,
"schema$id": schema_uri,
"enableDerivedAnnotations": False,
}
return syn.restPUT(f"/entity/{folder_id}/schema/binding", json.dumps(body))
def fetch_schema_properties(syn, schema_uri: str) -> dict:
"""Return the schema's property definitions including enum constraints."""
schema_id = schema_uri.split("/")[-1]
result = syn.restGET(f"/schema/type/registered/{schema_id}")
return result.get("validationSchema", {}).get("properties", {})
# Bind schema to files folder (NOT the Dataset entity or project)
bind_json_schema(syn, "org.foo.something-v1.0.0", files_folder_id)
props = fetch_schema_properties(syn, "org.foo.something-v1.0.0")
# props["assay"]["enum"] → list of valid assay values; [] means no valid values → skip field
If a field's enum list is empty ([]), do not set it — no valid values exist in the current schema version.
Team Permissions
syn.setPermissions(
project_id,
principalId=team_id,
accessType=["READ", "DOWNLOAD", "CREATE", "UPDATE", "DELETE",
"CHANGE_PERMISSIONS", "CHANGE_SETTINGS", "MODERATE",
"UPDATE_SUBMISSION", "READ_PRIVATE_SUBMISSION"],
warn_if_inherits=False,
)
Wiki Management
import json
# Create
wiki = synapseclient.Wiki(title=project_name, owner=project_id, markdown=content)
syn.store(wiki)
# Update via REST (avoids object overhead)
wiki = syn.restGET(f"/entity/{project_id}/wiki")
wiki["markdown"] = updated_content
syn.restPUT(f"/entity/{project_id}/wiki/{wiki['id']}", json.dumps(wiki))
Best Practices
- Never set
resourceStatuson File entities — it belongs only on Project and Dataset entities. Setting it on files creates a spurious column in portal Dataset views. - Never add a custom
filenameannotation to File entities — the Synapsenameproperty is the filename column in Dataset views; a duplicate annotation creates a second column. columnIdsare always derived dynamically from actual file annotation keys. A hardcoded list goes stale as annotations evolve.- Portal tables are read-only — never mutate the live studies, files, or datasets portal tables.
syn.restPUT()cannot move entities — use the REST API directly for structural changes.- Use
path=url, synapseStore=Falsewhen creating File entities for externally-hosted files (do not useexternalURL=). - Official REST API docs: https://rest-docs.synapse.org/rest/
- Python client docs: https://python-docs.synapse.org/
Source & license
This open-source skill is cataloged on AgentStack and links to its original source — we do not rehost the code.
- Author: Sage-Bionetworks
- Source: Sage-Bionetworks/agent-skills
- License: Apache-2.0
Install and usage instructions live in the source repository linked above.
Reviews
No reviews yet, be the first.
Write a review
Versions
- v0.1.0 Imported from the upstream source.